allow multiple saml auth configs and full aac api

An account can now have multiple SAML configurations, and
can set an auth discovery url.

The old AAC API has been deprecated and this adds a normal
resource API for AACs

Test Plan:
 * Test the api be doing lots of things
 * Create two saml configurations
 * Test the individual login urls for each (/login/{id}) and verify they work
 * Test that the new SAML AAC UI works.
 * Test that the SAML configuration in position 1 is used as the default

closes #10497

Change-Id: Ibe35fcf788d9506542b1079cc7420912a1e9d9a2
Reviewed-on: https://gerrit.instructure.com/14042
Tested-by: Jenkins <jenkins@instructure.com>
Reviewed-by: Cody Cutrer <cody@instructure.com>
This commit is contained in:
Bracken Mosbacker 2012-09-28 16:02:02 -06:00
parent 82d7318c7d
commit c54d3060b2
20 changed files with 1453 additions and 292 deletions

View File

@ -52,7 +52,7 @@ gem 'rotp', '1.4.1'
gem 'rqrcode', '0.4.2'
gem 'rscribd', '1.2.0'
gem 'net-ldap', '0.3.1', :require => 'net/ldap'
gem 'ruby-saml-mod', '0.1.18'
gem 'ruby-saml-mod', '0.1.19'
gem 'rubycas-client', '2.2.1'
gem 'rubyzip', '0.9.4', :require => 'zip/zip'
gem 'sanitize', '2.0.3'

View File

@ -46,7 +46,7 @@ define [
toggleRegion = ($region, showRegion) ->
showRegion ?= ($region.is(':ui-dialog:hidden') || ($region.attr('aria-expanded') != 'true'))
$allElementsControllingRegion = $(".element_toggler[aria-controls=#{$region.attr('id')}]")
$allElementsControllingRegion = $("[aria-controls=#{$region.attr('id')}]")
# hide/un-hide .element_toggler's that point to this $region that were hidden because they have
# the data-hide-while-target-shown attribute

View File

@ -17,23 +17,72 @@
#
# @API Account Authentication Services
#
# @object AccountAuthorizationConfig
# // SAML configuration
# {
# "login_handle_name":null,
# "identifier_format":"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
# "auth_type":"saml",
# "id":1649,
# "log_out_url":"http://example.com/saml1/slo",
# "log_in_url":"http://example.com/saml1/sli",
# "certificate_fingerprint":"111222",
# "change_password_url":null,
# "requested_authn_context":null,
# "position":1,
# "idp_entity_id":"http://example.com/saml1",
# "login_attribute":"nameid"
# }
# // LDAP configuration
# {
# "auth_type":"ldap",
# "id":1650,
# "auth_host":"127.0.0.1",
# "auth_filter":"filter1",
# "auth_over_tls":null,
# "position":1,
# "auth_base":null,
# "auth_username":"username1",
# "auth_port":null
# }
# // CAS configuration
# {
# "login_handle_name":null,
# "auth_type":"cas",
# "id":1651,
# "log_in_url":null,
# "position":1,
# "auth_base":"127.0.0.1"
# }
class AccountAuthorizationConfigsController < ApplicationController
before_filter :require_context, :require_root_account_management
include Api::V1::AccountAuthorizationConfig
# @API List Authorization Configs
# Returns the list of authorization configs
#
# @example_request
#
# curl 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs' \
# -H 'Authorization: Bearer <token>'
#
# @returns [AccountAuthorizationConfig]
def index
@account_configs = @account.account_authorization_configs.to_a
while @account_configs.length < 2
@account_configs << @account.account_authorization_configs.new
@account_configs.last.auth_over_tls = :start_tls
if api_request?
render :json => aacs_json(@account.account_authorization_configs)
else
@account_configs = @account.account_authorization_configs.to_a
@saml_identifiers = Onelogin::Saml::NameIdentifiers::ALL_IDENTIFIERS
@saml_login_attributes = AccountAuthorizationConfig.saml_login_attributes
@saml_authn_contexts = [["No Value", nil]] + Onelogin::Saml::AuthnContexts::ALL_CONTEXTS.sort
end
@saml_identifiers = Onelogin::Saml::NameIdentifiers::ALL_IDENTIFIERS
@saml_login_attributes = AccountAuthorizationConfig.saml_login_attributes
@saml_authn_contexts = [["No Value", nil]] + Onelogin::Saml::AuthnContexts::ALL_CONTEXTS.sort
end
# @API Configure external authentication (SSO)
# @API Create Authorization Config
#
# Set the external account authentication service(s) for the account.
# Add external account authentication service(s) for the account.
# Services may be CAS, SAML, or LDAP.
#
# Each authentication service is specified as a set of parameters as
@ -47,6 +96,9 @@ class AccountAuthorizationConfigsController < ApplicationController
# identifiers; for example: 'Login', 'Username', 'Student ID', etc. The
# default is 'Email'.
#
# You can set the 'position' for any configuration. The config in the 1st position
# is considered the default.
#
# For CAS authentication services, the additional recognized parameters are:
#
# - auth_base
@ -60,6 +112,11 @@ class AccountAuthorizationConfigsController < ApplicationController
#
# For SAML authentication services, the additional recognized parameters are:
#
# - idp_entity_id
#
# The SAML IdP's entity ID - This is used to look up the correct SAML IdP if
# multiple are configured
#
# - log_in_url
#
# The SAML service's SSO target URL
@ -136,7 +193,7 @@ class AccountAuthorizationConfigsController < ApplicationController
#
# Forgot Password URL. Leave blank for default Canvas behavior.
#
# @argument account_authorization_config[n]
# - account_authorization_config[n] (deprecated)
# The nth service specification as described above. For instance, the
# auth_type of the first service is given by the
# account_authorization_config[0][auth_type] parameter. There must be
@ -145,20 +202,68 @@ class AccountAuthorizationConfigsController < ApplicationController
# are ignored; additional non-LDAP services after an initial LDAP service
# are ignored.
#
# Examples:
# @example_request
# # Create LDAP config
# curl 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs' \
# -F 'auth_type=ldap' \
# -F 'auth_host=ldap.mydomain.edu' \
# -F 'auth_filter=(sAMAccountName={{login}})' \
# -F 'auth_username=username' \
# -F 'auth_password=bestpasswordever' \
# -F 'position=1' \
# -H 'Authorization: Bearer <token>'
#
# @example_request
# # Create SAML config
# curl 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs' \
# -F 'auth_type=saml' \
# -F 'idp_entity_id=<idp_entity_id>' \
# -F 'log_in_url=<login_url>' \
# -F 'log_out_url=<logout_url>' \
# -F 'certificate_fingerprint=<fingerprint>' \
# -H 'Authorization: Bearer <token>'
#
# @example_request
# # Create CAS config
# curl 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs' \
# -F 'auth_type=cas' \
# -F 'auth_base=cas.mydomain.edu' \
# -F 'log_in_url=<login_url>' \
# -H 'Authorization: Bearer <token>'
#
# _Deprecated_ Examples:
#
# This endpoint still supports a deprecated version of setting the authorization configs.
# If you send data in this format it is considered a snapshot of how the configs
# should be setup and will clear any configs not sent.
#
# Simple CAS server integration.
#
# account_authorization_config[0][auth_type]=cas&
# account_authorization_config[0][auth_base]=cas.mydomain.edu
#
# Simple SAML server integration.
# Single SAML server integration.
#
# account_authorization_config[0][idp_entity_id]=http://idp.myschool.com/sso/saml2
# account_authorization_config[0][log_in_url]=saml-sso.mydomain.com&
# account_authorization_config[0][log_out_url]=saml-slo.mydomain.com&
# account_authorization_config[0][certificate_fingerprint]=1234567890ABCDEF&
# account_authorization_config[0][identifier_format]=urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
#
# Two SAML server integration with discovery url.
#
# discovery_url=http://www.myschool.com/sso/identity_provider_selection
# account_authorization_config[0][idp_entity_id]=http://idp.myschool.com/sso/saml2&
# account_authorization_config[0][log_in_url]=saml-sso.mydomain.com&
# account_authorization_config[0][log_out_url]=saml-slo.mydomain.com&
# account_authorization_config[0][certificate_fingerprint]=1234567890ABCDEF&
# account_authorization_config[0][identifier_format]=urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress&
# account_authorization_config[1][idp_entity_id]=http://idp.otherschool.com/sso/saml2&
# account_authorization_config[1][log_in_url]=saml-sso.otherdomain.com&
# account_authorization_config[1][log_out_url]=saml-slo.otherdomain.com&
# account_authorization_config[1][certificate_fingerprint]=ABCDEFG12345678789&
# account_authorization_config[1][identifier_format]=urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
#
# Single LDAP server integration.
#
# account_authorization_config[0][auth_type]=ldap&
@ -180,9 +285,108 @@ class AccountAuthorizationConfigsController < ApplicationController
# account_authorization_config[1][auth_username]=username&
# account_authorization_config[1][auth_password]=password
#
# @returns AccountAuthorizationConfig
def create
# Check if this is using the deprecated version of the api
if params[:account_authorization_config] && params[:account_authorization_config].has_key?("0")
if params.has_key?(:auth_type) || (params[:account_authorization_config] && params[:account_authorization_config].has_key?(:auth_type))
# it has deprecated configs, and non-deprecated
render :json => {:message => t('deprecated_fail', "Can't use both deprecated and current version of create at the same time.")}, :status => 400
else
update_all
end
elsif params.has_key?(:auth_type) || (params[:account_authorization_config] && params[:account_authorization_config].has_key?(:auth_type))
aac_data = params.has_key?(:account_authorization_config) ? params[:account_authorization_config] : params
data = filter_data(aac_data)
if @account.account_authorization_config
if @account.account_authorization_config.auth_type != data[:auth_type]
render :json => {:message => t('no_auth_mixing', 'Can not mix authentication types')}, :status => 400
return
elsif @account.account_authorization_config.auth_type == 'cas'
render :json => {:message => t('only_one_cas', "Can not create multiple CAS configurations")}, :status => 400
return
end
end
position = data.delete :position
account_config = @account.account_authorization_configs.create!(data)
if position.present?
account_config.insert_at(position)
account_config.save!
end
render :json => aac_json(account_config)
else
render :json => {:message => t('no_config_sent', "Must specify auth_type")}, :status => 400
end
end
# @API Update Authorization Config
# Update an authorization config using the same options as the create endpoint.
# You can not update an existing configuration to a new authentication type.
#
# @example_request
# # update SAML config
# curl -XPUT 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs/<id>' \
# -F 'idp_entity_id=<new_idp_entity_id>' \
# -F 'log_in_url=<new_url>' \
# -H 'Authorization: Bearer <token>'
#
# @returns AccountAuthorizationConfig
def update
aac_data = params.has_key?(:account_authorization_config) ? params[:account_authorization_config] : params
aac = @account.account_authorization_configs.find params[:id]
data = filter_data(aac_data)
if aac.auth_type != data[:auth_type]
render :json => {:message => t('no_changing_auth_types', 'Can not change type of authorization config, please delete and create new config.')}, :status => 400
return
end
position = data.delete :position
aac.update_attributes(data)
if position.present?
aac.insert_at(position)
aac.save!
end
render :json => aac_json(aac)
end
# @API Get Authorization Config
# Get the specified authorization config
#
# @example_request
# curl 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs/<id>' \
# -H 'Authorization: Bearer <token>'
#
# @returns AccountAuthorizationConfig
#
def show
aac = @account.account_authorization_configs.find params[:id]
render :json => aac_json(aac)
end
# @API Delete Authorization Config
# Delete the config
#
# @example_request
# curl -XDELETE 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs/<id>' \
# -H 'Authorization: Bearer <token>'
def destroy
aac = @account.account_authorization_configs.find params[:id]
aac.destroy
render :json => aac_json(aac)
end
# deprecated version of the AAC API
def update_all
account_configs_to_delete = @account.account_authorization_configs.to_a.dup
account_configs = {}
account_configs = []
(params[:account_authorization_config] || {}).sort {|a,b| a[0] <=> b[0] }.each do |idx, data|
id = data.delete :id
disabled = data.delete :disabled
@ -200,13 +404,77 @@ class AccountAuthorizationConfigsController < ApplicationController
end
if result
account_configs[account_config.id] = account_config
account_configs << account_config
else
return render :json => account_config.errors.to_json
end
end
account_configs_to_delete.map(&:destroy)
render :json => account_configs.to_json
account_configs.each_with_index{|aac, i| aac.insert_at(i+1);aac.save!}
@account.reload
if @account.account_authorization_configs.count > 1 && params[:discovery_url] && params[:discovery_url] != ''
@account.auth_discovery_url = params[:discovery_url]
else
@account.auth_discovery_url = nil
end
@account.save!
render :json => aacs_json(@account.account_authorization_configs)
end
# @API GET discovery url
# Get the discovery url
#
# @example_request
# curl 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs/discovery_url' \
# -H 'Authorization: Bearer <token>'
#
# @returns discovery url
def show_discovery_url
render :json => {:discovery_url => @account.auth_discovery_url}
end
# @API Set discovery url
#
# If you have multiple IdPs configured, you can set a `discovery_url`.
# If that is set, canvas will forward all users to that URL when they need to
# be authenticated. That page will need to then help the user figure out where
# they need to go to log in.
#
# If no discovery url is configured, the 1st auth config will be used to
# attempt to authenticate the user.
#
# @example_request
# curl -XPUT 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs/discovery_url' \
# -F 'discovery_url=<new_url>' \
# -H 'Authorization: Bearer <token>'
#
# @returns discovery url
def update_discovery_url
if params[:discovery_url] && params[:discovery_url] != ''
@account.auth_discovery_url = params[:discovery_url]
else
@account.auth_discovery_url = nil
end
@account.save!
render :json => {:discovery_url => @account.auth_discovery_url}
end
# @API Delete discovery url
# Clear discovery url
#
# @example_request
# curl -XDELETE 'https://<canvas>/api/v1/account/<account_id>/account_authorization_configs/discovery_url' \
# -H 'Authorization: Bearer <token>'
#
def destroy_discovery_url
@account.auth_discovery_url = nil
@account.save!
render :json => {:discovery_url => @account.auth_discovery_url}
end
def test_ldap_connection
@ -312,25 +580,9 @@ class AccountAuthorizationConfigsController < ApplicationController
end
protected
def recognized_params(auth_type)
case auth_type
when 'cas'
[ :auth_type, :auth_base, :log_in_url, :login_handle_name ]
when 'ldap'
[ :auth_type, :auth_host, :auth_port, :auth_over_tls, :auth_base,
:auth_filter, :auth_username, :auth_password, :change_password_url,
:identifier_format, :login_handle_name ]
when 'saml'
[ :auth_type, :log_in_url, :log_out_url, :change_password_url, :requested_authn_context,
:certificate_fingerprint, :identifier_format, :login_handle_name, :login_attribute ]
else
[]
end
end
def filter_data(data)
data ||= {}
data = data.slice(*recognized_params(data[:auth_type]))
data = data.slice(*AccountAuthorizationConfig.recognized_params(data[:auth_type]))
if data[:auth_type] == 'ldap'
data[:auth_over_tls] = 'start_tls' unless data.has_key?(:auth_over_tls)
data[:auth_over_tls] = AccountAuthorizationConfig.auth_over_tls_setting(data[:auth_over_tls])

View File

@ -88,7 +88,25 @@ class PseudonymSessionsController < ApplicationController
initiate_cas_login(cas_client)
elsif @is_saml && !params[:no_auto]
initiate_saml_login(request.host_with_port)
if params[:account_authorization_config_id]
if aac = @domain_root_account.account_authorization_configs.find_by_id(params[:account_authorization_config_id])
initiate_saml_login(request.host_with_port, aac)
else
message = t('errors.login_errors.no_config_for_id', "The Canvas account has no authentication configuration with that id")
if @domain_root_account.auth_discovery_url
redirect_to @domain_root_account.auth_discovery_url + "?message=#{URI.escape message}"
else
flash[:delegated_message] = message
redirect_to login_url(:no_auto=>'true')
end
end
else
if @domain_root_account.auth_discovery_url
redirect_to @domain_root_account.auth_discovery_url
else
initiate_saml_login(request.host_with_port)
end
end
else
flash[:delegated_message] = session.delete :delegated_message if session[:delegated_message]
maybe_render_mobile_login
@ -185,22 +203,26 @@ class PseudonymSessionsController < ApplicationController
if @domain_root_account.saml_authentication? and session[:saml_unique_id]
# logout at the saml identity provider
# once logged out it'll be redirected to here again
aac = @domain_root_account.account_authorization_config
settings = aac.saml_settings(request.host_with_port)
request = Onelogin::Saml::LogOutRequest.new(settings, session)
forward_url = request.generate_request
if aac.debugging? && aac.debug_get(:logged_in_user_id) == @current_user.id
aac.debug_set(:logout_request_id, request.id)
aac.debug_set(:logout_to_idp_url, forward_url)
aac.debug_set(:logout_to_idp_xml, request.request_xml)
aac.debug_set(:debugging, t('debug.logout_redirect', "LogoutRequest sent to IdP"))
if aac = @domain_root_account.account_authorization_configs.find_by_id(session[:saml_aac_id])
settings = aac.saml_settings(request.host_with_port)
request = Onelogin::Saml::LogOutRequest.new(settings, session)
forward_url = request.generate_request
if aac.debugging? && aac.debug_get(:logged_in_user_id) == @current_user.id
aac.debug_set(:logout_request_id, request.id)
aac.debug_set(:logout_to_idp_url, forward_url)
aac.debug_set(:logout_to_idp_xml, request.request_xml)
aac.debug_set(:debugging, t('debug.logout_redirect', "LogoutRequest sent to IdP"))
end
reset_session
session[:delegated_message] = message if message
redirect_to(forward_url)
return
else
reset_session
flash[:message] = t('errors.logout_errors.no_idp_found', "Canvas was unable to log you out at your identity provider")
end
reset_session
session[:delegated_message] = message if message
redirect_to(forward_url)
return
elsif @domain_root_account.cas_authentication? and session[:cas_login]
reset_session
session[:delegated_message] = message if message
@ -238,9 +260,29 @@ class PseudonymSessionsController < ApplicationController
logger.info "SAMLResponse[#{idx+1}/#{chunks.length}] #{chunk}"
end
aac = @domain_root_account.account_authorization_config
response = saml_response(params[:SAMLResponse])
if @domain_root_account.account_authorization_configs.count > 1
aac = @domain_root_account.account_authorization_configs.find_by_idp_entity_id(response.issuer)
if aac.nil?
logger.error "Attempted SAML login for #{response.issuer} on account without that IdP"
@pseudonym_session.destroy rescue true
reset_session
if @domain_root_account.auth_discovery_url
message = t('errors.login_errors.unrecognized_idp', "Canvas did not recognize your identity provider")
redirect_to @domain_root_account.auth_discovery_url + "?message=#{URI.escape message}"
else
flash[:delegated_message] = t 'errors.login_errors.no_idp_set', "The institution you logged in from is not configured on this account."
redirect_to login_url(:no_auto=>'true')
end
return
end
else
aac = @domain_root_account.account_authorization_config
end
settings = aac.saml_settings(request.host_with_port)
response = saml_response(params[:SAMLResponse], settings)
response.process(settings)
unique_id = nil
if aac.login_attribute == 'nameid'
@ -265,7 +307,9 @@ class PseudonymSessionsController < ApplicationController
aac.debug_set(:fingerprint_from_idp, response.fingerprint_from_idp)
aac.debug_set(:login_to_canvas_success, 'false')
end
login_error_message = t 'errors.login_error', "There was a problem logging in at %{institution}", :institution => @domain_root_account.display_name
if response.is_valid?
aac.debug_set(:is_valid_login_response, 'true') if debugging
@ -291,6 +335,7 @@ class PseudonymSessionsController < ApplicationController
session[:name_qualifier] = response.name_qualifier
session[:session_index] = response.session_index
session[:return_to] = params[:RelayState] if params[:RelayState] && params[:RelayState] =~ /\A\/(\z|[^\/])/
session[:saml_aac_id] = aac.id
successful_login(@user, @pseudonym)
else
@ -305,13 +350,13 @@ class PseudonymSessionsController < ApplicationController
message = "Failed to log in correctly at IdP"
logger.warn message
aac.debug_set(:canvas_login_fail_message, message) if debugging
flash[:delegated_message] = t 'errors.login_error', "There was a problem logging in at %{institution}", :institution => @domain_root_account.display_name
flash[:delegated_message] = login_error_message
redirect_to login_url(:no_auto=>'true')
elsif response.no_authn_context?
message = "Attempted SAML login for unsupported authn_context at IdP."
logger.warn message
aac.debug_set(:canvas_login_fail_message, message) if debugging
flash[:delegated_message] = t 'errors.login_error', "There was a problem logging in at %{institution}", :institution => @domain_root_account.display_name
flash[:delegated_message] = login_error_message
redirect_to login_url(:no_auto=>'true')
else
message = "Unexpected SAML status code - status code: #{response.status_code rescue ""} - Status Message: #{response.status_message rescue ""}"
@ -327,37 +372,38 @@ class PseudonymSessionsController < ApplicationController
logger.error "Failed to verify SAML signature."
@pseudonym_session.destroy rescue true
reset_session
flash[:delegated_message] = t 'errors.login_error', "There was a problem logging in at %{institution}", :institution => @domain_root_account.display_name
flash[:delegated_message] = login_error_message
redirect_to login_url(:no_auto=>'true')
end
elsif !params[:SAMLResponse]
logger.error "saml_consume request with no SAMLResponse parameter"
@pseudonym_session.destroy rescue true
reset_session
flash[:delegated_message] = t 'errors.login_error', "There was a problem logging in at %{institution}", :institution => @domain_root_account.display_name
flash[:delegated_message] = login_error_message
redirect_to login_url(:no_auto=>'true')
else
logger.error "Attempted SAML login on non-SAML enabled account."
@pseudonym_session.destroy rescue true
reset_session
flash[:delegated_message] = t 'errors.login_error', "There was a problem logging in at %{institution}", :institution => @domain_root_account.display_name
flash[:delegated_message] = login_error_message
redirect_to login_url(:no_auto=>'true')
end
end
def saml_logout
if @domain_root_account.saml_authentication? && params[:SAMLResponse]
aac = @domain_root_account.account_authorization_config
settings = aac.saml_settings(request.host_with_port)
response = Onelogin::Saml::LogoutResponse.new(params[:SAMLResponse], settings)
response.logger = logger
response = saml_logout_response(params[:SAMLResponse])
if aac = @domain_root_account.account_authorization_configs.find_by_idp_entity_id(response.issuer)
settings = aac.saml_settings(request.host_with_port)
response.process(settings)
if aac.debugging? && aac.debug_get(:logout_request_id) == response.in_response_to
aac.debug_set(:idp_logout_response_encoded, params[:SAMLResponse])
aac.debug_set(:idp_logout_response_xml_encrypted, response.xml)
aac.debug_set(:idp_logout_in_response_to, response.in_response_to)
aac.debug_set(:idp_logout_destination, response.destination)
aac.debug_set(:debugging, t('debug.logout_redirect_from_idp', "Received LogoutResponse from IdP"))
if aac.debugging? && aac.debug_get(:logout_request_id) == response.in_response_to
aac.debug_set(:idp_logout_response_encoded, params[:SAMLResponse])
aac.debug_set(:idp_logout_response_xml_encrypted, response.xml)
aac.debug_set(:idp_logout_in_response_to, response.in_response_to)
aac.debug_set(:idp_logout_destination, response.destination)
aac.debug_set(:debugging, t('debug.logout_redirect_from_idp', "Received LogoutResponse from IdP"))
end
end
end
redirect_to :action => :destroy
@ -369,12 +415,18 @@ class PseudonymSessionsController < ApplicationController
@cas_client = CASClient::Client.new(config)
end
def saml_response(raw_response, settings)
def saml_response(raw_response, settings=nil)
response = Onelogin::Saml::Response.new(raw_response, settings)
response.logger = logger
response
end
def saml_logout_response(raw_response, settings=nil)
response = Onelogin::Saml::LogoutResponse.new(raw_response, settings)
response.logger = logger
response
end
def forbid_on_files_domain
if HostUrl.is_file_host?(request.host_with_port)
reset_session

View File

@ -58,7 +58,7 @@ class Account < ActiveRecord::Base
has_many :active_folders, :class_name => 'Folder', :as => :context, :conditions => ['folders.workflow_state != ?', 'deleted'], :order => 'folders.name'
has_many :active_folders_with_sub_folders, :class_name => 'Folder', :as => :context, :include => [:active_sub_folders], :conditions => ['folders.workflow_state != ?', 'deleted'], :order => 'folders.name'
has_many :active_folders_detailed, :class_name => 'Folder', :as => :context, :include => [:active_sub_folders, :active_file_attachments], :conditions => ['folders.workflow_state != ?', 'deleted'], :order => 'folders.name'
has_many :account_authorization_configs, :order => 'id'
has_many :account_authorization_configs, :order => "position"
has_many :account_reports
has_many :grading_standards, :as => :context
has_many :assessment_questions, :through => :assessment_question_banks
@ -672,6 +672,18 @@ class Account < ActiveRecord::Base
def saml_authentication?
!!(self.account_authorization_config && self.account_authorization_config.saml_authentication?)
end
def multi_auth?
self.account_authorization_configs.count > 1
end
def auth_discovery_url=(url)
self.settings[:auth_discovery_url] = url
end
def auth_discovery_url
self.settings[:auth_discovery_url]
end
# When a user is invited to a course, do we let them see a preview of the
# course even without registering? This is part of the free-for-teacher

View File

@ -20,13 +20,14 @@ require 'onelogin/saml'
class AccountAuthorizationConfig < ActiveRecord::Base
belongs_to :account
acts_as_list :scope => :account
attr_accessible :account, :auth_port, :auth_host, :auth_base, :auth_username,
:auth_password, :auth_password_salt, :auth_type, :auth_over_tls,
:log_in_url, :log_out_url, :identifier_format,
:certificate_fingerprint, :entity_id, :change_password_url,
:login_handle_name, :ldap_filter, :auth_filter, :requested_authn_context,
:login_attribute
:login_attribute, :idp_entity_id
before_validation :set_saml_defaults, :if => Proc.new { |aac| aac.saml_authentication? }
validates_presence_of :account_id
@ -36,6 +37,23 @@ class AccountAuthorizationConfig < ActiveRecord::Base
# if the config changes, clear out last_timeout_failure so another attempt can be made immediately
before_save :clear_last_timeout_failure
def self.recognized_params(auth_type)
case auth_type
when 'cas'
[ :auth_type, :auth_base, :log_in_url, :login_handle_name ]
when 'ldap'
[ :auth_type, :auth_host, :auth_port, :auth_over_tls, :auth_base,
:auth_filter, :auth_username, :auth_password, :change_password_url,
:identifier_format, :login_handle_name, :position ]
when 'saml'
[ :auth_type, :log_in_url, :log_out_url, :change_password_url, :requested_authn_context,
:certificate_fingerprint, :identifier_format, :login_handle_name,
:login_attribute, :idp_entity_id, :position]
else
[]
end
end
def self.auth_over_tls_setting(value)
case value
when nil, '', false, 'false', 'f', 0, '0'

View File

@ -1,4 +1,5 @@
<% @account_config = @account_configs.first %>
<% @account_config ||= @account.account_authorization_configs.new %>
<% form_id = @account_config.cas_authentication? ? 'auth_form' : 'cas_form' %>
<% active = @account_config.cas_authentication? ? 'class="active"' : '' %>
<div id="cas_div" <%= raw active %>>
@ -27,7 +28,7 @@
<td style="vertical-align: top; width: 200px;"><%= f.blabel :login_handle_name, :en => "Login Label" %></td>
<td style="vertical-align: top;" class="nobr">
<%= f.text_field :login_handle_name, :class => "auth_form", :style => "width: 300px;", :placeholder => AccountAuthorizationConfig.default_delegated_login_handle_name %>
<span class="auth_info auth_login_handle_name"><%= @account_configs[0].login_handle_name || AccountAuthorizationConfig.default_login_handle_name %></span>
<span class="auth_info auth_login_handle_name"><%= @account_config.login_handle_name || AccountAuthorizationConfig.default_login_handle_name %></span>
<span class="auth_form" style="font-size: smaller;">
<br><%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %>
</span>

View File

@ -1,23 +1,31 @@
<% @account_config = @account_configs.first %>
<%
@ldap_configs = @account_configs.clone
while @ldap_configs.length < 2
@ldap_configs << @account.account_authorization_configs.new
@ldap_configs.last.auth_over_tls = :start_tls
end
@account_config = @ldap_configs.first
%>
<% form_id = @account_config.ldap_authentication? ? 'auth_form' : 'ldap_form' %>
<% active = @account_config.ldap_authentication? ? 'class="active"' : '' %>
<div id="ldap_div" <%= raw active %>>
<% form_tag(context_url(@account, :context_update_all_authorization_configs_url), :method => :put, :id => form_id, :class=>"auth_type ldap_form" ) do %>
<table class="formtable" style="margin-left: 20px;">
<%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @account_configs[0] } %>
<%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @ldap_configs[0] } %>
<tr>
<th><%= before_label(t(:auth_type_label, "Type")) %></th>
<th><%= t :setting_type_ldap, 'LDAP' %></th>
</tr>
<% fields_for @account_configs[0], :index => 0 do |f| %>
<% fields_for @ldap_configs[0], :index => 0 do |f| %>
<%= f.hidden_field :disabled, :value => '0' %>
<%= render :partial => 'ldap_settings_fields', :locals => { :f => f, :account_config => @account_configs[0] } %>
<%= render :partial => 'ldap_settings_fields', :locals => { :f => f, :account_config => @ldap_configs[0] } %>
<tr>
<td style="vertical-align: top; width: 200px;"><%= f.blabel :login_handle_name, :en => "Login Label" %></td>
<td style="vertical-align: top;" class="nobr">
<%= f.text_field :login_handle_name, :class => "auth_form", :style => "width: 300px;", :placeholder => AccountAuthorizationConfig.default_login_handle_name %>
<span class="auth_info auth_login_handle_name"><%= @account_configs[0].login_handle_name || AccountAuthorizationConfig.default_login_handle_name %></span>
<span class="auth_info auth_login_handle_name"><%= @ldap_configs[0].login_handle_name || AccountAuthorizationConfig.default_login_handle_name %></span>
<span class="auth_form" style="font-size: smaller;">
<br><%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %>
</span>
@ -28,7 +36,7 @@
<td style="vertical-align: top;" class="nobr">
<%= f.text_field :change_password_url, :class => "auth_form", :style => "width: 300px;" %>
<div style="font-size: 0.8em;"><span class="auth_form"><%= t(:change_password_url_help, "Leave blank for default Canvas behavior") %></span></div>
<span class="auth_info auth_forgot_password_url"><%= @account_configs[0].change_password_url || t(:change_password_url_not_specified, "None specified") %></span>
<span class="auth_info auth_forgot_password_url"><%= @ldap_configs[0].change_password_url || t(:change_password_url_not_specified, "None specified") %></span>
</td>
</tr>
<% end %>
@ -36,24 +44,29 @@
<div>
<div class="auth_form" style="margin: 15px;">
<a href="#" class="add_secondary_ldap_link" style="<%= "display: none;" if @account_configs[1].ldap_authentication? %>"><%= t(:add_secondary_ldap_server_link, "Add Secondary LDAP Server") %></a>
<a href="#" class="add_secondary_ldap_link" style="<%= "display: none;" if @ldap_configs[1].ldap_authentication? %>"><%= t(:add_secondary_ldap_server_link, "Add Secondary LDAP Server") %></a>
</div>
</div>
<table class="formtable ldap_secondary" style="margin-left: 20px; margin-top: 20px; <%= "display: none;" unless @account_configs[1].ldap_authentication? %>">
<%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @account_configs[1] } %>
<table class="formtable ldap_secondary" style="margin-left: 20px; margin-top: 20px; <%= "display: none;" unless @ldap_configs[1].ldap_authentication? %>">
<%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @ldap_configs[1] } %>
<tr>
<th><%= before_label(t(:auth_type_label, "Type")) %></th>
<th><%= t(:secondary_ldap_label, "Secondary LDAP") %><% unless @account_configs.length > 2 %> <a href="#" class="auth_form remove_secondary_ldap_link"><%= t(:remove_secondary_ldap_link, "(Remove)") %></a><% end %></th>
<th><%= t(:secondary_ldap_label, "Secondary LDAP") %>
<% unless @ldap_configs.length > 2 %>
<a href="#" class="auth_form remove_secondary_ldap_link">
<%= t(:remove_secondary_ldap_link, "(Remove)") %></a>
<% end %>
</th>
</tr>
<% fields_for @account_configs[1], :index => 1 do |f| %>
<%= f.hidden_field :disabled, :value => @account_configs[1].ldap_authentication? ? '0' : '1', :id => 'secondary_ldap_config_disabled' %>
<%= render :partial => 'ldap_settings_fields', :locals => { :f => f, :account_config => @account_configs[1] } %>
<% fields_for @ldap_configs[1], :index => 1 do |f| %>
<%= f.hidden_field :disabled, :value => @ldap_configs[1].ldap_authentication? ? '0' : '1', :id => 'secondary_ldap_config_disabled' %>
<%= render :partial => 'ldap_settings_fields', :locals => { :f => f, :account_config => @ldap_configs[1] } %>
<% end %>
</table>
<% if @account_configs.length > 2 %>
<% @account_configs[2..-1].each_with_index do |aac, i| %>
<% if @ldap_configs.length > 2 %>
<% @ldap_configs[2..-1].each_with_index do |aac, i| %>
<% next unless aac.ldap_authentication? %>
<table class="formtable ldap_secondary" style="margin-left: 20px; margin-top: 20px;">
<%= render :partial => 'ldap_timeout_error', :locals => { :account_config => aac } %>

