Commit 391bc665 authored by Drew Blessing's avatar Drew Blessing

Only allow invite to be accepted by user with matching email

Previously, any user was able to accept an invite even if the
user's email addresses didn't match the invite. A note was
displayed but the invite could still be accepted. With this
change, a user without a matching, confirmed email address
is unable to accept the invite.

Changelog: security
parent 9f3d696e
...@@ -20,7 +20,7 @@ class InvitesController < ApplicationController ...@@ -20,7 +20,7 @@ class InvitesController < ApplicationController
end end
def accept def accept
if member.accept_invite!(current_user) if current_user_matches_invite? && member.accept_invite!(current_user)
redirect_to invite_details[:path], notice: helpers.invite_accepted_notice(member) redirect_to invite_details[:path], notice: helpers.invite_accepted_notice(member)
else else
redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") }) redirect_back_or_default(options: { alert: _("The invitation could not be accepted.") })
...@@ -52,7 +52,7 @@ class InvitesController < ApplicationController ...@@ -52,7 +52,7 @@ class InvitesController < ApplicationController
end end
def current_user_matches_invite? def current_user_matches_invite?
@member.invite_email == current_user.email current_user.verified_emails.include?(@member.invite_email)
end end
def member? def member?
......
- page_title _("Invitation") - page_title _("Invitation")
%h3.page-title= _("Invitation") %h3.page-title= _("Invitation")
%p - if current_user_matches_invite?
= _("You have been invited") - if member?
- inviter = @member.created_by
- if inviter
= _("by")
= link_to inviter.name, user_url(inviter)
= _("to join %{source_name}") % { source_name: @invite_details[:title] }
%strong
= link_to @invite_details[:name], @invite_details[:url]
= _("as %{role}.") % { role: @member.human_access }
- if member?
%p %p
= _("However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation.") % { member_source: @invite_details[:title] } = _("You are already a member of this %{member_source}.") % { member_source: @invite_details[:title] }
.actions
= link_to _("Go to %{source_name}") % { source_name: @invite_details[:title] }, @invite_details[:url], class: "btn gl-button btn-confirm"
- if !current_user_matches_invite? - else
%p %p
- mail_to_invite_email = mail_to(@member.invite_email) - inviter = @member.created_by
- mail_to_current_user = mail_to(current_user.email) - link_to_inviter = link_to(inviter.name, user_url(inviter))
- link_to_current_user = link_to(current_user.to_reference, user_url(current_user)) - link_to_source = link_to(@invite_details[:name], @invite_details[:url])
= _("Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
= html_escape(_("You have been invited by %{link_to_inviter} to join %{source_name} %{strong_open}%{link_to_source}%{strong_close} as %{role}")) % { link_to_inviter: link_to_inviter, source_name: @invite_details[:title], strong_open: '<strong>'.html_safe, link_to_source: link_to_source, strong_close: '</strong>'.html_safe, role: @member.human_access }
- if !member?
.actions .actions
= link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn gl-button btn-confirm" = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn gl-button btn-confirm"
= link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn gl-button btn-danger gl-ml-3" = link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn gl-button btn-danger gl-ml-3"
- else
%p
- mail_to_invite_email = mail_to(@member.invite_email)
- mail_to_current_user = mail_to(current_user.email)
- link_to_current_user = link_to(current_user.to_reference, user_url(current_user))
= _("This invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}.").html_safe % { mail_to_invite_email: mail_to_invite_email, mail_to_current_user: mail_to_current_user, link_to_current_user: link_to_current_user }
%p
= _("Sign in as a user with the matching email address, add the email to this account, or sign-up for a new account using the matching email.")
...@@ -15260,6 +15260,9 @@ msgstr "" ...@@ -15260,6 +15260,9 @@ msgstr ""
msgid "Go full screen" msgid "Go full screen"
msgstr "" msgstr ""
msgid "Go to %{source_name}"
msgstr ""
msgid "Go to commits" msgid "Go to commits"
msgstr "" msgstr ""
...@@ -16392,9 +16395,6 @@ msgstr "" ...@@ -16392,9 +16395,6 @@ msgstr ""
msgid "How many users will be evaluating the trial?" msgid "How many users will be evaluating the trial?"
msgstr "" msgstr ""
msgid "However, you are already a member of this %{member_source}. Sign in using a different account to accept the invitation."
msgstr ""
msgid "I accept the %{terms_link}" msgid "I accept the %{terms_link}"
msgstr "" msgstr ""
...@@ -22526,9 +22526,6 @@ msgstr "" ...@@ -22526,9 +22526,6 @@ msgstr ""
msgid "Note that pushing to GitLab requires write access to this repository." msgid "Note that pushing to GitLab requires write access to this repository."
msgstr "" msgstr ""
msgid "Note that this invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
msgstr ""
msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token." msgid "Note: As an administrator you may like to configure %{github_integration_link}, which will allow login via GitHub and allow connecting repositories without generating a Personal Access Token."
msgstr "" msgstr ""
...@@ -30454,6 +30451,9 @@ msgstr "" ...@@ -30454,6 +30451,9 @@ msgstr ""
msgid "Sign in / Register" msgid "Sign in / Register"
msgstr "" msgstr ""
msgid "Sign in as a user with the matching email address, add the email to this account, or sign-up for a new account using the matching email."
msgstr ""
msgid "Sign in preview" msgid "Sign in preview"
msgstr "" msgstr ""
...@@ -33718,6 +33718,9 @@ msgstr "" ...@@ -33718,6 +33718,9 @@ msgstr ""
msgid "This group, its subgroups and projects will be removed on %{date} since its parent group '%{parent_group_name}'' has been scheduled for removal." msgid "This group, its subgroups and projects will be removed on %{date} since its parent group '%{parent_group_name}'' has been scheduled for removal."
msgstr "" msgstr ""
msgid "This invitation was sent to %{mail_to_invite_email}, but you are signed in as %{link_to_current_user} with email %{mail_to_current_user}."
msgstr ""
msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed." msgid "This is a \"Ghost User\", created to hold all issues authored by users that have since been deleted. This user cannot be removed."
msgstr "" msgstr ""
...@@ -37477,6 +37480,9 @@ msgstr "" ...@@ -37477,6 +37480,9 @@ msgstr ""
msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete." msgid "You are about to transfer the control of your account to %{group_name} group. This action is NOT reversible, you won't be able to access any of your groups and projects outside of %{group_name} once this transfer is complete."
msgstr "" msgstr ""
msgid "You are already a member of this %{member_source}."
msgstr ""
msgid "You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution." msgid "You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr "" msgstr ""
...@@ -37798,7 +37804,7 @@ msgstr "" ...@@ -37798,7 +37804,7 @@ msgstr ""
msgid "You have been granted %{member_human_access} access to project %{name}." msgid "You have been granted %{member_human_access} access to project %{name}."
msgstr "" msgstr ""
msgid "You have been invited" msgid "You have been invited by %{link_to_inviter} to join %{source_name} %{strong_open}%{link_to_source}%{strong_close} as %{role}"
msgstr "" msgstr ""
msgid "You have been redirected to the only result; see the %{a_start}search results%{a_end} instead." msgid "You have been redirected to the only result; see the %{a_start}search results%{a_end} instead."
...@@ -38394,9 +38400,6 @@ msgstr "" ...@@ -38394,9 +38400,6 @@ msgstr ""
msgid "archived:" msgid "archived:"
msgstr "" msgstr ""
msgid "as %{role}."
msgstr ""
msgid "assign yourself" msgid "assign yourself"
msgstr "" msgstr ""
...@@ -39875,9 +39878,6 @@ msgstr "" ...@@ -39875,9 +39878,6 @@ msgstr ""
msgid "time summary" msgid "time summary"
msgstr "" msgstr ""
msgid "to join %{source_name}"
msgstr ""
msgid "toggle collapse" msgid "toggle collapse"
msgstr "" msgstr ""
......
...@@ -25,9 +25,64 @@ RSpec.describe InvitesController do ...@@ -25,9 +25,64 @@ RSpec.describe InvitesController do
end end
end end
shared_examples 'invite email match enforcement' do |error_status:, flash_alert: nil|
it 'accepts user if invite email matches signed in user' do
expect do
request
end.to change { project_members.include?(user) }.from(false).to(true)
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to include 'You have been granted'
end
it 'accepts invite if invite email matches confirmed secondary email' do
secondary_email = create(:email, :confirmed, user: user)
member.update!(invite_email: secondary_email.email)
expect do
request
end.to change { project_members.include?(user) }.from(false).to(true)
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to include 'You have been granted'
end
it 'does not accept if invite email matches unconfirmed secondary email' do
secondary_email = create(:email, user: user)
member.update!(invite_email: secondary_email.email)
expect do
request
end.not_to change { project_members.include?(user) }
expect(response).to have_gitlab_http_status(error_status)
expect(flash[:alert]).to eq(flash_alert)
end
it 'does not accept if invite email does not match signed in user' do
member.update!(invite_email: 'bogus@email.com')
expect do
request
end.not_to change { project_members.include?(user) }
expect(response).to have_gitlab_http_status(error_status)
expect(flash[:alert]).to eq(flash_alert)
end
end
describe 'GET #show', :snowplow do describe 'GET #show', :snowplow do
subject(:request) { get :show, params: params } subject(:request) { get :show, params: params }
context 'when logged in' do
before do
sign_in(user)
end
it_behaves_like 'invite email match enforcement', error_status: :ok
it_behaves_like 'invalid token'
end
context 'when it is an initial invite email' do context 'when it is an initial invite email' do
let(:extra_params) { { invite_type: 'initial_email' } } let(:extra_params) { { invite_type: 'initial_email' } }
...@@ -69,34 +124,6 @@ RSpec.describe InvitesController do ...@@ -69,34 +124,6 @@ RSpec.describe InvitesController do
end end
end end
context 'when logged in' do
before do
sign_in(user)
end
it 'accepts user if invite email matches signed in user' do
expect do
request
end.to change { project_members.include?(user) }.from(false).to(true)
expect(response).to have_gitlab_http_status(:found)
expect(flash[:notice]).to include 'You have been granted'
end
it 'forces re-confirmation if email does not match signed in user' do
member.update!(invite_email: 'bogus@email.com')
expect do
request
end.not_to change { project_members.include?(user) }
expect(response).to have_gitlab_http_status(:ok)
expect(flash[:notice]).to be_nil
end
it_behaves_like 'invalid token'
end
context 'when not logged in' do context 'when not logged in' do
context 'when invite token belongs to a valid member' do context 'when invite token belongs to a valid member' do
context 'when instance allows sign up' do context 'when instance allows sign up' do
...@@ -223,6 +250,7 @@ RSpec.describe InvitesController do ...@@ -223,6 +250,7 @@ RSpec.describe InvitesController do
subject(:request) { post :accept, params: params } subject(:request) { post :accept, params: params }
it_behaves_like 'invite email match enforcement', error_status: :redirect, flash_alert: 'The invitation could not be accepted.'
it_behaves_like 'invalid token' it_behaves_like 'invalid token'
end end
......
...@@ -90,48 +90,17 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do ...@@ -90,48 +90,17 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end end
context 'when signed in and an invite link is clicked' do context 'when signed in and an invite link is clicked' do
context 'when an invite email is a secondary email for the user' do
let(:invite_email) { 'user_secondary@example.com' }
before do
sign_in(user)
visit invite_path(group_invite.raw_invite_token)
end
it 'sends user to the invite url and allows them to decline' do
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content("Note that this invitation was sent to #{invite_email}")
expect(page).to have_content("but you are signed in as #{user.to_reference} with email #{user.email}")
click_link('Decline')
expect(page).to have_content('You have declined the invitation')
expect(current_path).to eq(dashboard_projects_path)
expect { group_invite.reload }.to raise_error ActiveRecord::RecordNotFound
end
it 'sends uer to the invite url and allows them to accept' do
expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_content("Note that this invitation was sent to #{invite_email}")
expect(page).to have_content("but you are signed in as #{user.to_reference} with email #{user.email}")
click_link('Accept invitation')
expect(page).to have_content('You have been granted')
expect(current_path).to eq(activity_group_path(group))
end
end
context 'when user is an existing member' do context 'when user is an existing member' do
before do before do
sign_in(owner) group.add_developer(user)
sign_in(user)
visit invite_path(group_invite.raw_invite_token) visit invite_path(group_invite.raw_invite_token)
end end
it 'shows message user already a member' do it 'shows message user already a member' do
expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) expect(current_path).to eq(invite_path(group_invite.raw_invite_token))
expect(page).to have_link(owner.name, href: user_url(owner)) expect(page).to have_link(user.name, href: user_path(user))
expect(page).to have_content('However, you are already a member of this group.') expect(page).to have_content('You are already a member of this group.')
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment