diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index c27e181ebd6d542dc8314caa1e0b3973257841af..65ea90d0b5dd915fdcbce2d043fc62370ae05bff 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -94,6 +94,18 @@ def access_token_expired_email(user)
end
end
+ def access_token_revoked_email(user, token_name)
+ return unless user&.active?
+
+ @user = user
+ @token_name = token_name
+ @target_url = profile_personal_access_tokens_url
+
+ Gitlab::I18n.with_locale(@user.preferred_language) do
+ mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("A personal access token has been revoked")))
+ end
+ end
+
def ssh_key_expired_email(user, fingerprints)
return unless user&.active?
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index d29bd15ac2e131c97b348172089f753b110495f2..1224cf80b76c4a66f0ca4a6b93136b49feb55fad 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -87,6 +87,13 @@ def access_token_expired(user)
mailer.access_token_expired_email(user).deliver_later
end
+ # Notify the user when one of their personal access tokens is revoked
+ def access_token_revoked(user, token_name)
+ return unless user.can?(:receive_notifications)
+
+ mailer.access_token_revoked_email(user, token_name).deliver_later
+ end
+
# Notify the user when at least one of their ssh key has expired today
def ssh_key_expired(user, fingerprints)
return unless user.can?(:receive_notifications)
diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb
index 0275d03bcc92750b98773b3c8dfdc89acd0ef492..732da75da3a1a0474fa1cd22406f0d24a94c0811 100644
--- a/app/services/personal_access_tokens/revoke_service.rb
+++ b/app/services/personal_access_tokens/revoke_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module PersonalAccessTokens
- class RevokeService
+ class RevokeService < BaseService
attr_reader :token, :current_user, :group
def initialize(current_user = nil, token: nil, group: nil )
@@ -15,6 +15,7 @@ def execute
if token.revoke!
log_event
+ notification_service.access_token_revoked(token.user, token.name)
ServiceResponse.success(message: success_message)
else
ServiceResponse.error(message: error_message)
diff --git a/app/views/notify/access_token_revoked_email.html.haml b/app/views/notify/access_token_revoked_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4d9b9e14d145b20884641031356ead8cd8d54c50
--- /dev/null
+++ b/app/views/notify/access_token_revoked_email.html.haml
@@ -0,0 +1,7 @@
+%p
+ = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
+%p
+ = html_escape(_('A personal access token, named %{code_start}%{token_name}%{code_end}, has been revoked.')) % { code_start: ''.html_safe, token_name: @token_name, code_end: ''.html_safe }
+%p
+ - pat_link_start = ''.html_safe % { url: @target_url }
+ = html_escape(_('You can check your tokens or create a new one in your %{pat_link_start}personal access tokens settings%{pat_link_end}.')) % { pat_link_start: pat_link_start, pat_link_end: ''.html_safe }
diff --git a/app/views/notify/access_token_revoked_email.text.erb b/app/views/notify/access_token_revoked_email.text.erb
new file mode 100644
index 0000000000000000000000000000000000000000..17dd628d76c54c77498500d263b0f4bd0c0cb748
--- /dev/null
+++ b/app/views/notify/access_token_revoked_email.text.erb
@@ -0,0 +1,5 @@
+<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
+
+<%= _('A personal access token, named %{token_name}, has been revoked.') % { token_name: @token_name } %>
+
+<%= _('You can check your tokens or create a new one in your personal access tokens settings %{pat_link}.') % { pat_link: @target_url } %>
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e69e89e62eaa29d7df754c521537860714749441..a01e3642b301d2e42099d93f5b3132faa8f892a5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1700,6 +1700,15 @@ msgstr ""
msgid "A page with that title already exists"
msgstr ""
+msgid "A personal access token has been revoked"
+msgstr ""
+
+msgid "A personal access token, named %{code_start}%{token_name}%{code_end}, has been revoked."
+msgstr ""
+
+msgid "A personal access token, named %{token_name}, has been revoked."
+msgstr ""
+
msgid "A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features"
msgstr ""
@@ -45682,6 +45691,12 @@ msgstr ""
msgid "You can check it in your in your personal access tokens settings %{pat_link}."
msgstr ""
+msgid "You can check your tokens or create a new one in your %{pat_link_start}personal access tokens settings%{pat_link_end}."
+msgstr ""
+
+msgid "You can check your tokens or create a new one in your personal access tokens settings %{pat_link}."
+msgstr ""
+
msgid "You can create a new %{link}."
msgstr ""
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index f6f02f2ba3966d099f80141caf91882c11644398..073d8aee842d9469458df25a3da0ca2da2e9e34d 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -246,6 +246,35 @@
end
end
+ describe 'user personal access token has been revoked' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:token) { create(:personal_access_token, user: user) }
+
+ context 'when valid' do
+ subject { Notify.access_token_revoked_email(user, token.name) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
+
+ it 'is sent to the user' do
+ is_expected.to deliver_to user.email
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /^A personal access token has been revoked$/i
+ end
+
+ it 'provides the names of the token' do
+ is_expected.to have_body_text /#{token.name}/
+ end
+
+ it 'includes the email reason' do
+ is_expected.to have_body_text %r{You're receiving this email because of your account on localhost<\/a>}
+ end
+ end
+ end
+
describe 'SSH key notification' do
let_it_be_with_reload(:user) { create(:user) }
let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 935dcef1011c5ad237dd1a59e64dcbc6a95f8f01..8fbf023cda072cccc8545c0883f094b5cd3c3a7f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -337,6 +337,27 @@
end
end
end
+
+ describe '#access_token_revoked' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pat) { create(:personal_access_token, user: user) }
+
+ subject(:notification_service) { notification.access_token_revoked(user, pat.name) }
+
+ it 'sends email to the token owner' do
+ expect { notification_service }.to have_enqueued_email(user, pat.name, mail: "access_token_revoked_email")
+ end
+
+ context 'when user is not allowed to receive notifications' do
+ before do
+ user.block!
+ end
+
+ it 'does not send email to the token owner' do
+ expect { notification_service }.not_to have_enqueued_email(user, pat.name, mail: "access_token_revoked_email")
+ end
+ end
+ end
end
describe 'SSH Keys' do