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