FEATURE: allow multiple secrets for Discourse SSO provider

This splits off the logic between SSO keys used incoming vs outgoing, it allows to far better restrict who is allowed to log in using a site.

This allows for better auditing of the SSO provider feature
This commit is contained in:
Maja Komel 2018-10-15 07:03:53 +02:00 committed by Sam
parent 6acdea37c4
commit 27e732a58d
15 changed files with 459 additions and 230 deletions

View File

@ -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 [];
}
}
});

View File

@ -11,7 +11,8 @@ const CUSTOM_TYPES = [
"value_list",
"category",
"uploaded_image_list",
"compact_list"
"compact_list",
"secret_list"
];
export default Ember.Mixin.create({

View File

@ -0,0 +1,22 @@
{{#if collection}}
<div class="values">
{{#each collection as |value index|}}
<div class="value" data-index={{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"}}
</div>
{{/each}}
</div>
{{/if}}
<div class="value">
{{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"}}
</div>

View File

@ -0,0 +1,3 @@
{{secret-value-list setting=setting values=value}}
{{setting-validation-message message=validationMessage}}
<div class='desc'>{{{unbound setting.description}}}</div>

View File

@ -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"] {

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
);
}
});