diff --git a/Gemfile b/Gemfile index 5c0d3604a4f..19a4a68dfad 100644 --- a/Gemfile +++ b/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' diff --git a/app/coffeescripts/behaviors/elementToggler.coffee b/app/coffeescripts/behaviors/elementToggler.coffee index e9f66fae70e..ac9a728ccfc 100644 --- a/app/coffeescripts/behaviors/elementToggler.coffee +++ b/app/coffeescripts/behaviors/elementToggler.coffee @@ -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 diff --git a/app/controllers/account_authorization_configs_controller.rb b/app/controllers/account_authorization_configs_controller.rb index 6b6f184d93e..20efe4ec361 100644 --- a/app/controllers/account_authorization_configs_controller.rb +++ b/app/controllers/account_authorization_configs_controller.rb @@ -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:///api/v1/account//account_authorization_configs' \ + # -H 'Authorization: Bearer ' + # + # @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:///api/v1/account//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 ' + # + # @example_request + # # Create SAML config + # curl 'https:///api/v1/account//account_authorization_configs' \ + # -F 'auth_type=saml' \ + # -F 'idp_entity_id=' \ + # -F 'log_in_url=' \ + # -F 'log_out_url=' \ + # -F 'certificate_fingerprint=' \ + # -H 'Authorization: Bearer ' + # + # @example_request + # # Create CAS config + # curl 'https:///api/v1/account//account_authorization_configs' \ + # -F 'auth_type=cas' \ + # -F 'auth_base=cas.mydomain.edu' \ + # -F 'log_in_url=' \ + # -H 'Authorization: Bearer ' + # + # _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:///api/v1/account//account_authorization_configs/' \ + # -F 'idp_entity_id=' \ + # -F 'log_in_url=' \ + # -H 'Authorization: Bearer ' + # + # @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:///api/v1/account//account_authorization_configs/' \ + # -H 'Authorization: Bearer ' + # + # @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:///api/v1/account//account_authorization_configs/' \ + # -H 'Authorization: Bearer ' + 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:///api/v1/account//account_authorization_configs/discovery_url' \ + # -H 'Authorization: Bearer ' + # + # @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:///api/v1/account//account_authorization_configs/discovery_url' \ + # -F 'discovery_url=' \ + # -H 'Authorization: Bearer ' + # + # @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:///api/v1/account//account_authorization_configs/discovery_url' \ + # -H 'Authorization: Bearer ' + # + 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]) diff --git a/app/controllers/pseudonym_sessions_controller.rb b/app/controllers/pseudonym_sessions_controller.rb index 4601658ba96..ddc38ae3b48 100644 --- a/app/controllers/pseudonym_sessions_controller.rb +++ b/app/controllers/pseudonym_sessions_controller.rb @@ -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 diff --git a/app/models/account.rb b/app/models/account.rb index ffb3704d5dc..e9a5d2bccf8 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -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 diff --git a/app/models/account_authorization_config.rb b/app/models/account_authorization_config.rb index 858b2e72f13..4ce53dbeac7 100644 --- a/app/models/account_authorization_config.rb +++ b/app/models/account_authorization_config.rb @@ -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' diff --git a/app/views/account_authorization_configs/_cas_settings.html.erb b/app/views/account_authorization_configs/_cas_settings.html.erb index 260931fd072..b36d037208e 100644 --- a/app/views/account_authorization_configs/_cas_settings.html.erb +++ b/app/views/account_authorization_configs/_cas_settings.html.erb @@ -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"' : '' %>
> @@ -27,7 +28,7 @@ <%= f.blabel :login_handle_name, :en => "Login Label" %> <%= f.text_field :login_handle_name, :class => "auth_form", :style => "width: 300px;", :placeholder => AccountAuthorizationConfig.default_delegated_login_handle_name %> - +
<%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %>
diff --git a/app/views/account_authorization_configs/_ldap_settings.html.erb b/app/views/account_authorization_configs/_ldap_settings.html.erb index 8239e189622..b36b8f731e3 100644 --- a/app/views/account_authorization_configs/_ldap_settings.html.erb +++ b/app/views/account_authorization_configs/_ldap_settings.html.erb @@ -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"' : '' %>
> <% form_tag(context_url(@account, :context_update_all_authorization_configs_url), :method => :put, :id => form_id, :class=>"auth_type ldap_form" ) do %> - <%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @account_configs[0] } %> + <%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @ldap_configs[0] } %> - <% 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] } %> <% end %> @@ -36,24 +44,29 @@ -
<%= before_label(t(:auth_type_label, "Type")) %> <%= t :setting_type_ldap, 'LDAP' %>
<%= f.blabel :login_handle_name, :en => "Login Label" %> <%= f.text_field :login_handle_name, :class => "auth_form", :style => "width: 300px;", :placeholder => AccountAuthorizationConfig.default_login_handle_name %> - +
<%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %>
@@ -28,7 +36,7 @@
<%= f.text_field :change_password_url, :class => "auth_form", :style => "width: 300px;" %>
<%= t(:change_password_url_help, "Leave blank for default Canvas behavior") %>
- <%= @account_configs[0].change_password_url || t(:change_password_url_not_specified, "None specified") %> + <%= @ldap_configs[0].change_password_url || t(:change_password_url_not_specified, "None specified") %>
"> - <%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @account_configs[1] } %> +
"> + <%= render :partial => 'ldap_timeout_error', :locals => { :account_config => @ldap_configs[1] } %> - + - <% 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 %>
<%= before_label(t(:auth_type_label, "Type")) %><%= t(:secondary_ldap_label, "Secondary LDAP") %><% unless @account_configs.length > 2 %> <%= t(:remove_secondary_ldap_link, "(Remove)") %><% end %><%= t(:secondary_ldap_label, "Secondary LDAP") %> + <% unless @ldap_configs.length > 2 %> + + <%= t(:remove_secondary_ldap_link, "(Remove)") %> + <% end %> +
- <% 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? %> <%= render :partial => 'ldap_timeout_error', :locals => { :account_config => aac } %> diff --git a/app/views/account_authorization_configs/_saml_settings.html.erb b/app/views/account_authorization_configs/_saml_settings.html.erb index 8a86515166e..bfca8dd8b8e 100644 --- a/app/views/account_authorization_configs/_saml_settings.html.erb +++ b/app/views/account_authorization_configs/_saml_settings.html.erb @@ -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? +%>
> -<% 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 %> -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
<%= f.blabel :auth_type, :en => "Type" %> - - <%= @account_config.auth_type || 'SAML' %> - - <%= @account_config.auth_type || 'SAML' %> -
<%= f.blabel :log_in_url, :en => "Log On URL" %> - <%= f.text_field :log_in_url, :class => "auth_form", :style => "width: 450px;" %> - -
<%= f.blabel :log_out_url, :en => "Log Out URL" %> - <%= f.text_field :log_out_url, :class => "auth_form", :style => "width: 450px;" %> - <%= @account_config.log_out_url %> -
<%= f.blabel :change_password_url, :en => "Change Password Link" %> - <%= f.text_field :change_password_url, :class => "auth_form", :style => "width: 450px;" %> - <%= @account_config.change_password_url %> -
<%= f.blabel :certificate_fingerprint, :en => "Certificate Fingerprint" %> - <%= f.text_field :certificate_fingerprint, :class => "auth_form", :style => "width: 450px;" %> - <%= @account_config.certificate_fingerprint %> -
<%= f.blabel :login_attribute, :en => "Login Attribute" %> - <%= f.select :login_attribute, @saml_login_attributes, {}, {:class => "auth_form"} %> - -
<%= f.blabel :identifier_format, :en => "Identifier Format" %> - <%= f.select :identifier_format, @saml_identifiers, {}, {:class => "auth_form"} %> - <%= @account_config.identifier_format %> -
<%= f.blabel :requested_authn_context, :en => "Authentication Context" %> - <%= f.select :requested_authn_context, @saml_authn_contexts, {}, {:class => "auth_form"} %> - <%= @account_config.requested_authn_context %> -
<%= f.blabel :login_handle_name, :en => "Login Label" %> - <%= f.text_field :login_handle_name, :class => "auth_form", :style => "width: 300px;", :placeholder => AccountAuthorizationConfig.default_delegated_login_handle_name %> - - -
<%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %> -
-
- - - - -
- - <%= link_to(t(:saml_meta_data_link, "Click here to see the service provider identity XML for this account."), :saml_meta_data) %> - -
- -
+ + <% @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) + %> + +
+ + + <% + 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 %> + +
+ +
+ <%= f.text_field :idp_entity_id, :class => "input-xlarge" %> +
+
+ +
+ +
+ <%= f.text_field :log_in_url, :class => "input-xlarge" %> +
+
+ +
+ +
+ <%= f.text_field :log_out_url, :class => "input-xlarge" %> +
+
+ +
+ +
+ <%= f.text_field :change_password_url, :class => "input-xlarge" %> +
+
+ +
+ +
+ <%= f.text_field :certificate_fingerprint, :class => "input-xlarge" %> +
+
+ +
+ +
+ <%= f.select :login_attribute, @saml_login_attributes, {}, {:class => "input-xlarge"} %> +
+
+ +
+ +
+ <%= f.select :identifier_format, @saml_identifiers, {}, {:class => "input-xlarge"} %> +
+
+ +
+ +
+ <%= f.select :requested_authn_context, @saml_authn_contexts, {}, {:class => "input-xlarge"} %> +
+
+ +
+ +
+ <%= f.text_field :login_handle_name, :class => "input-xlarge" %> +