View File

@ -1,101 +1,196 @@
<% @account_config = @account_configs.first %>
<% form_id = @account_config.saml_authentication? ? 'auth_form' : 'saml_form' %>
<% active = @account_config.saml_authentication? ? 'class="active"' : '' %>
<% debugging = @account_config && @account_config.debugging? %>
<%
@saml_configs = @account_configs.clone
@saml_configs << @account.account_authorization_configs.new
@account_config = @saml_configs.first
active = @account_config.saml_authentication? ? 'class="active"' : ''
position_options = []
(@saml_configs.length - 1).times {|i|position_options << [i + 1,i + 1]}
debugging = @account_config && @account_config.debugging?
%>
<div id="saml_div" <%= raw active %>>
<% form_tag(context_url(@account, :context_update_all_authorization_configs_url), :method => :put, :id => form_id, :class => "auth_type saml_form") do %>
<% fields_for @account_config, :index => 0 do |f| %>
<%= f.hidden_field :auth_type, :value => 'saml' %>
<%= f.hidden_field :id %>
<table class="formtable" style="margin-left: 20px;">
<tr>
<td><%= f.blabel :auth_type, :en => "Type" %></td>
<td>
<span class="auth_form">
<%= @account_config.auth_type || 'SAML' %>
</span>
<span class="auth_info"><%= @account_config.auth_type || 'SAML' %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :log_in_url, :en => "Log On URL" %></td>
<td class="nobr">
<%= f.text_field :log_in_url, :class => "auth_form", :style => "width: 450px;" %>
<span class="auth_info log_in_url"><%= @account_config.log_in_url %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :log_out_url, :en => "Log Out URL" %></td>
<td class="nobr">
<%= f.text_field :log_out_url, :class => "auth_form", :style => "width: 450px;" %>
<span class="auth_info log_out_url"><%= @account_config.log_out_url %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :change_password_url, :en => "Change Password Link" %></td>
<td class="nobr">
<%= f.text_field :change_password_url, :class => "auth_form", :style => "width: 450px;" %>
<span class="auth_info change_password_url"><%= @account_config.change_password_url %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :certificate_fingerprint, :en => "Certificate Fingerprint" %></td>
<td class="nobr">
<%= f.text_field :certificate_fingerprint, :class => "auth_form", :style => "width: 450px;" %>
<span class="auth_info certificate_fingerprint"><%= @account_config.certificate_fingerprint %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :login_attribute, :en => "Login Attribute" %></td>
<td class="nobr">
<%= f.select :login_attribute, @saml_login_attributes, {}, {:class => "auth_form"} %>
<span class="auth_info login_attribute"><%= @saml_login_attributes.invert[@account_config.login_attribute] %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :identifier_format, :en => "Identifier Format" %></td>
<td class="nobr">
<%= f.select :identifier_format, @saml_identifiers, {}, {:class => "auth_form"} %>
<span class="auth_info identifier_format"><%= @account_config.identifier_format %></span>
</td>
</tr>
<tr>
<td><%= f.blabel :requested_authn_context, :en => "Authentication Context" %></td>
<td class="nobr">
<%= f.select :requested_authn_context, @saml_authn_contexts, {}, {:class => "auth_form"} %>
<span class="auth_info requested_authn_context"><%= @account_config.requested_authn_context %></span>
</td>
</tr>
<tr>
<td style="vertical-align: top; width: 200px;"><%= f.blabel :login_handle_name, :en => "Login Label" %></td>
<td style="vertical-align: top;" class="nobr">
<%= f.text_field :login_handle_name, :class => "auth_form", :style => "width: 300px;", :placeholder => AccountAuthorizationConfig.default_delegated_login_handle_name %>
<span class="auth_info auth_login_handle_name"><%= @account_configs[0].login_handle_name || AccountAuthorizationConfig.default_login_handle_name %></span>
<span class="auth_form" style="font-size: smaller;">
<br><%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %>
</span>
</td>
</tr>
<tr>
<td colspan="4">
<span class="auth_form">
<button type="submit" class="button"><%= t(:save_button, "Save Authentication Settings") %></button>
<button type="button" class="cancel_button button-secondary"><%= t("#buttons.cancel", "Cancel") %></button>
</span>
</td>
</tr>
<tr>
<td colspan="2">
<span class="auth_info">
<%= link_to(t(:saml_meta_data_link, "Click here to see the service provider identity XML for this account."), :saml_meta_data) %>
</span>
</td>
</tr>
</table>
<div class="debugging">
<% @saml_configs.each_with_index do |config, i|
form_id = "saml_config_#{config.id}_form"
delete_url = config.new_record? ? '' : api_v1_account_delete_aac_path(@account, config)
%>
<div>
<div class="admin-link-hover-area well auth_config"
aria-controls="<%= form_id %>"
data-hide-while-target-shown=true
<%= hidden(true) if config.new_record? %>>
<div class="admin-links">
<button class="al-trigger ui-button">
<span class="al-trigger-inner">Manage</span>
</button>
<ul class="al-options">
<li><a href="#" class="element_toggler" aria-controls="<%= form_id %>"><span class="ui-icon ui-icon-pencil"></span>Edit</a>
</li>
<li>
<a href="#" data-url="<%= delete_url %>"
data-remove=".auth_config"
rel="nofollow"
data-confirm="Are you sure?"><span class="ui-icon ui-icon-trash"></span>Delete</a></li>
</ul>
</div>
<table class="formtable" style="margin-left: 20px;">
<tr>
<td><%= t :auth_type, "Type" %></td>
<td><%= config.auth_type || 'SAML' %></td>
</tr>
<% if @account.multi_auth? %>
<tr>
<td><%= t :auth_url, "Login url for this config" %></td>
<td><%= aac_login_url(config) %></td>
</tr>
<% end %>
<tr>
<td><%= t :idp_entity_id, "IdP Entity ID" %></td>
<td class="nobr"><%= config.idp_entity_id %></td>
</tr>
<tr>
<td><%= t :log_in_url, "Log On URL" %></td>
<td class="nobr"><%= config.log_in_url %></td>
</tr>
<tr>
<td><%= t :log_out_url, "Log Out URL" %></td>
<td class="nobr"><%= config.log_out_url %></td>
</tr>
<tr>
<td><%= t :change_password_url, "Change Password Link" %></td>
<td class="nobr"><%= config.change_password_url %></td>
</tr>
<tr>
<td><%= t :certificate_fingerprint, "Certificate Fingerprint" %></td>
<td class="nobr"><%= config.certificate_fingerprint %></td>
</tr>
<tr>
<td><%= t :login_attribute, "Login Attribute" %></td>
<td class="nobr"><%= @saml_login_attributes.invert[config.login_attribute] %></td>
</tr>
<tr>
<td><%= t :identifier_format, "Identifier Format" %></td>
<td class="nobr"><%= config.identifier_format %></td>
</tr>
<tr>
<td><%= t :requested_authn_context, "Authentication Context" %></td>
<td class="nobr"><%= config.requested_authn_context %></td>
</tr>
<tr>
<td style="vertical-align: top; width: 200px;"><%= t :login_handle_name, "Login Label" %></td>
<td style="vertical-align: top;" class="nobr"><%= config.login_handle_name || AccountAuthorizationConfig.default_login_handle_name %></td>
</tr>
<% if @account.multi_auth? %>
<tr>
<td><%= t :position, "Position" %></td>
<td><%= config.position %></td>
</tr>
<% end %>
</table>
</div>
<%
url = config.new_record? ? api_v1_account_create_aac_path(@account) : api_v1_account_update_aac_path(@account, config)
method = config.new_record? ? :post : :put
form_tag(url, :method => method, :id => form_id, :class => "form-horizontal bootstrap-form auth_type saml_form well", :style => hidden) do
%>
<% fields_for config do |f| %>
<%= f.hidden_field :auth_type, :value => 'saml' %>
<%= f.hidden_field :id %>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :idp_entity_id, :en => "IdP Entity ID" %></label>
<div class="controls">
<%= f.text_field :idp_entity_id, :class => "input-xlarge" %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :log_in_url, :en => "Log On URL" %></label>
<div class="controls">
<%= f.text_field :log_in_url, :class => "input-xlarge" %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :log_out_url, :en => "Log Out URL" %></label>
<div class="controls">
<%= f.text_field :log_out_url, :class => "input-xlarge" %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :change_password_url, :en => "Change Password Link" %></label>
<div class="controls">
<%= f.text_field :change_password_url, :class => "input-xlarge" %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :certificate_fingerprint, :en => "Certificate Fingerprint" %></label>
<div class="controls">
<%= f.text_field :certificate_fingerprint, :class => "input-xlarge" %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :login_attribute, :en => "Login Attribute" %></label>
<div class="controls">
<%= f.select :login_attribute, @saml_login_attributes, {}, {:class => "input-xlarge"} %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :identifier_format, :en => "Identifier Format" %></label>
<div class="controls">
<%= f.select :identifier_format, @saml_identifiers, {}, {:class => "input-xlarge"} %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :requested_authn_context, :en => "Authentication Context" %></label>
<div class="controls">
<%= f.select :requested_authn_context, @saml_authn_contexts, {}, {:class => "input-xlarge"} %>
</div>
</div>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :login_handle_name, :en => "Login Label" %></label>
<div class="controls">
<%= f.text_field :login_handle_name, :class => "input-xlarge" %>
<p class="help-block"><%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %></p>
</div>
</div>
<% if @saml_configs.length > 1 %>
<div class="control-group">
<label class="control-label" for="input01"><%= f.blabel :position, :en => "Position" %></label>
<div class="controls">
<% options = config.new_record? ? [["Last", nil]] + position_options : position_options %>
<%= f.select :position, options, {}, {:class => "input-xlarge"} %>
</div>
</div>
<% end %>
<button type="submit" class="button btn-primary">Submit</button>
<% unless config.new_record? %>
<button class="element_toggler button" aria-controls="<%= form_id %>">Cancel</button>
<% end %>
<% end %>
<% end %>
</div>
<% end %>
<button class="element_toggler button"
aria-controls="saml_config__form"
data-hide-while-target-shown=true>Add New SAML Config</button>
<div class="debugging">
<h3 style="margin-top: 10px"><%= t(:saml_debugging, "Debugging") %></h3>
<div id="saml_debug_console">
<p>
<%= t 'saml_debug_instructions', <<-TEXT
@ -110,7 +205,7 @@
<a href="<%= account_saml_testing_url(@account) %>" id="refresh_saml_debugging" class="button" style="<%= hidden(debugging) %>"><%= t('refresh_debugging', 'Refresh') %></a>
<a href="<%= account_saml_testing_stop_url(@account) %>" id="stop_saml_debugging" class="button" style="<%= hidden(debugging) %>"><%= t('stop_debugging', 'Stop Debugging') %></a>
</p>
<div id="saml_debug_info" style="<%= hidden(debugging) %>">
<% if @account_config && @account_config.debugging? %>
<%= render :partial => 'saml_testing.html' %>
@ -118,7 +213,5 @@
</div>
</div>
</div>
<% end %>
<% end %>
</div>

