Skip to content

Commit cc146e1

Browse files
committed
make saml entity use the account's domain
AccountAuthorizationConfigs now require the entity_id to be set for SAML configs. It will be set to the domain of the account. For existing SAML configs, the entity_id will be set to what is in the SAML config file if there is one. This commit also allows SAML meta data to show up for all domains even if they're not configured for SAML. This helps admins set up initial SAML configurations. Test Plan: * Create a saml configuration for an account * Look at the metadata, the entity_id should be set to the domain/saml2 and not what is in the saml config file * Try loading the meta data for an account with no SAML config. It should load and have the proper entity_id closes #6713 Change-Id: Ia98543c996285d9b1febd788c3f3ec072b672b12 Reviewed-on: https://gerrit.instructure.com/7967 Reviewed-by: Cody Cutrer <cody@instructure.com> Tested-by: Hudson <hudson@instructure.com>
1 parent cc06e7e commit cc146e1

6 files changed

Lines changed: 213 additions & 43 deletions

File tree

app/controllers/accounts_controller.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -387,12 +387,8 @@ def saml_meta_data
387387
# This needs to be publicly available since external SAML
388388
# servers need to be able to access it without being authenticated.
389389
# It is used to disclose our SAML configuration settings.
390-
if @domain_root_account.account_authorization_config and @domain_root_account.account_authorization_config.auth_type == 'saml'
391-
settings = @domain_root_account.account_authorization_config.saml_settings(request.env['canvas.account_domain'])
392-
render :xml => Onelogin::Saml::MetaData.create(settings)
393-
else
394-
render :xml => ""
395-
end
390+
settings = AccountAuthorizationConfig.saml_settings_for_account(@domain_root_account, request.env['canvas.account_domain'])
391+
render :xml => Onelogin::Saml::MetaData.create(settings)
396392
end
397393

398394
# TODO Refactor add_account_user and remove_account_user actions into

app/models/account_authorization_config.rb

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ class AccountAuthorizationConfig < ActiveRecord::Base
2727
:certificate_fingerprint, :entity_id, :change_password_url,
2828
:login_handle_name, :ldap_filter, :auth_filter
2929

30+
before_validation :set_saml_entity_id, :if => Proc.new { |aac| aac.saml_authentication? }
3031
validates_presence_of :account_id
32+
validates_presence_of :entity_id, :if => Proc.new{|aac| aac.saml_authentication?}
3133
after_save :disable_open_registration_if_delegated
3234

3335
def ldap_connection
@@ -41,6 +43,10 @@ def ldap_connection
4143
ldap
4244
end
4345

46+
def set_saml_entity_id
47+
self.entity_id ||= saml_default_entity_id
48+
end
49+
4450
def sanitized_ldap_login(login)
4551
[ [ '\\', '\5c' ], [ '*', '\2a' ], [ '(', '\28' ], [ ')', '\29' ], [ "\00", '\00' ] ].each do |re|
4652
login.gsub!(re[0], re[1])
@@ -79,56 +85,75 @@ def auth_decrypted_password
7985
return nil unless self.auth_password_salt && self.auth_crypted_password
8086
Canvas::Security.decrypt_password(self.auth_crypted_password, self.auth_password_salt, 'instructure_auth')
8187
end
88+
89+
def self.saml_default_entity_id_for_account(account)
90+
"http://#{HostUrl.context_host(account)}/saml2"
91+
end
92+
93+
def saml_default_entity_id
94+
AccountAuthorizationConfig.saml_default_entity_id_for_account(self.account)
95+
end
8296

8397
def saml_settings(preferred_account_domain=nil)
8498
return nil unless self.auth_type == 'saml'
85-
app_config = Setting.from_config('saml')
86-
raise "This Canvas instance isn't configured for SAML" unless app_config
8799

88100
unless @saml_settings
89-
domain = HostUrl.context_host(self.account, preferred_account_domain)
90-
@saml_settings = Onelogin::Saml::Settings.new
101+
@saml_settings = AccountAuthorizationConfig.saml_settings_for_account(self.account, preferred_account_domain)
91102

