mirror of
https://github.com/discourse/discourse.git
synced 2024-12-03 16:03:41 +08:00
df3886d6e5
This commit introduces a new site setting "google_oauth2_hd_groups". If enabled, group information will be fetched from Google during authentication, and stored in the Discourse database. These 'associated groups' can be connected to a Discourse group via the "Membership" tab of the group preferences UI. The majority of the implementation is generic, so we will be able to add support to more authentication methods in the near future. https://meta.discourse.org/t/managing-group-membership-via-authentication/175950
332 lines
13 KiB
Ruby
332 lines
13 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'rails_helper'
|
|
|
|
describe Auth::ManagedAuthenticator do
|
|
let(:authenticator) {
|
|
Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
end.new
|
|
}
|
|
|
|
let(:hash) {
|
|
OmniAuth::AuthHash.new(
|
|
provider: "myauth",
|
|
uid: "1234",
|
|
info: {
|
|
name: "Best Display Name",
|
|
email: "awesome@example.com",
|
|
nickname: "IAmGroot"
|
|
},
|
|
credentials: {
|
|
token: "supersecrettoken"
|
|
},
|
|
extra: {
|
|
raw_info: {
|
|
randominfo: "some info"
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
let(:create_hash) {
|
|
OmniAuth::AuthHash.new(
|
|
provider: "myauth",
|
|
uid: "1234"
|
|
)
|
|
}
|
|
|
|
def create_auth_result(attrs)
|
|
auth_result = Auth::Result.new
|
|
attrs.each { |k, v| auth_result.send("#{k}=", v) }
|
|
auth_result
|
|
end
|
|
|
|
describe 'after_authenticate' do
|
|
it 'can match account from an existing association' do
|
|
user = Fabricate(:user)
|
|
associated = UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", last_used: 1.year.ago)
|
|
result = authenticator.after_authenticate(hash)
|
|
|
|
expect(result.user.id).to eq(user.id)
|
|
associated.reload
|
|
expect(associated.last_used).to be >= 1.day.ago
|
|
expect(associated.info["name"]).to eq("Best Display Name")
|
|
expect(associated.info["email"]).to eq("awesome@example.com")
|
|
expect(associated.credentials["token"]).to eq("supersecrettoken")
|
|
expect(associated.extra["raw_info"]["randominfo"]).to eq("some info")
|
|
end
|
|
|
|
it 'only sets email valid for present strings' do
|
|
# (Twitter sometimes sends empty email strings)
|
|
result = authenticator.after_authenticate(create_hash.merge(info: { email: "email@example.com" }))
|
|
expect(result.email_valid).to eq(true)
|
|
|
|
result = authenticator.after_authenticate(create_hash.merge(info: { email: "" }))
|
|
expect(result.email_valid).to be_falsey
|
|
|
|
result = authenticator.after_authenticate(create_hash.merge(info: { email: nil }))
|
|
expect(result.email_valid).to be_falsey
|
|
end
|
|
|
|
describe 'connecting to another user account' do
|
|
fab!(:user1) { Fabricate(:user) }
|
|
fab!(:user2) { Fabricate(:user) }
|
|
before { UserAssociatedAccount.create!(user: user1, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it 'works by default' do
|
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
|
expect(result.user.id).to eq(user2.id)
|
|
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false)
|
|
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true)
|
|
end
|
|
|
|
it 'still works if another user has a matching email' do
|
|
Fabricate(:user, email: hash.dig(:info, :email))
|
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
|
expect(result.user.id).to eq(user2.id)
|
|
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false)
|
|
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true)
|
|
end
|
|
|
|
it 'does not work when disabled' do
|
|
authenticator = Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
def can_connect_existing_user?
|
|
false
|
|
end
|
|
end.new
|
|
result = authenticator.after_authenticate(hash, existing_account: user2)
|
|
expect(result.user.id).to eq(user1.id)
|
|
expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(true)
|
|
expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe 'match by email' do
|
|
it 'downcases the email address from the authprovider' do
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { email: "HELLO@example.com" }))
|
|
expect(result.email).to eq('hello@example.com')
|
|
end
|
|
|
|
it 'works normally' do
|
|
user = Fabricate(:user)
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { email: user.email }))
|
|
expect(result.user.id).to eq(user.id)
|
|
expect(UserAssociatedAccount.find_by(provider_name: 'myauth', provider_uid: "1234").user_id).to eq(user.id)
|
|
end
|
|
|
|
it 'works if there is already an association with the target account' do
|
|
user = Fabricate(:user, email: "awesome@example.com")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user.id).to eq(user.id)
|
|
end
|
|
|
|
it 'does not match if match_by_email is false' do
|
|
authenticator = Class.new(described_class) do
|
|
def name
|
|
"myauth"
|
|
end
|
|
def match_by_email
|
|
false
|
|
end
|
|
end.new
|
|
user = Fabricate(:user, email: "awesome@example.com")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
end
|
|
end
|
|
|
|
context 'when no matching user' do
|
|
it 'returns the correct information' do
|
|
expect {
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user).to eq(nil)
|
|
expect(result.username).to eq("IAmGroot")
|
|
expect(result.email).to eq("awesome@example.com")
|
|
}.to change { UserAssociatedAccount.count }.by(1)
|
|
expect(UserAssociatedAccount.last.user).to eq(nil)
|
|
expect(UserAssociatedAccount.last.info["nickname"]).to eq("IAmGroot")
|
|
end
|
|
|
|
it 'works if there is already an association with the target account' do
|
|
user = Fabricate(:user, email: "awesome@example.com")
|
|
result = authenticator.after_authenticate(hash)
|
|
expect(result.user.id).to eq(user.id)
|
|
end
|
|
|
|
it 'works if there is no email' do
|
|
expect {
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { email: nil }))
|
|
expect(result.user).to eq(nil)
|
|
expect(result.username).to eq("IAmGroot")
|
|
expect(result.email).to eq(nil)
|
|
}.to change { UserAssociatedAccount.count }.by(1)
|
|
expect(UserAssociatedAccount.last.user).to eq(nil)
|
|
expect(UserAssociatedAccount.last.info["nickname"]).to eq("IAmGroot")
|
|
end
|
|
|
|
it 'will ignore name when equal to email' do
|
|
result = authenticator.after_authenticate(hash.deep_merge(info: { name: hash.info.email }))
|
|
expect(result.email).to eq(hash.info.email)
|
|
expect(result.name).to eq(nil)
|
|
end
|
|
end
|
|
|
|
describe "avatar on update" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "schedules the job upon update correctly" do
|
|
# No image supplied, do not schedule
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0)
|
|
|
|
# Image supplied, schedule
|
|
expect { result = authenticator.after_authenticate(hash.deep_merge(info: { image: "https://some.domain/image.jpg" })) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1)
|
|
|
|
# User already has profile picture, don't schedule
|
|
user.user_avatar = Fabricate(:user_avatar, custom_upload: Fabricate(:upload))
|
|
user.save!
|
|
expect { result = authenticator.after_authenticate(hash.deep_merge(info: { image: "https://some.domain/image.jpg" })) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0)
|
|
end
|
|
end
|
|
|
|
describe "profile on update" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "updates the user's location and bio, unless already set" do
|
|
{ description: :bio_raw, location: :location }.each do |auth_hash_key, profile_key|
|
|
user.user_profile.update(profile_key => "Initial Value")
|
|
# No value supplied, do not overwrite
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.not_to change { user.user_profile.reload; user.user_profile[profile_key] }
|
|
|
|
# Value supplied, still do not overwrite
|
|
expect { result = authenticator.after_authenticate(hash.deep_merge(info: { auth_hash_key => "New Value" })) }
|
|
.not_to change { user.user_profile.reload; user.user_profile[profile_key] }
|
|
|
|
# User has not set a value, so overwrite
|
|
user.user_profile.update(profile_key => "")
|
|
authenticator.after_authenticate(hash.deep_merge(info: { auth_hash_key => "New Value" }))
|
|
user.user_profile.reload
|
|
expect(user.user_profile[profile_key]).to eq("New Value")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "email update" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "updates the user's email if currently invalid" do
|
|
user.update!(email: "someemail@discourse.org")
|
|
# Existing email is valid, do not change
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.not_to change { user.reload.email }
|
|
|
|
user.update!(email: "someemail@discourse.invalid")
|
|
# Existing email is invalid, expect change
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.to change { user.reload.email }
|
|
|
|
expect(user.email).to eq("awesome@example.com")
|
|
end
|
|
|
|
it "doesn't raise error if email is taken" do
|
|
other_user = Fabricate(:user, email: "awesome@example.com")
|
|
user.update!(email: "someemail@discourse.invalid")
|
|
|
|
expect { result = authenticator.after_authenticate(hash) }
|
|
.not_to change { user.reload.email }
|
|
|
|
expect(user.email).to eq("someemail@discourse.invalid")
|
|
end
|
|
end
|
|
|
|
describe "avatar on create" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "doesn't schedule with no image" do
|
|
expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(0)
|
|
end
|
|
|
|
it "schedules with image" do
|
|
association.info["image"] = "https://some.domain/image.jpg"
|
|
association.save!
|
|
expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) }
|
|
.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1)
|
|
end
|
|
end
|
|
|
|
describe "profile on create" do
|
|
fab!(:user) { Fabricate(:user) }
|
|
let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") }
|
|
|
|
it "doesn't explode without profile" do
|
|
authenticator.after_create_account(user, create_auth_result(extra_data: create_hash))
|
|
end
|
|
|
|
it "works with profile" do
|
|
association.info["location"] = "DiscourseVille"
|
|
association.info["description"] = "Online forum expert"
|
|
association.save!
|
|
authenticator.after_create_account(user, create_auth_result(extra_data: create_hash))
|
|
expect(user.user_profile.bio_raw).to eq("Online forum expert")
|
|
expect(user.user_profile.location).to eq("DiscourseVille")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'description_for_user' do
|
|
fab!(:user) { Fabricate(:user) }
|
|
|
|
it 'returns empty string if no entry for user' do
|
|
expect(authenticator.description_for_user(user)).to eq("")
|
|
end
|
|
|
|
it 'returns correct information' do
|
|
association = UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", info: { nickname: "somenickname", email: "test@domain.tld", name: "bestname" })
|
|
expect(authenticator.description_for_user(user)).to eq('test@domain.tld')
|
|
association.update(info: { nickname: "somenickname", name: "bestname" })
|
|
expect(authenticator.description_for_user(user)).to eq('somenickname')
|
|
association.update(info: { nickname: "bestname" })
|
|
expect(authenticator.description_for_user(user)).to eq('bestname')
|
|
association.update(info: {})
|
|
expect(authenticator.description_for_user(user)).to eq(I18n.t("associated_accounts.connected"))
|
|
end
|
|
end
|
|
|
|
describe 'revoke' do
|
|
fab!(:user) { Fabricate(:user) }
|
|
|
|
it 'raises exception if no entry for user' do
|
|
expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound)
|
|
end
|
|
|
|
context "with valid record" do
|
|
before do
|
|
UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", info: { name: "somename" })
|
|
end
|
|
|
|
it 'revokes correctly' do
|
|
expect(authenticator.description_for_user(user)).to eq("somename")
|
|
expect(authenticator.can_revoke?).to eq(true)
|
|
expect(authenticator.revoke(user)).to eq(true)
|
|
|
|
expect(authenticator.description_for_user(user)).to eq("")
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|