View File

@ -12,8 +12,8 @@
<% content_for :right_side do %>
<div class="rs-margin-lr rs-margin-top">
<% has_auth = !@account_configs.first.auth_type.nil? %>
<a href="#" class="edit_auth_link button button-sidebar-wide" style="<%= hidden unless has_auth %>"><%= image_tag "edit.png" %><%= t(:edit_auth_link, "Edit Details")%></a>
<% has_auth = @account_configs.any? %>
<a href="#" class="edit_auth_link button button-sidebar-wide" style="<%= hidden if !has_auth || @account.saml_authentication? %>"><%= image_tag "edit.png" %><%= t(:edit_auth_link, "Edit Details")%></a>
<a href="#" class="test_ldap_link button button-sidebar-wide" style="<%= hidden unless @account_configs.map {|c| c.auth_type}.include?("ldap") %>"><%= image_tag "pending_review.png" %><%= t(:test_ldap_link, "Test Authentication")%></a>
<%= link_to image_tag("delete.png") + t(:delete_auth_link, "Remove Authentication"), context_url(@account, :context_remove_all_authorization_configs_url), :confirm => t(:delete_auth_confirmation, "Are you sure? Users may not be able to log in if this is removed."), :method => :delete, :class=>"delete_auth_link button button-sidebar-wide", :style => "#{ hidden unless has_auth}" %>
<div class="add_auth_div" style="<%= hidden if has_auth %>">
@ -59,7 +59,7 @@ using the normal Canvas login procedure. For this account the url would be %{url
<%= render :partial => "ldap_settings" %>
<%= render :partial => "ldap_settings_test" %>
<%= render :partial => "saml_settings" %>
<% unless @account_configs.first.auth_type %>
<% unless @account_configs.any? %>
<div id="no_auth"><%= t(:no_auth_type_description, "This account does not currently integrate with an identity provider.") %></div>
<% end %>

View File

@ -491,6 +491,7 @@ ActionController::Routing::Routes.draw do |map|
map.grades "grades", :controller => "users", :action => "grades"
map.login "login", :controller => "pseudonym_sessions", :action => "new", :conditions => {:method => :get}
map.aac_login "login/:account_authorization_config_id", :controller => "pseudonym_sessions", :action => "new", :conditions => {:method => :get}
map.connect "login", :controller => "pseudonym_sessions", :action=> "create", :conditions => {:method => :post}
map.logout "logout", :controller => "pseudonym_sessions", :action => "destroy"
map.cas_login "login/cas", :controller => "pseudonym_sessions", :action => "new", :conditions => {:method => :get}
@ -819,7 +820,15 @@ ActionController::Routing::Routes.draw do |map|
end
api.with_options(:controller => :account_authorization_configs) do |authorization_configs|
authorization_configs.post 'accounts/:account_id/account_authorization_configs', :action => 'update_all'
authorization_configs.get 'accounts/:account_id/account_authorization_configs/discovery_url', :action => :show_discovery_url
authorization_configs.put 'accounts/:account_id/account_authorization_configs/discovery_url', :action => :update_discovery_url
authorization_configs.delete 'accounts/:account_id/account_authorization_configs/discovery_url', :action => :destroy_discovery_url
authorization_configs.get 'accounts/:account_id/account_authorization_configs', :action => :index
authorization_configs.get 'accounts/:account_id/account_authorization_configs/:id', :action => :show
authorization_configs.post 'accounts/:account_id/account_authorization_configs', :action => :create, :path_name => 'account_create_aac'
authorization_configs.put 'accounts/:account_id/account_authorization_configs/:id', :action => :update, :path_name => 'account_update_aac'
authorization_configs.delete 'accounts/:account_id/account_authorization_configs/:id', :action => :destroy, :path_name => 'account_delete_aac'
end
api.get 'users/:user_id/page_views', :controller => :page_views, :action => :index, :path_name => 'user_page_views'

View File

@ -0,0 +1,28 @@
class AddSamlProperties < ActiveRecord::Migration
tag :predeploy
def self.up
add_column :account_authorization_configs, :idp_entity_id, :string
add_column :account_authorization_configs, :position, :integer
if connection.adapter_name =~ /postgres/i
execute <<-SQL
UPDATE account_authorization_configs aac
SET position =
CASE WHEN (SELECT count(*) FROM account_authorization_configs WHERE account_id = aac.account_id) > 1
THEN aac.id
ELSE 1
END;
SQL
else
execute <<-SQL
UPDATE account_authorization_configs
SET position = account_authorization_configs.id;
SQL
end
end
def self.down
remove_column :account_authorization_configs, :idp_entity_id
remove_column :account_authorization_configs, :position
end
end

View File

@ -0,0 +1,34 @@
#
# Copyright (C) 2012 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 Api::V1::AccountAuthorizationConfig
include Api::V1::Json
def aacs_json(aacs)
aacs.map do |aac|
aac_json(aac)
end
end
def aac_json(aac)
AccountAuthorizationConfig.recognized_params(aac.auth_type).inject(api_json(aac, nil, nil, :only => [:id, :position])) do |h, key|
h[key] = aac.send(key) unless key == :auth_password
h
end
end
end

View File

@ -247,9 +247,9 @@ module AuthenticationMethods
delegated_auth_redirect(cas_client.add_service_to_login_url(cas_login_url))
end
def initiate_saml_login(current_host=nil)
def initiate_saml_login(current_host=nil, aac=nil)
reset_session_for_login
aac = @domain_root_account.account_authorization_config
aac ||= @domain_root_account.account_authorization_config
settings = aac.saml_settings(current_host)
request = Onelogin::Saml::AuthRequest.new(settings)
forward_url = request.generate_request

View File

@ -16,6 +16,7 @@ define([
event.preventDefault();
$("#auth_form").find(".cancel_button:first").click();
new_type = $(this).find(":selected").val();
$(".active").each(function(i){$(this).removeClass('active');})
if(new_type == "" || new_type == null){
new_type = null;
}

View File

@ -23,73 +23,392 @@ describe "AccountAuthorizationConfigs API", :type => :integration do
@account = account_model(:name => 'root')
user_with_pseudonym(:active_all => true, :account => @account)
@account.add_user(@user)
@cas_hash = {"auth_type" => "cas", "auth_base" => "127.0.0.1"}
@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'}
@ldap_hash = {'auth_type' => 'ldap', 'auth_host' => '127.0.0.1', 'auth_filter' => 'filter1', 'auth_username' => 'username1', 'auth_password' => 'password1'}
end
it "should set the authorization config" do
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'update_all', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"auth_type" => "cas", "auth_base" => "127.0.0.1"}}})
@account.reload
@account.account_authorization_configs.size.should == 1
config = @account.account_authorization_configs.first
config.auth_type.should == 'cas'
config.auth_base.should == '127.0.0.1'
context "/index" do
def call_index(status=200)
api_call(:get, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'index', :account_id => @account.id.to_s, :format => 'json' },
{}, {}, :expected_status => status)
end
it "should return all aacs in position order" do
config1 = @account.account_authorization_configs.create!(@saml_hash.merge(:idp_entity_id => "a"))
config2 = @account.account_authorization_configs.create!(@saml_hash.merge(:idp_entity_id => "d"))
config3 = @account.account_authorization_configs.create!(@saml_hash.merge(:idp_entity_id => "r"))
config3.move_to_top
config3.save!
res = call_index
res.map{|c|c['idp_entity_id']}.join.should == 'rad'
end
it "should return unauthorized error" do
course_with_student(:course => @course)
call_index(401)
end
end
it "should set multiple configs" do
ldap1 = {'auth_type' => 'ldap', 'auth_host' => '127.0.0.1', 'auth_filter' => 'filter1', 'auth_username' => 'username1', 'auth_password' => 'password1'}
ldap2 = {'auth_type' => 'ldap', 'auth_host' => '127.0.0.2', 'auth_filter' => 'filter2', 'auth_username' => 'username2', 'auth_password' => 'password2'}
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'update_all', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => ldap1, "1" => ldap2}})
context "/create" do
# the deprecated mass-update/create is tested in account_authorization_configs_deprecated_api_spec.rb
@account.reload
@account.account_authorization_configs.size.should == 2
config1 = @account.account_authorization_configs.first
config2 = @account.account_authorization_configs.second
def call_create(params, status = 200)
json = api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
params, {}, :expected_status => status)
@account.reload
json
end
config1.auth_type.should == 'ldap'
config1.auth_host.should == '127.0.0.1'
config1.auth_filter.should == 'filter1'
config1.auth_username.should == 'username1'
config1.auth_decrypted_password.should == 'password1'
it "should create a saml aac" do
call_create(@saml_hash)
aac = @account.account_authorization_config
aac.auth_type.should == 'saml'
aac.idp_entity_id.should == 'http://example.com/saml1'
aac.log_in_url.should == 'http://example.com/saml1/sli'
aac.log_out_url.should == 'http://example.com/saml1/slo'
aac.certificate_fingerprint.should == '111222'
aac.identifier_format.should == 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
aac.position.should == 1
end
config2.auth_type.should == 'ldap'
config2.auth_host.should == '127.0.0.2'
config2.auth_filter.should == 'filter2'
config2.auth_username.should == 'username2'
config2.auth_decrypted_password.should == 'password2'
it "should work with rails form style params" do
call_create({:account_authorization_config => @saml_hash})
aac = @account.account_authorization_config
aac.auth_type.should == 'saml'
aac.idp_entity_id.should == 'http://example.com/saml1'
end
it "should create multiple saml aacs" do
call_create(@saml_hash)
call_create(@saml_hash.merge('idp_entity_id' => "secondeh"))
aac1 = @account.account_authorization_configs.first
aac1.idp_entity_id.should == 'http://example.com/saml1'
aac1.position.should == 1
aac2 = @account.account_authorization_configs.last
aac2.idp_entity_id.should == 'secondeh'
aac2.position.should == 2
end
it "should create an ldap aac" do
call_create(@ldap_hash)
aac = @account.account_authorization_config
aac.auth_type.should == 'ldap'
aac.auth_host.should == '127.0.0.1'
aac.auth_filter.should == 'filter1'
aac.auth_username.should == 'username1'
aac.auth_decrypted_password.should == 'password1'
aac.position.should == 1
end
it "should create multiple ldap aacs" do
call_create(@ldap_hash)
call_create(@ldap_hash.merge('auth_host' => '127.0.0.2'))
aac = @account.account_authorization_configs.first
aac.auth_host.should == '127.0.0.1'
aac.position.should == 1
aac2 = @account.account_authorization_configs.last
aac2.auth_host.should == '127.0.0.2'
aac2.position.should == 2
end
it "should default ldap auth_over_tls to 'start_tls'" do
call_create(@ldap_hash)
@account.account_authorization_config.auth_over_tls.should == 'start_tls'
end
it "should create a cas aac" do
call_create(@cas_hash)
aac = @account.account_authorization_config
aac.auth_type.should == 'cas'
aac.auth_base.should == '127.0.0.1'
aac.position.should == 1
end
it "should not allow multiple cas aacs (for now)" do
call_create(@cas_hash)
json = call_create(@cas_hash, 400)
json['message'].should == "Can not create multiple CAS configurations"
end
it "should error when mixing auth_types (for now)" do
call_create(@ldap_hash)
json = call_create(@saml_hash, 400)
json['message'].should == 'Can not mix authentication types'
end
it "should update positions" do
call_create(@ldap_hash)
call_create(@ldap_hash.merge('auth_host' => '127.0.0.2', 'position' => 1))
@account.account_authorization_config.auth_host.should == '127.0.0.2'
call_create(@ldap_hash.merge('auth_host' => '127.0.0.3', 'position' => 2))
@account.account_authorization_configs[0].auth_host.should == '127.0.0.2'
@account.account_authorization_configs[1].auth_host.should == '127.0.0.3'
@account.account_authorization_configs[2].auth_host.should == '127.0.0.1'
end
it "should error if deprecated and new style are used" do
json = call_create({:account_authorization_config => {"0" => @ldap_hash}}.merge(@ldap_hash), 400)
json['message'].should == "Can't use both deprecated and current version of create at the same time."
end
it "should error if empty post params sent" do
json = call_create({}, 400)
json['message'].should == "Must specify auth_type"
end
it "should return unauthorized error" do
course_with_student(:course => @course)
call_create({}, 401)
end
it "should disable open registration when setting delegated auth" do
@account.settings = { :open_registration => true }
@account.save!
call_create(@cas_hash)
@account.open_registration?.should be_false
end
end
it "should update existing configs" do
config = @account.account_authorization_configs.create!("auth_type" => "cas", "auth_base" => "127.0.0.1")
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'update_all', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"id" => config.id.to_s, "auth_type" => "cas", "auth_base" => "127.0.0.2"}}})
@account.reload
config.reload
context "/show" do
def call_show(id, status = 200)
api_call(:get, "/api/v1/accounts/#{@account.id}/account_authorization_configs/#{id}",
{ :controller => 'account_authorization_configs', :action => 'show', :account_id => @account.id.to_s, :id => id.to_param, :format => 'json' },
{}, {}, :expected_status => status)
end
@account.account_authorization_configs.size.should == 1
@account.account_authorization_configs.first.should == config
config.auth_base.should == '127.0.0.2'
it "should return saml aac" do
aac = @account.account_authorization_configs.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'
json.should == @saml_hash
end
it "should return ldap aac" do
aac = @account.account_authorization_configs.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'] = nil
@ldap_hash['login_handle_name'] = nil
@ldap_hash['identifier_format'] = nil
@ldap_hash['change_password_url'] = nil
@ldap_hash['position'] = 1
json.should == @ldap_hash
end
it "should return cas aac" do
aac = @account.account_authorization_configs.create!(@cas_hash)
json = call_show(aac.id)
@cas_hash['login_handle_name'] = nil
@cas_hash['log_in_url'] = nil
@cas_hash['id'] = aac.id
@cas_hash['position'] = 1
json.should == @cas_hash
end
it "should 404" do
call_show(0, 404)
end
it "should return unauthorized error" do
course_with_student(:course => @course)
call_show(0, 401)
end
end
it "should delete configs not referenced" do
config = @account.account_authorization_configs.create!("auth_type" => "cas", "auth_base" => "127.0.0.1")
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'update_all', :account_id => @account.id.to_s, :format => 'json' })
@account.reload
@account.account_authorization_configs.should be_empty
context "/update" do
def call_update(id, params, status = 200)
json = api_call(:put, "/api/v1/accounts/#{@account.id}/account_authorization_configs/#{id}",
{ :controller => 'account_authorization_configs', :action => 'update', :account_id => @account.id.to_s, :id => id.to_param, :format => 'json' },
params, {}, :expected_status => status)
@account.reload
json
end
it "should update a saml aac" do
aac = @account.account_authorization_configs.create!(@saml_hash)
@saml_hash['idp_entity_id'] = 'hahahaha'
call_update(aac.id, @saml_hash)
aac.reload
aac.idp_entity_id.should == 'hahahaha'
end
it "should work with rails form style params" do
aac = @account.account_authorization_configs.create!(@saml_hash)
@saml_hash['idp_entity_id'] = 'hahahaha'
call_update(aac.id, {:account_authorization_config => @saml_hash})
aac.reload
aac.idp_entity_id.should == 'hahahaha'
end
it "should update an ldap aac" do
aac = @account.account_authorization_configs.create!(@ldap_hash)
@ldap_hash['auth_host'] = '192.168.0.1'
call_update(aac.id, @ldap_hash)
aac.reload
aac.auth_host.should == '192.168.0.1'
end
it "should update a cas aac" do
aac = @account.account_authorization_configs.create!(@cas_hash)
@cas_hash['auth_base'] = '192.168.0.1'
call_update(aac.id, @cas_hash)
aac.reload
aac.auth_base.should == '192.168.0.1'
end
it "should error when mixing auth_types" do
aac = @account.account_authorization_configs.create!(@saml_hash)
json = call_update(aac.id, @cas_hash, 400)
json['message'].should == 'Can not change type of authorization config, please delete and create new config.'
end
it "should update positions" do
aac = @account.account_authorization_configs.create!(@ldap_hash)
@ldap_hash['auth_host'] = '192.168.0.1'
aac2 = @account.account_authorization_configs.create!(@ldap_hash)
@ldap_hash['position'] = 1
call_update(aac2.id, @ldap_hash)
@account.account_authorization_config.id.should == aac2.id
end
it "should 404" do
call_update(0, {}, 404)
end
it "should return unauthorized error" do
course_with_student(:course => @course)
call_update(0, {}, 401)
end
end
it "should discard config parameters not recognized for the given auth_type" do
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'update_all', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"auth_type" => "cas", "auth_base" => "127.0.0.1", "auth_filter" => "discarded"}}})
@account.reload
@account.account_authorization_configs.size.should == 1
config = @account.account_authorization_configs.first
config.auth_type.should == 'cas'
config.auth_filter.should be_nil
context "/destroy" do
def call_destroy(id, status = 200)
json = api_call(:delete, "/api/v1/accounts/#{@account.id}/account_authorization_configs/#{id}",
{ :controller => 'account_authorization_configs', :action => 'destroy', :account_id => @account.id.to_s, :id => id.to_param, :format => 'json' },
{}, {}, :expected_status => status)
@account.reload
json
end
it "should delete" do
aac = @account.account_authorization_configs.create!(@saml_hash)
call_destroy(aac.id)
@account.account_authorization_config.should be_nil
end
it "should reposition correctly" do
aac = @account.account_authorization_configs.create!(@saml_hash)
aac2 = @account.account_authorization_configs.create!(@saml_hash)
aac3 = @account.account_authorization_configs.create!(@saml_hash)
aac4 = @account.account_authorization_configs.create!(@saml_hash)
call_destroy(aac.id)
aac2.reload
aac3.reload
aac4.reload
@account.account_authorization_configs.count.should == 3
@account.account_authorization_config.id.should == aac2.id
aac2.position.should == 1
aac3.position.should == 2
aac4.position.should == 3
call_destroy(aac3.id)
aac2.reload
aac4.reload
@account.account_authorization_configs.count.should == 2
@account.account_authorization_config.id.should == aac2.id
aac2.position.should == 1
aac4.position.should == 2
end
it "should 404" do
call_destroy(0, 404)
end
it "should return unauthorized error" do
course_with_student(:course => @course)
call_destroy(0, 401)
end
end
context "discovery url" do
append_before do
@account.auth_discovery_url = "http://example.com/auth"
@account.save!
end
it "should get the url" do
json = api_call(:get, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'show_discovery_url', :account_id => @account.id.to_s, :format => 'json' })
json.should == {'discovery_url' => @account.auth_discovery_url}
end
it "should set the url" do
json = api_call(:put, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'update_discovery_url', :account_id => @account.id.to_s, :format => 'json' },
{'discovery_url' => 'http://example.com/different_url'})
json.should == {'discovery_url' => 'http://example.com/different_url'}
@account.reload
@account.auth_discovery_url.should == 'http://example.com/different_url'
end
it "should clear if set to empty string" do
json = api_call(:put, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'update_discovery_url', :account_id => @account.id.to_s, :format => 'json' },
{'discovery_url' => ''})
json.should == {'discovery_url' => nil}
@account.reload
@account.auth_discovery_url.should == nil
end
it "should delete the url" do
json = api_call(:delete, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'destroy_discovery_url', :account_id => @account.id.to_s, :format => 'json' })
json.should == {'discovery_url' => nil}
@account.reload
@account.auth_discovery_url.should == nil
end
it "should return unauthorized" do
course_with_student(:course => @course)
api_call(:get, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'show_discovery_url', :account_id => @account.id.to_s, :format => 'json' },
{},{}, :expected_status => 401)
api_call(:put, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'update_discovery_url', :account_id => @account.id.to_s, :format => 'json' },
{'discovery_url' => ''},{}, :expected_status => 401)
@account.reload; @account.auth_discovery_url = "http://example.com/auth"
api_call(:delete, "/api/v1/accounts/#{@account.id}/account_authorization_configs/discovery_url",
{ :controller => 'account_authorization_configs', :action => 'destroy_discovery_url', :account_id => @account.id.to_s, :format => 'json' },
{},{}, :expected_status => 401)
@account.reload; @account.auth_discovery_url = "http://example.com/auth"
end
end
end

