User can keep their commit email private

The private commit email is automatically generated in the format:
id-username@noreply.HOSTNAME

GitLab instance admins are able to change the HOSTNAME portion,
that defaults to Gitlab's hostname, to whatever they prefer.
parent c81d4a65
......@@ -217,7 +217,8 @@ module ApplicationSettingsHelper
:user_oauth_applications,
:version_check_enabled,
:web_ide_clientside_preview_enabled,
:diff_max_patch_bytes
:diff_max_patch_bytes,
:commit_email_hostname
]
end
......
......@@ -22,7 +22,7 @@ module AvatarsHelper
end
def avatar_icon_for_email(email = nil, size = nil, scale = 2, only_path: true)
user = User.find_by_any_email(email.try(:downcase))
user = User.find_by_any_email(email)
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
else
......
# frozen_string_literal: true
module ProfilesHelper
def commit_email_select_options(user)
private_email = user.private_commit_email
verified_emails = user.verified_emails - [private_email]
[
[s_("Profiles|Use a private email - %{email}").html_safe % { email: private_email }, Gitlab::PrivateCommitEmail::TOKEN],
verified_emails
]
end
def selected_commit_email(user)
user.read_attribute(:commit_email) || user.commit_email
end
def attribute_provider_label(attribute)
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute)
......
......@@ -187,6 +187,8 @@ class ApplicationSetting < ActiveRecord::Base
validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :commit_email_hostname, format: { with: /\A[^@]+\z/ }
validates :archive_builds_in_seconds,
allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1.day.seconds }
......@@ -299,10 +301,15 @@ class ApplicationSetting < ActiveRecord::Base
user_default_internal_regex: nil,
user_show_add_ssh_key_message: true,
usage_stats_set_by_user_id: nil,
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
commit_email_hostname: default_commit_email_hostname
}
end
def self.default_commit_email_hostname
"users.noreply.#{Gitlab.config.gitlab.host}"
end
def self.create_from_defaults
create(defaults)
end
......@@ -358,6 +365,10 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages))
end
def commit_email_hostname
super.presence || self.class.default_commit_email_hostname
end
def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level))
end
......
......@@ -260,7 +260,7 @@ class Commit
request_cache(:author) { author_email.downcase }
def committer
@committer ||= User.find_by_any_email(committer_email.downcase)
@committer ||= User.find_by_any_email(committer_email)
end
def parents
......
......@@ -347,7 +347,11 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email, confirmed: false)
by_any_email(email, confirmed: confirmed).take
return unless email
downcased = email.downcase
find_by_private_commit_email(downcased) || by_any_email(downcased, confirmed: confirmed).take
end
# Returns a relation containing all the users for the given Email address
......@@ -361,6 +365,12 @@ class User < ActiveRecord::Base
from_union([users, emails])
end
def find_by_private_commit_email(email)
user_id = Gitlab::PrivateCommitEmail.user_id_for_email(email)
find_by(id: user_id)
end
def filter(filter_name)
case filter_name
when 'admins'
......@@ -633,6 +643,10 @@ class User < ActiveRecord::Base
def commit_email
return self.email unless has_attribute?(:commit_email)
if super == Gitlab::PrivateCommitEmail::TOKEN
return private_commit_email
end
# The commit email is the same as the primary email if undefined
super.presence || self.email
end
......@@ -645,6 +659,10 @@ class User < ActiveRecord::Base
has_attribute?(:commit_email) && super
end
def private_commit_email
Gitlab::PrivateCommitEmail.for_user(self)
end
# see if the new email is already a verified secondary email
def check_for_verified_email
skip_reconfirmation! if emails.confirmed.where(email: self.email).any?
......@@ -1020,13 +1038,21 @@ class User < ActiveRecord::Base
def verified_emails
verified_emails = []
verified_emails << email if primary_email_verified?
verified_emails << private_commit_email
verified_emails.concat(emails.confirmed.pluck(:email))
verified_emails
end
def verified_email?(check_email)
downcased = check_email.downcase
email == downcased ? primary_email_verified? : emails.confirmed.where(email: downcased).exists?
if email == downcased
primary_email_verified?
else
user_id = Gitlab::PrivateCommitEmail.user_id_for_email(downcased)
user_id == id || emails.confirmed.where(email: downcased).exists?
end
end
def hook_attrs
......
......@@ -20,5 +20,11 @@
By default GitLab sends emails in HTML and plain text formats so mail
clients can choose what format to use. Disable this option if you only
want to send emails in plain text format.
.form-group
= f.label :commit_email_hostname, _('Custom hostname (for private commit emails)'), class: 'label-bold'
= f.text_field :commit_email_hostname, class: 'form-control'
.form-text.text-muted
- commit_email_hostname_docs_link = link_to _('Learn more'), help_page_path('user/admin_area/settings/email', anchor: 'custom-private-commit-email-hostname'), target: '_blank'
= _("This setting will update the hostname that is used to generate private commit emails. %{learn_more}").html_safe % { learn_more: commit_email_hostname_docs_link }
= f.submit 'Save changes', class: "btn btn-success"
......@@ -91,8 +91,9 @@
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
{ help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2'
= f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email),
{ help: 'This email will be used for web based operations, such as edits and merges.' },
- commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank')
= f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)),
{ help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } },
control_class: 'select2'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
{ help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
......
---
title: Adds option to override commit email with a noreply private email
merge_request: 22560
author:
type: added
# frozen_string_literal: true
class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column(:application_settings, :commit_email_hostname, :string, null: true)
end
end
......@@ -166,6 +166,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do
t.integer "receive_max_input_size"
t.integer "diff_max_patch_bytes", default: 102400, null: false
t.integer "archive_builds_in_seconds"
t.string "commit_email_hostname"
end
create_table "audit_events", force: :cascade do |t|
......
......@@ -171,8 +171,8 @@ class Commit
extend Gitlab::Cache::RequestCache
def author
User.find_by_any_email(author_email.downcase)
User.find_by_any_email(author_email)
end
request_cache(:author) { author_email.downcase }
request_cache(:author) { author_email }
end
```
......@@ -3,3 +3,20 @@
## Custom logo
The logo in the header of some emails can be customized, see the [logo customization section](../../../customization/branded_page_and_email_header.md).
## Custom hostname for private commit emails
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
This configuration option sets the email hostname for [private commit emails](../../profile/index.md#private-commit-email),
and it's, by default, set to `users.noreply.YOUR_CONFIGURED_HOSTNAME`.
In order to change this option:
1. Go to **Admin area > Settings** (`/admin/application_settings`).
1. Under the **Email** section, change the **Custom hostname (for private commit emails)** field.
1. Hit **Save** for the changes to take effect.
NOTE: **Note**: Once the hostname gets configured, every private commit email using the previous hostname, will not get
recognized by GitLab. This can directly conflict with certain [Push rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) such as
`Check whether author is a GitLab user` and `Check whether committer is the current authenticated user`.
......@@ -31,6 +31,7 @@ From there, you can:
- Update your personal information
- Set a [custom status](#current-status) for your profile
- Manage your [commit email](#commit-email) for your profile
- Manage [2FA](account/two_factor_authentication.md)
- Change your username and [delete your account](account/delete_account.md)
- Manage applications that can
......@@ -132,6 +133,45 @@ They may however contain emoji codes such as `I'm on vacation :palm_tree:`.
You can also set your current status [using the API](../../api/users.md#user-status).
## Commit email
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21598) in GitLab 11.4.
A commit email, is the email that will be displayed in every Git-related action done through the
GitLab interface.
You are able to select from the list of your own verified emails which email you want to use as the commit email.
To change it:
1. Open the user menu in the top-right corner of the navigation bar.
1. Hit **Commit email** selection box.
1. Select any of the verified emails.
1. Hit **Update profile settings**.
### Private commit email
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22560) in GitLab 11.5.
GitLab provides the user with an automatically generated private commit email option,
which allows the user to not make their email information public.
To enable this option:
1. Open the user menu in the top-right corner of the navigation bar.
1. Hit **Commit email** selection box.
1. Select **Use a private email** option.
1. Hit **Update profile settings**.
Once this option is enabled, every Git-related action will be performed using the private commit email.
In order to stay fully annonymous, you can also copy this private commit email
and configure it on your local machine using the following command:
```
git config --global user.email "YOUR_PRIVATE_COMMIT_EMAIL"
```
## Troubleshooting
### Why do I keep getting signed out?
......
# frozen_string_literal: true
module Gitlab
module PrivateCommitEmail
TOKEN = "_private".freeze
class << self
def regex
hostname_regexp = Regexp.escape(Gitlab::CurrentSettings.current_application_settings.commit_email_hostname)
/\A(?<id>([0-9]+))\-([^@]+)@#{hostname_regexp}\z/
end
def user_id_for_email(email)
match = email&.match(regex)
return unless match
match[:id].to_i
end
def for_user(user)
hostname = Gitlab::CurrentSettings.current_application_settings.commit_email_hostname
"#{user.id}-#{user.username}@#{hostname}"
end
end
end
end
......@@ -2094,6 +2094,9 @@ msgstr ""
msgid "Custom CI config path"
msgstr ""
msgid "Custom hostname (for private commit emails)"
msgstr ""
msgid "Custom notification events"
msgstr ""
......@@ -4701,6 +4704,9 @@ msgstr ""
msgid "Profiles|Invalid username"
msgstr ""
msgid "Profiles|Learn more"
msgstr ""
msgid "Profiles|Made a private contribution"
msgstr ""
......@@ -4743,6 +4749,9 @@ msgstr ""
msgid "Profiles|This email will be displayed on your public profile."
msgstr ""
msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}"
msgstr ""
msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
msgstr ""
......@@ -4767,6 +4776,9 @@ msgstr ""
msgid "Profiles|Upload new avatar"
msgstr ""
msgid "Profiles|Use a private email - %{email}"
msgstr ""
msgid "Profiles|Username change failed - %{message}"
msgstr ""
......@@ -6296,6 +6308,9 @@ msgstr ""
msgid "This setting can be overridden in each project."
msgstr ""
msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}"
msgstr ""
msgid "This source diff could not be displayed because it is too large."
msgstr ""
......
require 'rails_helper'
describe ProfilesHelper do
describe '#commit_email_select_options' do
it 'returns an array with private commit email along with all the verified emails' do
user = create(:user)
private_email = user.private_commit_email
verified_emails = user.verified_emails - [private_email]
emails = [
["Use a private email - #{private_email}", Gitlab::PrivateCommitEmail::TOKEN],
verified_emails
]
expect(helper.commit_email_select_options(user)).to match_array(emails)
end
end
describe '#selected_commit_email' do
let(:user) { create(:user) }
it 'returns main email when commit email attribute is nil' do
expect(helper.selected_commit_email(user)).to eq(user.email)
end
it 'returns DB stored commit_email' do
user.update(commit_email: Gitlab::PrivateCommitEmail::TOKEN)
expect(helper.selected_commit_email(user)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
end
end
describe '#email_provider_label' do
it "returns nil for users without external email" do
user = create(:user)
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::PrivateCommitEmail do
let(:hostname) { Gitlab::CurrentSettings.current_application_settings.commit_email_hostname }
context '.regex' do
subject { described_class.regex }
it { is_expected.to match("1-foo@#{hostname}") }
it { is_expected.not_to match("1-foo@#{hostname}.foo") }
it { is_expected.not_to match('1-foo@users.noreply.gitlab.com') }
it { is_expected.not_to match('foo-1@users.noreply.gitlab.com') }
it { is_expected.not_to match('foobar@gitlab.com') }
end
context '.user_id_for_email' do
let(:id) { 1 }
it 'parses user id from email' do
email = "#{id}-foo@#{hostname}"
expect(described_class.user_id_for_email(email)).to eq(id)
end
it 'returns nil on invalid commit email' do
email = "#{id}-foo@users.noreply.bar.com"
expect(described_class.user_id_for_email(email)).to be_nil
end
end
context '.for_user' do
it 'returns email in the format id-username@hostname' do
user = create(:user)
expect(described_class.for_user(user)).to eq("#{user.id}-#{user.username}@#{hostname}")
end
end
end
......@@ -25,6 +25,9 @@ describe ApplicationSetting do
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
......@@ -107,6 +110,14 @@ describe ApplicationSetting do
it { expect(setting.repository_storages).to eq(['default']) }
end
context '#commit_email_hostname' do
it 'returns configured gitlab hostname if commit_email_hostname is not defined' do
setting.update(commit_email_hostname: nil)
expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}")
end
end
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
......
......@@ -183,6 +183,12 @@ describe User do
expect(found_user.commit_email).to eq(user.email)
end
it 'returns the private commit email when commit_email has _private' do
user.update_column(:commit_email, Gitlab::PrivateCommitEmail::TOKEN)
expect(user.commit_email).to eq(user.private_commit_email)
end
it 'can be set to a confirmed email' do
confirmed = create(:email, :confirmed, user: user)
user.commit_email = confirmed.email
......@@ -333,6 +339,40 @@ describe User do
expect(user).to be_valid
end
end
context 'set_commit_email' do
it 'keeps commit email when private commit email is being used' do
user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
end
it 'keeps the commit email when nil' do
user = create(:user, commit_email: nil)
expect(user.read_attribute(:commit_email)).to be_nil
end
it 'reverts to nil when email is not verified' do
user = create(:user, commit_email: "foo@bar.com")
expect(user.read_attribute(:commit_email)).to be_nil
end
end
context 'owns_commit_email' do
it 'accepts private commit email' do
user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
expect(user).to be_valid
end
it 'accepts nil commit email' do
user = build(:user, commit_email: nil)
expect(user).to be_valid
end
end
end
end
......@@ -1075,6 +1115,14 @@ describe User do
end
describe '.find_by_any_email' do
it 'finds user through private commit email' do
user = create(:user)
private_email = user.private_commit_email
expect(described_class.find_by_any_email(private_email)).to eq(user)
expect(described_class.find_by_any_email(private_email, confirmed: true)).to eq(user)
end
it 'finds by primary email' do
user = create(:user, email: 'foo@example.com')
......@@ -1082,6 +1130,13 @@ describe User do
expect(described_class.find_by_any_email(user.email, confirmed: true)).to eq user
end
it 'finds by uppercased email' do
user = create(:user, email: 'foo@example.com')
expect(described_class.find_by_any_email(user.email.upcase)).to eq user
expect(described_class.find_by_any_email(user.email.upcase, confirmed: true)).to eq user
end
it 'finds by secondary email' do
email = create(:email, email: 'foo@example.com')
user = email.user
......@@ -1457,7 +1512,7 @@ describe User do
email_confirmed = create :email, user: user, confirmed_at: Time.now
create :email, user: user
expect(user.verified_emails).to match_array([user.email, email_confirmed.email])
expect(user.verified_emails).to match_array([user.email, user.private_commit_email, email_confirmed.email])
end
end
......@@ -1473,6 +1528,10 @@ describe User do
expect(user.verified_email?(email_confirmed.email.titlecase)).to be_truthy
end
it 'returns true when user is found through private commit email' do
expect(user.verified_email?(user.private_commit_email)).to be_truthy
end
it 'returns false when the email is not verified/confirmed' do
email_unconfirmed = create :email, user: user
user.reload
......@@ -1668,6 +1727,24 @@ describe User do
end
end
describe '.find_by_private_commit_email' do
context 'with email' do
set(:user) { create(:user) }
it 'returns user through private commit email' do
expect(described_class.find_by_private_commit_email(user.private_commit_email)).to eq(user)
end
it 'returns nil when email other than private_commit_email is used' do
expect(described_class.find_by_private_commit_email(user.email)).to be_nil
end
end
it 'returns nil when email is nil' do
expect(described_class.find_by_private_commit_email(nil)).to be_nil
end
end
describe '#sort_by_attribute' do
before do
described_class.delete_all
......
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