Commit 01032275 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch...

Merge branch '300714-provide-name-of-expiring-token-in-personal-access-token-expiration-mail' into 'master'

Provide name of expiring token in personal access token expiration mail

See merge request gitlab-org/gitlab!53766
parents e763c44e 3435fcde
...@@ -50,15 +50,16 @@ module Emails ...@@ -50,15 +50,16 @@ module Emails
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def access_token_about_to_expire_email(user) def access_token_about_to_expire_email(user, token_names)
return unless user return unless user
@user = user @user = user
@token_names = token_names
@target_url = profile_personal_access_tokens_url @target_url = profile_personal_access_tokens_url
@days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE @days_to_expire = PersonalAccessToken::DAYS_TO_EXPIRE
Gitlab::I18n.with_locale(@user.preferred_language) do Gitlab::I18n.with_locale(@user.preferred_language) do
mail(to: @user.notification_email, subject: subject(_("Your Personal Access Tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire })) mail(to: @user.notification_email, subject: subject(_("Your personal access tokens will expire in %{days_to_expire} days or less") % { days_to_expire: @days_to_expire }))
end end
end end
......
...@@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord ...@@ -4,6 +4,7 @@ class PersonalAccessToken < ApplicationRecord
include Expirable include Expirable
include TokenAuthenticatable include TokenAuthenticatable
include Sortable include Sortable
include EachBatch
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
add_authentication_token_field :token, digest: true add_authentication_token_field :token, digest: true
...@@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord ...@@ -97,6 +98,10 @@ class PersonalAccessToken < ApplicationRecord
end end
def set_default_scopes def set_default_scopes
# When only loading a select set of attributes, for example using `EachBatch`,
# the `scopes` attribute is not present, so we can't initialize it.
return unless has_attribute?(:scopes)
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end end
......
...@@ -66,10 +66,10 @@ class NotificationService ...@@ -66,10 +66,10 @@ class NotificationService
# Notify the owner of the personal access token, when it is about to expire # Notify the owner of the personal access token, when it is about to expire
# And mark the token with about_to_expire_delivered # And mark the token with about_to_expire_delivered
def access_token_about_to_expire(user) def access_token_about_to_expire(user, token_names)
return unless user.can?(:receive_notifications) return unless user.can?(:receive_notifications)
mailer.access_token_about_to_expire_email(user).deliver_later mailer.access_token_about_to_expire_email(user, token_names).deliver_later
end end
# Notify the user when at least one of their personal access tokens has expired today # Notify the user when at least one of their personal access tokens has expired today
......
%p %p
= _('Hi %{username}!') % { username: sanitize_name(@user.name) } = _('Hi %{username}!') % { username: sanitize_name(@user.name) }
%p %p
= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire } = _('One or more of your personal access tokens will expire in %{days_to_expire} days or less:') % { days_to_expire: @days_to_expire }
%p %p
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url } %ul
- @token_names.each do |token|
%li= token
%p
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
= html_escape(_('You can create a new one or check them in your %{pat_link_start}personal access tokens%{pat_link_end} settings')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe } = html_escape(_('You can create a new one or check them in your %{pat_link_start}personal access tokens%{pat_link_end} settings')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %> <%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
<%= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less.') % { days_to_expire: @days_to_expire} %> <%= _('One or more of your personal access tokens will expire in %{days_to_expire} days or less:') % { days_to_expire: @days_to_expire } %>
<% @token_names.each do |token| %>
- <%= token %>
<% end %>
<%= _('You can create a new one or check them in your personal access tokens settings %{pat_link}') % { pat_link: @target_url } %> <%= _('You can create a new one or check them in your personal access tokens settings %{pat_link}') % { pat_link: @target_url } %>
...@@ -7,17 +7,32 @@ module PersonalAccessTokens ...@@ -7,17 +7,32 @@ module PersonalAccessTokens
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
MAX_TOKENS = 100
def perform(*args) def perform(*args)
notification_service = NotificationService.new notification_service = NotificationService.new
limit_date = PersonalAccessToken::DAYS_TO_EXPIRE.days.from_now.to_date limit_date = PersonalAccessToken::DAYS_TO_EXPIRE.days.from_now.to_date
User.with_expiring_and_not_notified_personal_access_tokens(limit_date).find_each do |user| User.with_expiring_and_not_notified_personal_access_tokens(limit_date).find_each do |user|
with_context(user: user) do with_context(user: user) do
notification_service.access_token_about_to_expire(user) expiring_user_tokens = user.personal_access_tokens.without_impersonation.expiring_and_not_notified(limit_date)
# rubocop: disable CodeReuse/ActiveRecord
# We never materialise the token instances. We need the names to mention them in the
# email. Later we trigger an update query on the entire relation, not on individual instances.
token_names = expiring_user_tokens.limit(MAX_TOKENS).pluck(:name)
# We're limiting to 100 tokens so we avoid loading too many tokens into memory.
# At the time of writing this would only affect 69 users on GitLab.com
# rubocop: enable CodeReuse/ActiveRecord
notification_service.access_token_about_to_expire(user, token_names)
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring tokens" Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring tokens"
user.personal_access_tokens.without_impersonation.expiring_and_not_notified(limit_date).update_all(expire_notification_delivered: true) expiring_user_tokens.each_batch do |expiring_tokens|
expiring_tokens.update_all(expire_notification_delivered: true)
end
end end
end end
end end
......
---
title: Provide name of expiring token in personal access token expiration mail
merge_request: 53766
author:
type: changed
...@@ -21067,7 +21067,7 @@ msgstr "" ...@@ -21067,7 +21067,7 @@ msgstr ""
msgid "One or more of your personal access tokens has expired." msgid "One or more of your personal access tokens has expired."
msgstr "" msgstr ""
msgid "One or more of your personal access tokens will expire in %{days_to_expire} days or less." msgid "One or more of your personal access tokens will expire in %{days_to_expire} days or less:"
msgstr "" msgstr ""
msgid "Only 'Reporter' roles and above on tiers Premium and above can see Value Stream Analytics." msgid "Only 'Reporter' roles and above on tiers Premium and above can see Value Stream Analytics."
...@@ -34389,9 +34389,6 @@ msgstr "" ...@@ -34389,9 +34389,6 @@ msgstr ""
msgid "Your Personal Access Token was revoked" msgid "Your Personal Access Token was revoked"
msgstr "" msgstr ""
msgid "Your Personal Access Tokens will expire in %{days_to_expire} days or less"
msgstr ""
msgid "Your Primary Email will be used for avatar detection." msgid "Your Primary Email will be used for avatar detection."
msgstr "" msgstr ""
...@@ -34563,6 +34560,9 @@ msgstr "" ...@@ -34563,6 +34560,9 @@ msgstr ""
msgid "Your personal access token has expired" msgid "Your personal access token has expired"
msgstr "" msgstr ""
msgid "Your personal access tokens will expire in %{days_to_expire} days or less"
msgstr ""
msgid "Your profile" msgid "Your profile"
msgstr "" msgstr ""
......
...@@ -125,8 +125,9 @@ RSpec.describe Emails::Profile do ...@@ -125,8 +125,9 @@ RSpec.describe Emails::Profile do
describe 'user personal access token is about to expire' do describe 'user personal access token is about to expire' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:expiring_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now) }
subject { Notify.access_token_about_to_expire_email(user) } subject { Notify.access_token_about_to_expire_email(user, [expiring_token.name]) }
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
...@@ -137,13 +138,17 @@ RSpec.describe Emails::Profile do ...@@ -137,13 +138,17 @@ RSpec.describe Emails::Profile do
end end
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /^Your Personal Access Tokens will expire in 7 days or less$/i is_expected.to have_subject /^Your personal access tokens will expire in 7 days or less$/i
end end
it 'mentions the access tokens will expire' do it 'mentions the access tokens will expire' do
is_expected.to have_body_text /One or more of your personal access tokens will expire in 7 days or less/ is_expected.to have_body_text /One or more of your personal access tokens will expire in 7 days or less/
end end
it 'provides the names of expiring tokens' do
is_expected.to have_body_text /#{expiring_token.name}/
end
it 'includes a link to personal access tokens page' do it 'includes a link to personal access tokens page' do
is_expected.to have_body_text /#{profile_personal_access_tokens_path}/ is_expected.to have_body_text /#{profile_personal_access_tokens_path}/
end end
......
...@@ -243,11 +243,12 @@ RSpec.describe NotificationService, :mailer do ...@@ -243,11 +243,12 @@ RSpec.describe NotificationService, :mailer do
describe 'AccessToken' do describe 'AccessToken' do
describe '#access_token_about_to_expire' do describe '#access_token_about_to_expire' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:pat) { create(:personal_access_token, user: user, expires_at: 5.days.from_now) }
subject { notification.access_token_about_to_expire(user) } subject { notification.access_token_about_to_expire(user, [pat.name]) }
it 'sends email to the token owner' do it 'sends email to the token owner' do
expect { subject }.to have_enqueued_email(user, mail: "access_token_about_to_expire_email") expect { subject }.to have_enqueued_email(user, [pat.name], mail: "access_token_about_to_expire_email")
end end
end end
......
...@@ -7,18 +7,23 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do ...@@ -7,18 +7,23 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
describe '#perform' do describe '#perform' do
context 'when a token needs to be notified' do context 'when a token needs to be notified' do
let_it_be(:pat) { create(:personal_access_token, expires_at: 5.days.from_now) } let_it_be(:user) { create(:user) }
let_it_be(:expiring_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now) }
let_it_be(:expiring_token2) { create(:personal_access_token, user: user, expires_at: 3.days.from_now) }
let_it_be(:notified_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now, expire_notification_delivered: true) }
let_it_be(:not_expiring_token) { create(:personal_access_token, user: user, expires_at: 1.month.from_now) }
let_it_be(:impersonation_token) { create(:personal_access_token, user: user, expires_at: 5.days.from_now, impersonation: true) }
it 'uses notification service to send the email' do it 'uses notification service to send the email' do
expect_next_instance_of(NotificationService) do |notification_service| expect_next_instance_of(NotificationService) do |notification_service|
expect(notification_service).to receive(:access_token_about_to_expire).with(pat.user) expect(notification_service).to receive(:access_token_about_to_expire).with(user, match_array([expiring_token.name, expiring_token2.name]))
end end
worker.perform worker.perform
end end
it 'marks the notification as delivered' do it 'marks the notification as delivered' do
expect { worker.perform }.to change { pat.reload.expire_notification_delivered }.from(false).to(true) expect { worker.perform }.to change { expiring_token.reload.expire_notification_delivered }.from(false).to(true)
end end
end end
...@@ -27,7 +32,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do ...@@ -27,7 +32,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
it "doesn't use notification service to send the email" do it "doesn't use notification service to send the email" do
expect_next_instance_of(NotificationService) do |notification_service| expect_next_instance_of(NotificationService) do |notification_service|
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user) expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user, [pat.name])
end end
worker.perform worker.perform
...@@ -43,7 +48,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do ...@@ -43,7 +48,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
it "doesn't use notification service to send the email" do it "doesn't use notification service to send the email" do
expect_next_instance_of(NotificationService) do |notification_service| expect_next_instance_of(NotificationService) do |notification_service|
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user) expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user, [pat.name])
end end
worker.perform worker.perform
......
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