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:
parent
82d7318c7d
commit
c54d3060b2
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 } %>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %>
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -32,3 +32,4 @@ describe "acts_as_list" do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue