diff --git a/app/assets/javascripts/admin/components/secret-value-list.js.es6 b/app/assets/javascripts/admin/components/secret-value-list.js.es6
new file mode 100644
index 00000000000..939ba45c9e7
--- /dev/null
+++ b/app/assets/javascripts/admin/components/secret-value-list.js.es6
@@ -0,0 +1,87 @@
+import { on } from "ember-addons/ember-computed-decorators";
+
+export default Ember.Component.extend({
+ classNameBindings: [":value-list", ":secret-value-list"],
+ inputInvalidKey: Ember.computed.empty("newKey"),
+ inputInvalidSecret: Ember.computed.empty("newSecret"),
+ inputDelimiter: null,
+ collection: null,
+ values: null,
+
+ @on("didReceiveAttrs")
+ _setupCollection() {
+ const values = this.get("values");
+
+ this.set(
+ "collection",
+ this._splitValues(values, this.get("inputDelimiter") || "\n")
+ );
+ },
+
+ actions: {
+ changeKey(index, newValue) {
+ this._replaceValue(index, newValue, "key");
+ },
+
+ changeSecret(index, newValue) {
+ this._replaceValue(index, newValue, "secret");
+ },
+
+ addValue() {
+ if (this.get("inputInvalidKey") || this.get("inputInvalidSecret")) return;
+ this._addValue(this.get("newKey"), this.get("newSecret"));
+ this.setProperties({ newKey: "", newSecret: "" });
+ },
+
+ removeValue(value) {
+ this._removeValue(value);
+ }
+ },
+
+ _addValue(value, secret) {
+ this.get("collection").addObject({ key: value, secret: secret });
+ this._saveValues();
+ },
+
+ _removeValue(value) {
+ const collection = this.get("collection");
+ collection.removeObject(value);
+ this._saveValues();
+ },
+
+ _replaceValue(index, newValue, keyName) {
+ let item = this.get("collection")[index];
+ Ember.set(item, keyName, newValue);
+
+ this._saveValues();
+ },
+
+ _saveValues() {
+ this.set(
+ "values",
+ this.get("collection")
+ .map(function(elem) {
+ return `${elem.key}|${elem.secret}`;
+ })
+ .join("\n")
+ );
+ },
+
+ _splitValues(values, delimiter) {
+ if (values && values.length) {
+ const keys = ["key", "secret"];
+ var res = [];
+ values.split(delimiter).forEach(function(str) {
+ var object = {};
+ str.split("|").forEach(function(a, i) {
+ object[keys[i]] = a;
+ });
+ res.push(object);
+ });
+
+ return res;
+ } else {
+ return [];
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/mixins/setting-component.js.es6 b/app/assets/javascripts/admin/mixins/setting-component.js.es6
index 532abe8f20c..9408bfbceba 100644
--- a/app/assets/javascripts/admin/mixins/setting-component.js.es6
+++ b/app/assets/javascripts/admin/mixins/setting-component.js.es6
@@ -11,7 +11,8 @@ const CUSTOM_TYPES = [
"value_list",
"category",
"uploaded_image_list",
- "compact_list"
+ "compact_list",
+ "secret_list"
];
export default Ember.Mixin.create({
diff --git a/app/assets/javascripts/admin/templates/components/secret-value-list.hbs b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs
new file mode 100644
index 00000000000..7058504facc
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/secret-value-list.hbs
@@ -0,0 +1,22 @@
+{{#if collection}}
+
+ {{#each collection as |value index|}}
+
+ {{d-button action="removeValue"
+ actionParam=value
+ icon="times"
+ class="remove-value-btn btn-small"}}
+ {{input value=value.key class="value-input" focus-out=(action "changeKey" index)}}
+ {{input value=value.secret class="value-input" focus-out=(action "changeSecret" index) type="password"}}
+
+ {{/each}}
+
+{{/if}}
+
+
+ {{text-field value=newKey class="new-value-input key" placeholder=setting.placeholder.key}}
+ {{input type="password" value=newSecret class="new-value-input secret" placeholder=setting.placeholder.value}}
+ {{d-button action="addValue"
+ icon="plus"
+ class="add-value-btn btn-small"}}
+
diff --git a/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs
new file mode 100644
index 00000000000..1e71c18c638
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/site-settings/secret-list.hbs
@@ -0,0 +1,3 @@
+{{secret-value-list setting=setting values=value}}
+{{setting-validation-message message=validationMessage}}
+{{{unbound setting.description}}}
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss
index 69269d1f12c..24721a26be6 100644
--- a/app/assets/stylesheets/common/admin/admin_base.scss
+++ b/app/assets/stylesheets/common/admin/admin_base.scss
@@ -865,6 +865,17 @@ table#user-badges {
}
}
+@mixin value-btn {
+ width: 29px;
+ border: 1px solid $primary-low;
+ outline: none;
+ padding: 0;
+
+ &:focus {
+ border-color: $tertiary;
+ }
+}
+
.value-list {
.value {
padding: 0.125em 0;
@@ -891,15 +902,8 @@ table#user-badges {
}
.remove-value-btn {
+ @include value-btn;
margin-right: 0.25em;
- width: 29px;
- border: 1px solid $primary-low;
- outline: none;
- padding: 0;
-
- &:focus {
- border-color: $tertiary;
- }
}
}
.values {
@@ -907,6 +911,40 @@ table#user-badges {
}
}
+.secret-value-list {
+ .value {
+ flex-flow: row wrap;
+ margin-left: -0.25em;
+ margin-top: -0.125em;
+ .new-value-input {
+ flex: 1;
+ }
+ .value-input,
+ .new-value-input {
+ margin-top: 0.125em;
+ &:last-of-type {
+ margin-left: 0.25em;
+ }
+ }
+ .remove-value-btn {
+ margin-left: 0.25em;
+ margin-top: 0.125em;
+ }
+ .add-value-btn {
+ @include value-btn;
+ margin-left: 0.25em;
+ margin-top: 0.125em;
+ }
+ &:last-of-type {
+ .new-value-input {
+ &:first-of-type {
+ margin-left: 0.25em;
+ }
+ }
+ }
+ }
+}
+
// Mobile view text-inputs need some padding
.mobile-view .admin-contents {
input[type="text"] {
diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb
index b523943e8d9..af1e761a65f 100644
--- a/app/controllers/session_controller.rb
+++ b/app/controllers/session_controller.rb
@@ -46,7 +46,7 @@ class SessionController < ApplicationController
payload ||= request.query_string
if SiteSetting.enable_sso_provider
- sso = SingleSignOn.parse(payload, SiteSetting.sso_secret)
+ sso = SingleSignOn.parse(payload)
if sso.return_sso_url.blank?
render plain: "return_sso_url is blank, it must be provided", status: 400
diff --git a/app/services/wildcard_domain_checker.rb b/app/services/wildcard_domain_checker.rb
new file mode 100644
index 00000000000..3f91811f07e
--- /dev/null
+++ b/app/services/wildcard_domain_checker.rb
@@ -0,0 +1,10 @@
+module WildcardDomainChecker
+
+ def self.check_domain(domain, external_domain)
+ escaped_domain = domain[0] == "*" ? Regexp.escape(domain).sub("\\*", '\S*') : Regexp.escape(domain)
+ domain_regex = Regexp.new("^#{escaped_domain}$", 'i')
+
+ external_domain.match(domain_regex)
+ end
+
+end
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 6a5e45265a3..2606cee026f 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1319,6 +1319,7 @@ en:
enable_sso_provider: "Implement Discourse SSO provider protocol at the /session/sso_provider endpoint, requires sso_secret to be set"
sso_url: "URL of single sign on endpoint (must include http:// or https://)"
sso_secret: "Secret string used to cryptographically authenticate SSO information, be sure it is 10 characters or longer"
+ sso_provider_secrets: "A list of domain-secret pairs that are using Discourse as a SSO provider. Make sure SSO secret is 10 characters or longer. Wildcard symbol * can be used to match any domain or only a part of it (e.g. *.example.com)."
sso_overrides_bio: "Overrides user bio in user profile and prevents user from changing it"
sso_overrides_groups: "Synchronize all manual group membership with groups specified in the groups sso attribute (WARNING: if you do not specify groups all manual group membership will be cleared for user)"
sso_overrides_email: "Overrides local email with external site email from SSO payload on every login, and prevent local changes. (WARNING: discrepancies can occur due to normalization of local emails)"
@@ -1862,6 +1863,11 @@ en:
max_username_length_exists: "You cannot set the maximum username length below the longest username (%{username})."
max_username_length_range: "You cannot set the maximum below the minimum."
+ placeholder:
+ sso_provider_secrets:
+ key: "www.example.com"
+ value: "SSO secret"
+
search:
within_post: "#%{post_number} by %{username}"
types:
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 1a882a9c1ef..c98dc0610d8 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -341,6 +341,13 @@ login:
sso_secret:
default: ''
secret: true
+ sso_provider_secrets:
+ default: ''
+ type: list
+ list_type: secret
+ placeholder:
+ key: "sso_provider.key_placeholder"
+ value: "sso_provider.value_placeholder"
sso_overrides_groups: false
sso_overrides_bio: false
sso_overrides_email:
diff --git a/db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb b/db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb
new file mode 100644
index 00000000000..4e95023b430
--- /dev/null
+++ b/db/migrate/20181005084357_add_sso_provider_secrets_to_site_settings.rb
@@ -0,0 +1,12 @@
+class AddSsoProviderSecretsToSiteSettings < ActiveRecord::Migration[5.2]
+ def up
+ return unless SiteSetting.enable_sso_provider && SiteSetting.sso_secret.present?
+ sso_secret = SiteSetting.sso_secret
+ execute "INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
+ VALUES ('sso_provider_secrets', 8, '*|#{sso_secret}', now(), now())"
+ end
+
+ def down
+ execute "DELETE FROM site_settings WHERE name = 'sso_provider_secrets'"
+ end
+end
diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb
index 6900b633b2b..f5a59e26a63 100644
--- a/lib/single_sign_on.rb
+++ b/lib/single_sign_on.rb
@@ -50,9 +50,14 @@ class SingleSignOn
def self.parse(payload, sso_secret = nil)
sso = new
- sso.sso_secret = sso_secret if sso_secret
parsed = Rack::Utils.parse_query(payload)
+ decoded = Base64.decode64(parsed["sso"])
+ decoded_hash = Rack::Utils.parse_query(decoded)
+
+ return_sso_url = decoded_hash['return_sso_url']
+ sso.sso_secret = sso_secret || (provider_secret(return_sso_url) if return_sso_url)
+
if sso.sign(parsed["sso"]) != parsed["sig"]
diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}"
if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m
@@ -83,6 +88,17 @@ class SingleSignOn
sso
end
+ def self.provider_secret(return_sso_url)
+ provider_secrets = SiteSetting.sso_provider_secrets.split(/[\|,\n]/)
+ provider_secrets_hash = Hash[*provider_secrets]
+ return_url_host = URI.parse(return_sso_url).host
+
+ secret = provider_secrets_hash.select do |domain, _|
+ WildcardDomainChecker.check_domain(domain, return_url_host)
+ end
+ secret.present? ? secret.values.first : nil
+ end
+
def diagnostics
SingleSignOn::ACCESSORS.map { |a| "#{a}: #{send(a)}" }.join("\n")
end
@@ -99,8 +115,9 @@ class SingleSignOn
@custom_fields ||= {}
end
- def sign(payload)
- OpenSSL::HMAC.hexdigest("sha256", sso_secret, payload)
+ def sign(payload, provider_secret = nil)
+ secret = provider_secret || sso_secret
+ OpenSSL::HMAC.hexdigest("sha256", secret, payload)
end
def to_url(base_url = nil)
@@ -108,9 +125,9 @@ class SingleSignOn
"#{base}#{base.include?('?') ? '&' : '?'}#{payload}"
end
- def payload
+ def payload(provider_secret = nil)
payload = Base64.strict_encode64(unsigned_payload)
- "sso=#{CGI::escape(payload)}&sig=#{sign(payload)}"
+ "sso=#{CGI::escape(payload)}&sig=#{sign(payload, provider_secret)}"
end
def unsigned_payload
diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index 490cc26b6d5..9122da94d91 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -220,7 +220,8 @@ module SiteSettingExtension
value: value.to_s,
category: categories[s],
preview: previews[s],
- secret: secret_settings.include?(s)
+ secret: secret_settings.include?(s),
+ placeholder: placeholder(s)
}.merge(type_supervisor.type_hash(s))
opts
@@ -231,6 +232,12 @@ module SiteSettingExtension
I18n.t("site_settings.#{setting}")
end
+ def placeholder(setting)
+ if !I18n.t("site_settings.placeholder.#{setting}", default: "").empty?
+ I18n.t("site_settings.placeholder.#{setting}")
+ end
+ end
+
def self.client_settings_cache_key
# NOTE: we use the git version in the key to ensure
# that we don't end up caching the incorrect version
diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb
index 8a540829d03..2f8b8a4bf75 100644
--- a/spec/requests/session_controller_spec.rb
+++ b/spec/requests/session_controller_spec.rb
@@ -521,153 +521,6 @@ RSpec.describe SessionController do
expect(response.status).to eq(419)
end
- describe 'can act as an SSO provider' do
- before do
- stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return(
- status: 200,
- body: lambda { |request| file_from_fixtures("logo.png") }
- )
-
- SiteSetting.enable_sso_provider = true
- SiteSetting.enable_sso = false
- SiteSetting.enable_local_logins = true
- SiteSetting.sso_secret = "topsecret"
-
- @sso = SingleSignOn.new
- @sso.nonce = "mynonce"
- @sso.sso_secret = SiteSetting.sso_secret
- @sso.return_sso_url = "http://somewhere.over.rainbow/sso"
-
- @user = Fabricate(:user, password: "myfrogs123ADMIN", active: true, admin: true)
- group = Fabricate(:group)
- group.add(@user)
-
- @user.create_user_avatar!
- UserAvatar.import_url_for_user(logo_fixture, @user)
- UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: false)
- UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: true)
-
- @user.reload
- @user.user_avatar.reload
- @user.user_profile.reload
- EmailToken.update_all(confirmed: true)
- end
-
- it "successfully logs in and redirects user to return_sso_url when the user is not logged in" do
- get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload), headers: headers
-
- expect(response).to redirect_to("/login")
-
- post "/session.json",
- params: { login: @user.username, password: "myfrogs123ADMIN" }, xhr: true
- location = response.cookies["sso_destination_url"]
- # javascript code will handle redirection of user to return_sso_url
- expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
-
- payload = location.split("?")[1]
- sso2 = SingleSignOn.parse(payload, "topsecret")
-
- expect(sso2.email).to eq(@user.email)
- expect(sso2.name).to eq(@user.name)
- expect(sso2.username).to eq(@user.username)
- expect(sso2.external_id).to eq(@user.id.to_s)
- expect(sso2.admin).to eq(true)
- expect(sso2.moderator).to eq(false)
- expect(sso2.groups).to eq(@user.groups.pluck(:name).join(","))
-
- expect(sso2.avatar_url.blank?).to_not eq(true)
- expect(sso2.profile_background_url.blank?).to_not eq(true)
- expect(sso2.card_background_url.blank?).to_not eq(true)
-
- expect(sso2.avatar_url).to start_with(Discourse.base_url)
- expect(sso2.profile_background_url).to start_with(Discourse.base_url)
- expect(sso2.card_background_url).to start_with(Discourse.base_url)
- end
-
- it "successfully redirects user to return_sso_url when the user is logged in" do
- sign_in(@user)
-
- get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload), headers: headers
-
- location = response.header["Location"]
- expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
-
- payload = location.split("?")[1]
- sso2 = SingleSignOn.parse(payload, "topsecret")
-
- expect(sso2.email).to eq(@user.email)
- expect(sso2.name).to eq(@user.name)
- expect(sso2.username).to eq(@user.username)
- expect(sso2.external_id).to eq(@user.id.to_s)
- expect(sso2.admin).to eq(true)
- expect(sso2.moderator).to eq(false)
- expect(sso2.groups).to eq(@user.groups.pluck(:name).join(","))
-
- expect(sso2.avatar_url.blank?).to_not eq(true)
- expect(sso2.profile_background_url.blank?).to_not eq(true)
- expect(sso2.card_background_url.blank?).to_not eq(true)
-
- expect(sso2.avatar_url).to start_with(Discourse.base_url)
- expect(sso2.profile_background_url).to start_with(Discourse.base_url)
- expect(sso2.card_background_url).to start_with(Discourse.base_url)
- end
-
- it 'handles non local content correctly' do
- SiteSetting.avatar_sizes = "100|49"
- SiteSetting.enable_s3_uploads = true
- SiteSetting.s3_access_key_id = "XXX"
- SiteSetting.s3_secret_access_key = "XXX"
- SiteSetting.s3_upload_bucket = "test"
- SiteSetting.s3_cdn_url = "http://cdn.com"
-
- stub_request(:any, /test.s3.dualstack.us-east-1.amazonaws.com/).to_return(status: 200, body: "", headers: {})
-
- @user.create_user_avatar!
- upload = Fabricate(:upload, url: "//test.s3.dualstack.us-east-1.amazonaws.com/something")
-
- Fabricate(:optimized_image,
- sha1: SecureRandom.hex << "A" * 8,
- upload: upload,
- width: 98,
- height: 98,
- url: "//test.s3.amazonaws.com/something/else"
- )
-
- @user.update_columns(uploaded_avatar_id: upload.id)
- @user.user_profile.update_columns(
- profile_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something",
- card_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something"
- )
-
- @user.reload
- @user.user_avatar.reload
- @user.user_profile.reload
-
- sign_in(@user)
-
- stub_request(:get, "http://cdn.com/something/else").to_return(
- body: lambda { |request| File.new(Rails.root + 'spec/fixtures/images/logo.png') }
- )
-
- get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload), headers: headers
-
- location = response.header["Location"]
- # javascript code will handle redirection of user to return_sso_url
- expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
-
- payload = location.split("?")[1]
- sso2 = SingleSignOn.parse(payload, "topsecret")
-
- expect(sso2.avatar_url.blank?).to_not eq(true)
- expect(sso2.profile_background_url.blank?).to_not eq(true)
- expect(sso2.card_background_url.blank?).to_not eq(true)
-
- expect(sso2.avatar_url).to start_with("#{SiteSetting.s3_cdn_url}/original")
- expect(sso2.profile_background_url).to start_with(SiteSetting.s3_cdn_url)
- expect(sso2.card_background_url).to start_with(SiteSetting.s3_cdn_url)
- end
- end
-
describe 'local attribute override from SSO payload' do
before do
SiteSetting.email_editable = false
@@ -724,91 +577,159 @@ RSpec.describe SessionController do
end
describe '#sso_provider' do
- before do
- stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return(
- status: 200,
- body: lambda { |request| file_from_fixtures("logo.png") }
- )
+ let(:headers) { { host: Discourse.current_hostname } }
- SiteSetting.enable_sso_provider = true
- SiteSetting.enable_sso = false
- SiteSetting.enable_local_logins = true
- SiteSetting.sso_secret = "topsecret"
+ describe 'can act as an SSO provider' do
+ before do
+ stub_request(:any, /#{Discourse.current_hostname}\/uploads/).to_return(
+ status: 200,
+ body: lambda { |request| file_from_fixtures("logo.png") }
+ )
- @sso = SingleSignOn.new
- @sso.nonce = "mynonce"
- @sso.sso_secret = SiteSetting.sso_secret
- @sso.return_sso_url = "http://somewhere.over.rainbow/sso"
+ SiteSetting.enable_sso_provider = true
+ SiteSetting.enable_sso = false
+ SiteSetting.enable_local_logins = true
+ SiteSetting.sso_provider_secrets = "www.random.site|secretForRandomSite\nsomewhere.over.rainbow|secretForOverRainbow"
- @user = Fabricate(:user, password: "myfrogs123ADMIN", active: true, admin: true)
- @user.create_user_avatar!
- UserAvatar.import_url_for_user(logo_fixture, @user)
- UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: false)
- UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: true)
+ @sso = SingleSignOn.new
+ @sso.nonce = "mynonce"
+ @sso.return_sso_url = "http://somewhere.over.rainbow/sso"
- @user.reload
- @user.user_avatar.reload
- @user.user_profile.reload
- EmailToken.update_all(confirmed: true)
- end
+ @user = Fabricate(:user, password: "myfrogs123ADMIN", active: true, admin: true)
+ group = Fabricate(:group)
+ group.add(@user)
- it "successfully logs in and redirects user to return_sso_url when the user is not logged in" do
- get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload)
- expect(response).to redirect_to("/login")
+ @user.create_user_avatar!
+ UserAvatar.import_url_for_user(logo_fixture, @user)
+ UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: false)
+ UserProfile.import_url_for_user(logo_fixture, @user, is_card_background: true)
- post "/session.json",
- params: { login: @user.username, password: "myfrogs123ADMIN" },
- xhr: true
+ @user.reload
+ @user.user_avatar.reload
+ @user.user_profile.reload
+ EmailToken.update_all(confirmed: true)
+ end
- location = response.cookies["sso_destination_url"]
- # javascript code will handle redirection of user to return_sso_url
- expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
+ it "successfully logs in and redirects user to return_sso_url when the user is not logged in" do
+ get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForOverRainbow"))
- payload = location.split("?")[1]
- sso2 = SingleSignOn.parse(payload, "topsecret")
+ expect(response).to redirect_to("/login")
- expect(sso2.email).to eq(@user.email)
- expect(sso2.name).to eq(@user.name)
- expect(sso2.username).to eq(@user.username)
- expect(sso2.external_id).to eq(@user.id.to_s)
- expect(sso2.admin).to eq(true)
- expect(sso2.moderator).to eq(false)
- expect(sso2.groups).to eq(@user.groups.pluck(:name).join(","))
+ post "/session.json",
+ params: { login: @user.username, password: "myfrogs123ADMIN" }, xhr: true, headers: headers
- expect(sso2.avatar_url.blank?).to_not eq(true)
- expect(sso2.profile_background_url.blank?).to_not eq(true)
- expect(sso2.card_background_url.blank?).to_not eq(true)
+ location = response.cookies["sso_destination_url"]
+ # javascript code will handle redirection of user to return_sso_url
+ expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
- expect(sso2.avatar_url).to start_with(Discourse.base_url)
- expect(sso2.profile_background_url).to start_with(Discourse.base_url)
- expect(sso2.card_background_url).to start_with(Discourse.base_url)
- end
+ payload = location.split("?")[1]
+ sso2 = SingleSignOn.parse(payload)
- it "successfully redirects user to return_sso_url when the user is logged in" do
- sign_in(@user)
+ expect(sso2.email).to eq(@user.email)
+ expect(sso2.name).to eq(@user.name)
+ expect(sso2.username).to eq(@user.username)
+ expect(sso2.external_id).to eq(@user.id.to_s)
+ expect(sso2.admin).to eq(true)
+ expect(sso2.moderator).to eq(false)
+ expect(sso2.groups).to eq(@user.groups.pluck(:name).join(","))
- get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload)
+ expect(sso2.avatar_url.blank?).to_not eq(true)
+ expect(sso2.profile_background_url.blank?).to_not eq(true)
+ expect(sso2.card_background_url.blank?).to_not eq(true)
- location = response.header["Location"]
- expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
+ expect(sso2.avatar_url).to start_with(Discourse.base_url)
+ expect(sso2.profile_background_url).to start_with(Discourse.base_url)
+ expect(sso2.card_background_url).to start_with(Discourse.base_url)
+ end
- payload = location.split("?")[1]
- sso2 = SingleSignOn.parse(payload, "topsecret")
+ it "it fails to log in if secret is wrong" do
+ get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForRandomSite"))
- expect(sso2.email).to eq(@user.email)
- expect(sso2.name).to eq(@user.name)
- expect(sso2.username).to eq(@user.username)
- expect(sso2.external_id).to eq(@user.id.to_s)
- expect(sso2.admin).to eq(true)
- expect(sso2.moderator).to eq(false)
+ expect(response.status).to eq(500)
+ end
- expect(sso2.avatar_url.blank?).to_not eq(true)
- expect(sso2.profile_background_url.blank?).to_not eq(true)
- expect(sso2.card_background_url.blank?).to_not eq(true)
+ it "successfully redirects user to return_sso_url when the user is logged in" do
+ sign_in(@user)
- expect(sso2.avatar_url).to start_with("#{Discourse.store.absolute_base_url}/original")
- expect(sso2.profile_background_url).to start_with(Discourse.base_url)
- expect(sso2.card_background_url).to start_with(Discourse.base_url)
+ get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForOverRainbow"))
+
+ location = response.header["Location"]
+ expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
+
+ payload = location.split("?")[1]
+ sso2 = SingleSignOn.parse(payload)
+
+ expect(sso2.email).to eq(@user.email)
+ expect(sso2.name).to eq(@user.name)
+ expect(sso2.username).to eq(@user.username)
+ expect(sso2.external_id).to eq(@user.id.to_s)
+ expect(sso2.admin).to eq(true)
+ expect(sso2.moderator).to eq(false)
+ expect(sso2.groups).to eq(@user.groups.pluck(:name).join(","))
+
+ expect(sso2.avatar_url.blank?).to_not eq(true)
+ expect(sso2.profile_background_url.blank?).to_not eq(true)
+ expect(sso2.card_background_url.blank?).to_not eq(true)
+
+ expect(sso2.avatar_url).to start_with(Discourse.base_url)
+ expect(sso2.profile_background_url).to start_with(Discourse.base_url)
+ expect(sso2.card_background_url).to start_with(Discourse.base_url)
+ end
+
+ it 'handles non local content correctly' do
+ SiteSetting.avatar_sizes = "100|49"
+ SiteSetting.enable_s3_uploads = true
+ SiteSetting.s3_access_key_id = "XXX"
+ SiteSetting.s3_secret_access_key = "XXX"
+ SiteSetting.s3_upload_bucket = "test"
+ SiteSetting.s3_cdn_url = "http://cdn.com"
+
+ stub_request(:any, /test.s3.dualstack.us-east-1.amazonaws.com/).to_return(status: 200, body: "", headers: { referer: "fgdfds" })
+
+ @user.create_user_avatar!
+ upload = Fabricate(:upload, url: "//test.s3.dualstack.us-east-1.amazonaws.com/something")
+
+ Fabricate(:optimized_image,
+ sha1: SecureRandom.hex << "A" * 8,
+ upload: upload,
+ width: 98,
+ height: 98,
+ url: "//test.s3.amazonaws.com/something/else"
+ )
+
+ @user.update_columns(uploaded_avatar_id: upload.id)
+ @user.user_profile.update_columns(
+ profile_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something",
+ card_background: "//test.s3.dualstack.us-east-1.amazonaws.com/something"
+ )
+
+ @user.reload
+ @user.user_avatar.reload
+ @user.user_profile.reload
+
+ sign_in(@user)
+
+ stub_request(:get, "http://cdn.com/something/else").to_return(
+ body: lambda { |request| File.new(Rails.root + 'spec/fixtures/images/logo.png') }
+ )
+
+ get "/session/sso_provider", params: Rack::Utils.parse_query(@sso.payload("secretForOverRainbow"))
+
+ location = response.header["Location"]
+ # javascript code will handle redirection of user to return_sso_url
+ expect(location).to match(/^http:\/\/somewhere.over.rainbow\/sso/)
+
+ payload = location.split("?")[1]
+ sso2 = SingleSignOn.parse(payload)
+
+ expect(sso2.avatar_url.blank?).to_not eq(true)
+ expect(sso2.profile_background_url.blank?).to_not eq(true)
+ expect(sso2.card_background_url.blank?).to_not eq(true)
+
+ expect(sso2.avatar_url).to start_with("#{SiteSetting.s3_cdn_url}/original")
+ expect(sso2.profile_background_url).to start_with(SiteSetting.s3_cdn_url)
+ expect(sso2.card_background_url).to start_with(SiteSetting.s3_cdn_url)
+ end
end
end
diff --git a/spec/services/wildcard_domain_checker_spec.rb b/spec/services/wildcard_domain_checker_spec.rb
new file mode 100644
index 00000000000..806ca99246c
--- /dev/null
+++ b/spec/services/wildcard_domain_checker_spec.rb
@@ -0,0 +1,35 @@
+require 'rails_helper'
+
+describe WildcardDomainChecker do
+
+ describe 'check_domain' do
+ context 'valid domain' do
+ it 'returns correct domain' do
+ result1 = WildcardDomainChecker.check_domain('*.discourse.org', 'anything.is.possible.discourse.org')
+ expect(result1[0]).to eq('anything.is.possible.discourse.org')
+
+ result2 = WildcardDomainChecker.check_domain('www.discourse.org', 'www.discourse.org')
+ expect(result2[0]).to eq('www.discourse.org')
+
+ result3 = WildcardDomainChecker.check_domain('*', 'hello.discourse.org')
+ expect(result3[0]).to eq('hello.discourse.org')
+ end
+ end
+
+ context 'invalid domain' do
+ it "doesn't return the domain" do
+ result1 = WildcardDomainChecker.check_domain('*.discourse.org', 'bad-domain.discourse.org.evil.com')
+ expect(result1).to eq(nil)
+
+ result2 = WildcardDomainChecker.check_domain('www.discourse.org', 'www.discourse.org.evil.com')
+ expect(result2).to eq(nil)
+
+ result3 = WildcardDomainChecker.check_domain('www.discourse.org', 'www.www.discourse.org')
+ expect(result3).to eq(nil)
+
+ result4 = WildcardDomainChecker.check_domain('www.*.discourse.org', 'www.www.discourse.org')
+ expect(result4).to eq(nil)
+ end
+ end
+ end
+end
diff --git a/test/javascripts/components/secret-value-list-test.js.es6 b/test/javascripts/components/secret-value-list-test.js.es6
new file mode 100644
index 00000000000..1f602cbdbde
--- /dev/null
+++ b/test/javascripts/components/secret-value-list-test.js.es6
@@ -0,0 +1,63 @@
+import componentTest from "helpers/component-test";
+moduleForComponent("secret-value-list", { integration: true });
+
+componentTest("adding a value", {
+ template: "{{secret-value-list values=values}}",
+
+ async test(assert) {
+ this.set("values", "firstKey|FirstValue\nsecondKey|secondValue");
+
+ await fillIn(".new-value-input.key", "thirdKey");
+ await click(".add-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 2,
+ "it doesn't add the value to the list if secret is missing"
+ );
+
+ await fillIn(".new-value-input.key", "");
+ await fillIn(".new-value-input.secret", "thirdValue");
+ await click(".add-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 2,
+ "it doesn't add the value to the list if key is missing"
+ );
+
+ await fillIn(".new-value-input.key", "thirdKey");
+ await fillIn(".new-value-input.secret", "thirdValue");
+ await click(".add-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 3,
+ "it adds the value to the list of values"
+ );
+
+ assert.deepEqual(
+ this.get("values"),
+ "firstKey|FirstValue\nsecondKey|secondValue\nthirdKey|thirdValue",
+ "it adds the value to the list of values"
+ );
+ }
+});
+
+componentTest("removing a value", {
+ template: "{{secret-value-list values=values}}",
+
+ async test(assert) {
+ this.set("values", "firstKey|FirstValue\nsecondKey|secondValue");
+
+ await click(".values .value[data-index='0'] .remove-value-btn");
+
+ assert.ok(
+ find(".values .value").length === 1,
+ "it removes the value from the list of values"
+ );
+
+ assert.equal(
+ this.get("values"),
+ "secondKey|secondValue",
+ "it removes the expected value"
+ );
+ }
+});