92-
@saml_settings.issuer = self.entity_id || app_config[:entity_id]
93103
@saml_settings.idp_sso_target_url = self.log_in_url
94104
@saml_settings.idp_slo_target_url = self.log_out_url
95105
@saml_settings.idp_cert_fingerprint = self.certificate_fingerprint
96106
@saml_settings.name_identifier_format = self.identifier_format
97-
if ENV['RAILS_ENV'] == 'development'
98-
# if you set the domain to go to your local box in /etc/hosts you can test saml
99-
@saml_settings.assertion_consumer_service_url = "http://#{domain}/saml_consume"
100-
@saml_settings.sp_slo_url = "http://#{domain}/saml_logout"
101-
else
102-
@saml_settings.assertion_consumer_service_url = "https://#{domain}/saml_consume"
103-
@saml_settings.sp_slo_url = "https://#{domain}/saml_logout"
104-
end
105-
@saml_settings.tech_contact_name = app_config[:tech_contact_name] || 'Webmaster'
106-
@saml_settings.tech_contact_email = app_config[:tech_contact_email]
107-
108-
encryption = app_config[:encryption]
109-
if encryption.is_a?(Hash) && File.exists?(encryption[:xmlsec_binary])
110-
resolve_path = lambda {|path|
111-
if path.nil?
112-
nil
113-
elsif path[0,1] == '/'
114-
path
115-
else
116-
File.join(Rails.root, 'config', path)
117-
end
118-
}
119-
120-
private_key_path = resolve_path.call(encryption[:private_key])
121-
certificate_path = resolve_path.call(encryption[:certificate])
122-
123-
if File.exists?(private_key_path) && File.exists?(certificate_path)
124-
@saml_settings.xmlsec1_path = encryption[:xmlsec_binary]
125-
@saml_settings.xmlsec_certificate = certificate_path
126-
@saml_settings.xmlsec_privatekey = private_key_path
107+
end
108+
109+
@saml_settings
110+
end
111+
112+
def self.saml_settings_for_account(account, preferred_account_domain=nil)
113+
app_config = Setting.from_config('saml') || {}
114+
domain = HostUrl.context_host(account, preferred_account_domain)
115+
116+
settings = Onelogin::Saml::Settings.new
117+
if ENV['RAILS_ENV'] == 'development'
118+
# if you set the domain to go to your local box in /etc/hosts you can test saml
119+
settings.assertion_consumer_service_url = "http://#{domain}/saml_consume"
120+
settings.sp_slo_url = "http://#{domain}/saml_logout"
121+
else
122+
settings.assertion_consumer_service_url = "https://#{domain}/saml_consume"
123+
settings.sp_slo_url = "https://#{domain}/saml_logout"
124+
end
125+
settings.tech_contact_name = app_config[:tech_contact_name] || 'Webmaster'
126+
settings.tech_contact_email = app_config[:tech_contact_email] || ''
127+
128+
if account.saml_authentication?
129+
settings.issuer = account.account_authorization_config.entity_id
130+
else
131+
settings.issuer = saml_default_entity_id_for_account(account)
132+
end
133+
134+
encryption = app_config[:encryption]
135+
if encryption.is_a?(Hash) && File.exists?(encryption[:xmlsec_binary])
136+
resolve_path = lambda { |path|
137+
if path.nil?
138+
nil
139+
elsif path[0, 1] == '/'
140+
path
141+
else
142+
File.join(Rails.root, 'config', path)
127143
end
144+
}
145+
146+
private_key_path = resolve_path.call(encryption[:private_key])
147+
certificate_path = resolve_path.call(encryption[:certificate])
148+
149+
if File.exists?(private_key_path) && File.exists?(certificate_path)
150+
settings.xmlsec1_path = encryption[:xmlsec_binary]
151+
settings.xmlsec_certificate = certificate_path
152+
settings.xmlsec_privatekey = private_key_path
128153
end
129154
end
130155

131-
@saml_settings
156+
settings
132157
end
133158

134159
def email_identifier?
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
class SetSamlEntityId < ActiveRecord::Migration
2+
3+
# Sets all existing SAML configs to be what is currently in the config so they won't break
4+
# If there is no config use the host of the account
5+
# All future new SAML configs will use the host of the account
6+
def self.up
7+
old_default_domain = nil
8+
if app_config = Setting.from_config('saml')
9+
old_default_domain = app_config[:entity_id]
10+
end
11+
12+
AccountAuthorizationConfig.find_all_by_auth_type("saml").each do |aac|
13+
if aac.entity_id.blank?
14+
aac.entity_id = old_default_domain || aac.saml_default_entity_id
15+
aac.save!
16+
end
17+
end
18+
end
19+
20+
def self.down
21+
#raise ActiveRecord::IrreversibleMigration
22+
end
23+
end