View File

@ -0,0 +1,200 @@
#
# 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 "AccountAuthorizationConfigs API", :type => :integration do
before do
@account = account_model(:name => 'root')
user_with_pseudonym(:active_all => true, :account => @account)
@account.add_user(@user)
end
it "should set the authorization config" do
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"auth_type" => "cas", "auth_base" => "127.0.0.1"}}})
@account.reload
@account.account_authorization_configs.size.should == 1
config = @account.account_authorization_configs.first
config.auth_type.should == 'cas'
config.auth_base.should == '127.0.0.1'
end
it "should set multiple ldap configs" do
ldap1 = {'auth_type' => 'ldap', 'auth_host' => '127.0.0.1', 'auth_filter' => 'filter1', 'auth_username' => 'username1', 'auth_password' => 'password1'}
ldap2 = {'auth_type' => 'ldap', 'auth_host' => '127.0.0.2', 'auth_filter' => 'filter2', 'auth_username' => 'username2', 'auth_password' => 'password2'}
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => ldap1, "1" => ldap2}})
@account.reload
@account.account_authorization_configs.size.should == 2
config1 = @account.account_authorization_configs.first
config2 = @account.account_authorization_configs.second
config1.auth_type.should == 'ldap'
config1.auth_host.should == '127.0.0.1'
config1.auth_filter.should == 'filter1'
config1.auth_username.should == 'username1'
config1.auth_decrypted_password.should == 'password1'
config2.auth_type.should == 'ldap'
config2.auth_host.should == '127.0.0.2'
config2.auth_filter.should == 'filter2'
config2.auth_username.should == 'username2'
config2.auth_decrypted_password.should == 'password2'
end
it "should update existing configs" do
config = @account.account_authorization_configs.create!("auth_type" => "cas", "auth_base" => "127.0.0.1")
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"id" => config.id.to_s, "auth_type" => "cas", "auth_base" => "127.0.0.2"}}})
@account.reload
config.reload
@account.account_authorization_configs.size.should == 1
@account.account_authorization_configs.first.should == config
config.auth_base.should == '127.0.0.2'
end
it "should delete configs not referenced" do
config = @account.account_authorization_configs.create!("auth_type" => "cas", "auth_base" => "127.0.0.1")
config = @account.account_authorization_configs.create!("auth_type" => "cas", "auth_base" => "127.0.0.1")
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"id" => config.id.to_s, "auth_type" => "cas", "auth_base" => "127.0.0.2"}}})
@account.reload
@account.account_authorization_configs.count.should == 1
end
it "should discard config parameters not recognized for the given auth_type" do
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{ :controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json' },
{ :account_authorization_config => {"0" => {"auth_type" => "cas", "auth_base" => "127.0.0.1", "auth_filter" => "discarded"}}})
@account.reload
@account.account_authorization_configs.size.should == 1
config = @account.account_authorization_configs.first
config.auth_type.should == 'cas'
config.auth_filter.should be_nil
end
context "saml" do
append_before do
@saml1 = {'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'}
@saml2 = {'auth_type' => 'saml', 'idp_entity_id' => 'http://example.com/saml2', 'log_in_url' => 'http://example.com/saml1/sli2', 'log_out_url' => 'http://example.com/saml1/slo2', 'certificate_fingerprint' => '222111', 'identifier_format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}
end
def update_saml(data=nil)
data ||= {:account_authorization_config => {"0" => @saml1, "1" => @saml2}}
api_call(:post, "/api/v1/accounts/#{@account.id}/account_authorization_configs",
{:controller => 'account_authorization_configs', :action => 'create', :account_id => @account.id.to_s, :format => 'json'},
data)
end
it "should set multiple saml configs" do
update_saml
@account.reload
@account.account_authorization_configs.size.should == 2
config1 = @account.account_authorization_configs.first
config2 = @account.account_authorization_configs.second
config1.auth_type.should == 'saml'
config1.idp_entity_id.should == 'http://example.com/saml1'
config1.log_in_url.should == 'http://example.com/saml1/sli'
config1.log_out_url.should == 'http://example.com/saml1/slo'
config1.certificate_fingerprint.should == '111222'
config1.identifier_format.should == 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
config2.auth_type.should == 'saml'
config2.idp_entity_id.should == 'http://example.com/saml2'
config2.log_in_url.should == 'http://example.com/saml1/sli2'
config2.log_out_url.should == 'http://example.com/saml1/slo2'
config2.certificate_fingerprint.should == '222111'
config2.identifier_format.should == 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
end
it "should update the existing AACs" do
update_saml
@account.reload
config1 = @account.account_authorization_configs.first
config2 = @account.account_authorization_configs.second
@saml1['idp_entity_id'] = 'different'
@saml1['id'] = config1.id
@saml2['idp_entity_id'] = 'different2'
@saml2['id'] = config2.id
update_saml
@account.reload
@account.account_authorization_configs.size.should == 2
config1.reload
config1.idp_entity_id.should == 'different'
config2.reload
config2.idp_entity_id.should == 'different2'
end
it "should use the first config as the default" do
update_saml
@account.account_authorization_config.idp_entity_id.should == 'http://example.com/saml1'
end
it "should create new configs if they are reordered" do
update_saml
config1 = @account.account_authorization_configs.first
config2 = @account.account_authorization_configs.second
update_saml(:account_authorization_config => {"0" => @saml2, "1" => @saml1})
@account.reload
@account.account_authorization_configs.count.should == 2
config3 = @account.account_authorization_configs.first
config4 = @account.account_authorization_configs.second
config3.idp_entity_id.should == 'http://example.com/saml2'
config3.id.should_not == config2.id
config4.idp_entity_id.should == 'http://example.com/saml1'
config4.id.should_not == config1.id
end
it "should set the discovery url" do
update_saml({:account_authorization_config => {"0" => @saml1, "1" => @saml2}, :discovery_url => 'http://example.com/auth_discovery'})
@account.reload
@account.auth_discovery_url.should == 'http://example.com/auth_discovery'
end
it "should clear the discovery url" do
@account.auth_discovery_url = 'http://example.com/auth_discovery'
@account.save!
update_saml({:account_authorization_config => {"0" => @saml1, "1" => @saml2}, :discovery_url => ''})
@account.reload
@account.auth_discovery_url.should == nil
@account.auth_discovery_url = 'http://example.com/auth_discovery'
@account.save!
update_saml({:account_authorization_config => {"0" => @saml1}, :discovery_url => 'http://example.com/wutwut'})
@account.reload
@account.auth_discovery_url.should == nil
end
end
end

View File

@ -1,39 +0,0 @@
#
# 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__) + '/../spec_helper')
describe AccountAuthorizationConfigsController do
def account_with_admin_logged_in(opts = {})
@account = Account.default
account_admin_user
user_session(@admin)
end
describe "PUT 'update'" do
it "should disable open registration when setting delegated auth" do
account_with_admin_logged_in
@account.settings = { :open_registration => true }
@account.save!
put 'update_all', :account_id => @account.id, :account_authorization_config => [ [0, {:auth_type => 'cas'}]]
response.should be_success
@account.reload
@account.open_registration?.should be_false
end
end
end

View File

@ -189,7 +189,7 @@ describe PseudonymSessionsController do
@pseudonym.save!
controller.stubs(:saml_response).returns(
stub('response', :is_valid? => true, :success_status? => true, :name_id => unique_id, :name_qualifier => nil, :session_index => nil)
stub('response', :is_valid? => true, :success_status? => true, :name_id => unique_id, :name_qualifier => nil, :session_index => nil, :process => nil)
)
controller.request.env['canvas.domain_root_account'] = account1
@ -202,7 +202,7 @@ describe PseudonymSessionsController do
session.reset
controller.stubs(:saml_response).returns(
stub('response', :is_valid? => true, :success_status? => true, :name_id => unique_id, :name_qualifier => nil, :session_index => nil)
stub('response', :is_valid? => true, :success_status? => true, :name_id => unique_id, :name_qualifier => nil, :session_index => nil, :process => nil)
)
controller.request.env['canvas.domain_root_account'] = account2
@ -214,6 +214,173 @@ describe PseudonymSessionsController do
Setting.set_config("saml", nil)
end
context "multiple SAML configs" do
before do
@account = account_with_saml(:saml_log_in_url => "https://example.com/idp1/sli")
@unique_id = 'foo@example.com'
@user1 = user_with_pseudonym(:active_all => true, :username => @unique_id, :account => @account)
@aac1 = @account.account_authorization_configs.first
@aac1.idp_entity_id = "https://example.com/idp1"
@aac1.log_out_url = "https://example.com/idp1/slo"
@aac1.save!
@aac2 = @aac1.clone
@aac2.idp_entity_id = "https://example.com/idp2"
@aac2.log_in_url = "https://example.com/idp2/sli"
@aac2.log_out_url = "https://example.com/idp2/slo"
@aac2.position = nil
@aac2.save!
@stub_hash = {:issuer => @aac2.idp_entity_id, :is_valid? => true, :success_status? => true, :name_id => @unique_id, :name_qualifier => nil, :session_index => nil, :process => nil}
end
context "/saml_consume" do
def get_consume
controller.stubs(:saml_response).returns(
stub('response', @stub_hash)
)
controller.request.env['canvas.domain_root_account'] = @account
get 'saml_consume', :SAMLResponse => "foo", :RelayState => "/courses"
end
it "should find the SAML config by entity_id" do
@aac1.any_instantiation.expects(:saml_settings).never
@aac2.any_instantiation.expects(:saml_settings)
get_consume
response.should redirect_to(courses_url)
session[:saml_unique_id].should == @unique_id
end
it "/saml_consume should redirect to auth url if no AAC found" do
@account.auth_discovery_url = "http://example.com/discover"
@account.save!
@stub_hash[:issuer] = "hahahahahahaha"
get_consume
response.should redirect_to(@account.auth_discovery_url + "?message=Canvas%20did%20not%20recognize%20your%20identity%20provider")
end
it "/saml_consume should redirect to login screen with message if no AAC found" do
@stub_hash[:issuer] = "hahahahahahaha"
get_consume
flash[:delegated_message].should == "The institution you logged in from is not configured on this account."
response.should redirect_to(login_url(:no_auto=>'true'))
end
end
context "/new" do
def get_new(aac_id=nil)
controller.request.env['canvas.domain_root_account'] = @account
if aac_id
get 'new', :account_authorization_config_id => aac_id
else
get 'new'
end
end
it "should redirect to auth discovery url" do
@account.auth_discovery_url = "http://example.com/discover"
@account.save!
get_new
response.should redirect_to(@account.auth_discovery_url)
end
it "should redirect to default login" do
get_new
response.headers['Location'].starts_with?(controller.delegated_auth_redirect_uri(@aac1.log_in_url)).should be_true
end
it "should use the specified AAC" do
get_new("#{@aac1.id}")
response.headers['Location'].starts_with?(controller.delegated_auth_redirect_uri(@aac1.log_in_url)).should be_true
get_new("#{@aac2.id}")
response.headers['Location'].starts_with?(controller.delegated_auth_redirect_uri(@aac2.log_in_url)).should be_true
end
it "should redirect to auth discovery with unknown specified AAC" do
@account.auth_discovery_url = "http://example.com/discover"
@account.save!
get_new("0")
response.should redirect_to(@account.auth_discovery_url + "?message=The%20Canvas%20account%20has%20no%20authentication%20configuration%20with%20that%20id")
end
it "should redirect to login screen with message if unknown specified AAC" do
get_new("0")
flash[:delegated_message].should == "The Canvas account has no authentication configuration with that id"
response.should redirect_to(login_url(:no_auto=>'true'))
end
end
context "logging out" do
append_before do
controller.stubs(:saml_response).returns(
stub('response', @stub_hash)
)
controller.request.env['canvas.domain_root_account'] = @account
get 'saml_consume', :SAMLResponse => "foo", :RelayState => "/courses"
response.should redirect_to(courses_url)
session[:saml_unique_id].should == @unique_id
session[:saml_aac_id].should == @aac2.id
end
context '/destroy' do
it "should forward to correct IdP" do
get 'destroy'
response.headers['Location'].starts_with?(@aac2.log_out_url + "?SAMLRequest=").should be_true
end
it "should fail gracefully if AAC id gone" do
session[:saml_aac_id] = 0
get 'destroy'
flash[:message].should == "Canvas was unable to log you out at your identity provider"
response.should redirect_to(login_url(:no_auto=>'true'))
end
end
context '/saml_logout' do
def get_saml_consume
controller.stubs(:saml_logout_response).returns(
stub('response', @stub_hash)
)
controller.request.env['canvas.domain_root_account'] = @account
get 'saml_logout', :SAMLResponse => "foo", :RelayState => "/courses"
end
it "should find the correct AAC" do
@aac1.any_instantiation.expects(:saml_settings).never
@aac2.any_instantiation.expects(:saml_settings)
get_saml_consume
response.should redirect_to(:action => :destroy)
end
it "should still logout if AAC config not found" do
@aac1.any_instantiation.expects(:saml_settings).never
@aac2.any_instantiation.expects(:saml_settings).never
@stub_hash[:issuer] = "nobody eh"
get_saml_consume
response.should redirect_to(:action => :destroy)
end
end
end
end
context "login attributes" do
before(:each) do
Setting.set_config("saml", {})
@ -232,7 +399,7 @@ describe PseudonymSessionsController do
@aac.save
controller.stubs(:saml_response).returns(
stub('response', :is_valid? => true, :success_status? => true, :name_id => nil, :name_qualifier => nil, :session_index => nil,
stub('response', :is_valid? => true, :success_status? => true, :name_id => nil, :name_qualifier => nil, :session_index => nil, :process => nil,
:saml_attributes => {
'eduPersonPrincipalName' => "#{@unique_id}@example.edu"
})
@ -249,7 +416,7 @@ describe PseudonymSessionsController do
@aac.save
controller.stubs(:saml_response).returns(
stub('response', :is_valid? => true, :success_status? => true, :name_id => @unique_id, :name_qualifier => nil, :session_index => nil)
stub('response', :is_valid? => true, :success_status? => true, :name_id => @unique_id, :name_qualifier => nil, :session_index => nil, :process => nil)
)
controller.request.env['canvas.domain_root_account'] = @account
@ -273,7 +440,7 @@ describe PseudonymSessionsController do
@pseudonym.save!
controller.stubs(:saml_response).returns(
stub('response', :is_valid? => true, :success_status? => true, :name_id => nil, :name_qualifier => nil, :session_index => nil,
stub('response', :is_valid? => true, :success_status? => true, :name_id => nil, :name_qualifier => nil, :session_index => nil, :process => nil,
:saml_attributes => {
'eduPersonPrincipalName' => "#{unique_id}@example.edu"
})
@ -295,7 +462,7 @@ describe PseudonymSessionsController do
@pseudonym.save!
controller.stubs(:saml_response).returns(
stub('response', :is_valid? => true, :success_status? => true, :name_id => unique_id, :name_qualifier => nil, :session_index => nil)
stub('response', :is_valid? => true, :success_status? => true, :name_id => unique_id, :name_qualifier => nil, :session_index => nil, :process => nil)
)
controller.request.env['canvas.domain_root_account'] = account

View File

@ -32,3 +32,4 @@ describe "acts_as_list" do
end
end
end