<%= t(:login_handle_name_description, "The label used for unique login identifiers. Examples: Login, Username, Student ID, etc.") %>

+
+
+ <% if @saml_configs.length > 1 %> +
+ +
+ <% options = config.new_record? ? [["Last", nil]] + position_options : position_options %> + <%= f.select :position, options, {}, {:class => "input-xlarge"} %> +
+
+ <% end %> + + <% unless config.new_record? %> + + <% end %> + + <% end %> + <% end %> +
+ + <% end %> + + + + + +

<%= t(:saml_debugging, "Debugging") %>

- +

<%= t 'saml_debug_instructions', <<-TEXT @@ -110,7 +205,7 @@ <%= t('refresh_debugging', 'Refresh') %> <%= t('stop_debugging', 'Stop Debugging') %>

- +
<% if @account_config && @account_config.debugging? %> <%= render :partial => 'saml_testing.html' %> @@ -118,7 +213,5 @@
- - <% end %> -<% end %> +
diff --git a/app/views/account_authorization_configs/index.html.erb b/app/views/account_authorization_configs/index.html.erb index 70daeab65f3..176ab2242b1 100644 --- a/app/views/account_authorization_configs/index.html.erb +++ b/app/views/account_authorization_configs/index.html.erb @@ -12,8 +12,8 @@ <% content_for :right_side do %>
- <% has_auth = !@account_configs.first.auth_type.nil? %> - <%= image_tag "edit.png" %><%= t(:edit_auth_link, "Edit Details")%> + <% has_auth = @account_configs.any? %> + <%= image_tag "edit.png" %><%= t(:edit_auth_link, "Edit Details")%> "><%= image_tag "pending_review.png" %><%= t(:test_ldap_link, "Test Authentication")%> <%= 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}" %>
@@ -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? %>
<%= t(:no_auth_type_description, "This account does not currently integrate with an identity provider.") %>
<% end %> diff --git a/config/routes.rb b/config/routes.rb index f5e85c89038..781df34a1a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/db/migrate/20120921155127_add_saml_properties.rb b/db/migrate/20120921155127_add_saml_properties.rb new file mode 100644 index 00000000000..9b067782b1e --- /dev/null +++ b/db/migrate/20120921155127_add_saml_properties.rb @@ -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 diff --git a/lib/api/v1/account_authorization_config.rb b/lib/api/v1/account_authorization_config.rb new file mode 100644 index 00000000000..63d9a574b1d --- /dev/null +++ b/lib/api/v1/account_authorization_config.rb @@ -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 . +# + +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 diff --git a/lib/authentication_methods.rb b/lib/authentication_methods.rb index 94511684c23..6310d610477 100644 --- a/lib/authentication_methods.rb +++ b/lib/authentication_methods.rb @@ -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 diff --git a/public/javascripts/account_authorization_configs.js b/public/javascripts/account_authorization_configs.js index 9adeb563813..60b97a99918 100644 --- a/public/javascripts/account_authorization_configs.js +++ b/public/javascripts/account_authorization_configs.js @@ -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; } diff --git a/spec/apis/v1/account_authorization_configs_api_spec.rb b/spec/apis/v1/account_authorization_configs_api_spec.rb index a50daabbd9b..1383c11b56b 100644 --- a/spec/apis/v1/account_authorization_configs_api_spec.rb +++ b/spec/apis/v1/account_authorization_configs_api_spec.rb @@ -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 diff --git a/spec/apis/v1/account_authorization_configs_deprecated_api_spec.rb b/spec/apis/v1/account_authorization_configs_deprecated_api_spec.rb new file mode 100644 index 00000000000..a25976653cd --- /dev/null +++ b/spec/apis/v1/account_authorization_configs_deprecated_api_spec.rb @@ -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 . +# + +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 diff --git a/spec/controllers/account_authorization_configs_controller_spec.rb b/spec/controllers/account_authorization_configs_controller_spec.rb deleted file mode 100644 index 200e44600df..00000000000 --- a/spec/controllers/account_authorization_configs_controller_spec.rb +++ /dev/null @@ -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 . -# - -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 diff --git a/spec/controllers/pseudonym_sessions_controller_spec.rb b/spec/controllers/pseudonym_sessions_controller_spec.rb index 542f5d080f6..06b4da228d1 100644 --- a/spec/controllers/pseudonym_sessions_controller_spec.rb +++ b/spec/controllers/pseudonym_sessions_controller_spec.rb @@ -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 diff --git a/spec/lib/acts_as_list.rb b/spec/lib/acts_as_list.rb index 707a6a1d46e..dcd64e5a135 100644 --- a/spec/lib/acts_as_list.rb +++ b/spec/lib/acts_as_list.rb @@ -32,3 +32,4 @@ describe "acts_as_list" do end end end +