spec/integration/account_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#
2+
# Copyright (C) 2012 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
20+
21+
describe AccountsController do
22+
23+
context "SAML meta data" do
24+
before(:each) do
25+
Setting.set_config('saml', {
26+
:tech_contact_name => nil,
27+
:tech_contact_email => nil
28+
})
29+
@account = Account.create!(:name => "test")
30+
end
31+
32+
it 'should render for non SAML configured accounts' do
33+
get "/saml_meta_data"
34+
response.should be_success
35+
response.body.should_not == ""
36+
end
37+
38+
it "should use the correct entity_id" do
39+
HostUrl.stubs(:default_host).returns('bob.cody.instructure.com')
40+
@aac = @account.account_authorization_configs.create!(:auth_type => "saml")
41+
42+
get "/saml_meta_data"
43+
response.should be_success
44+
doc = Nokogiri::XML(response.body)
45+
doc.at_css("EntityDescriptor")['entityID'].should == "http://bob.cody.instructure.com/saml2"
46+
end
47+
48+
end
49+
50+
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#
2+
# Copyright (C) 2012 Instructure, Inc.
3+
#
4+
# This file is part of Canvas.
5+
#
6+
# Canvas is free software: you can redistribute it and/or modify it under
7+
# the terms of the GNU Affero General Public License as published by the Free
8+
# Software Foundation, version 3 of the License.
9+
#
10+
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
11+
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12+
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
13+
# details.
14+
#
15+
# You should have received a copy of the GNU Affero General Public License along
16+
# with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb')
20+
require 'db/migrate/20120106220543_set_saml_entity_id'
21+
22+
describe SetSamlEntityId do
23+
before(:each) do
24+
Setting.set_config('saml', {
25+
:entity_id => "http://watup_fool.com/saml2"
26+
})
27+
HostUrl.stubs(:default_host).returns('bob.cody.instructure.com')
28+
@account = Account.new
29+
@account.save
30+
@aac = @account.account_authorization_configs.create!(:auth_type => "saml")
31+
AccountAuthorizationConfig.update_all(:entity_id => nil, :id => [@aac.id])
32+
end
33+
34+
it "should set the entity_id to the current setting if none is set" do
35+
SetSamlEntityId.up
36+
@aac.reload
37+
@aac.entity_id.should == "http://watup_fool.com/saml2"
38+
end
39+
40+
it "should leave the entity_id the same if already set" do
41+
@aac.entity_id = "haha"
42+
@aac.save
43+
44+
SetSamlEntityId.up
45+
46+
@aac.reload
47+
@aac.entity_id.should == "haha"
48+
end
49+
50+
it "should use the account's domain if no config is set" do
51+
Setting.set_config('saml', {
52+
:entity_id => nil
53+
})
54+
55+
SetSamlEntityId.up
56+
57+
@aac.reload
58+
@aac.entity_id.should == "http://bob.cody.instructure.com/saml2"
59+
end
60+
61+
62+
end

spec/models/account_authorization_config_spec.rb

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@
3535
end
3636

3737
context "SAML settings" do
38+
before(:each) do
39+
@account = Account.create!(:name => "account")
40+
end
41+
3842
it "should load encryption settings" do
3943
file_that_exists = File.expand_path(__FILE__)
4044
Setting.set_config('saml', {
@@ -48,12 +52,22 @@
4852
}
4953
})
5054

51-
@account = Account.new
5255
config = @account.account_authorization_configs.build(:auth_type => 'saml')
5356

5457
s = config.saml_settings
5558
s.encryption_configured?.should be_true
5659
end
60+
61+
it "should set the entity_id with the current domain" do
62+
HostUrl.stubs(:default_host).returns('bob.cody.instructure.com')
63+
@aac = @account.account_authorization_configs.create!(:auth_type => "saml")
64+
@aac.entity_id.should == "http://bob.cody.instructure.com/saml2"
65+
end
66+
67+
it "should not overwrite a specific entity_id" do
68+
@aac = @account.account_authorization_configs.create!(:auth_type => "saml", :entity_id => "http://wtb.instructure.com/saml2")
69+
@aac.entity_id.should == "http://wtb.instructure.com/saml2"
70+
end
5771
end
5872

5973
context "password" do

0 commit comments

Comments
 (0)