Commit 29a0807e authored by Luke Duncalfe's avatar Luke Duncalfe

Merge branch 'expiring-ssh-key-notification' into 'master'

SSH key expiring soon email notification

See merge request gitlab-org/gitlab!58171
parents 07a4c90a a283ca46
...@@ -75,7 +75,7 @@ module Emails ...@@ -75,7 +75,7 @@ module Emails
end end
def ssh_key_expired_email(user, fingerprints) def ssh_key_expired_email(user, fingerprints)
return unless user && user.active? return unless user&.active?
@user = user @user = user
@fingerprints = fingerprints @fingerprints = fingerprints
...@@ -86,6 +86,18 @@ module Emails ...@@ -86,6 +86,18 @@ module Emails
end end
end end
def ssh_key_expiring_soon_email(user, fingerprints)
return unless user&.active?
@user = user
@fingerprints = fingerprints
@target_url = profile_keys_url
Gitlab::I18n.with_locale(@user.preferred_language) do
mail(to: @user.notification_email, subject: subject(_("Your SSH key is expiring soon.")))
end
end
def unknown_sign_in_email(user, ip, time) def unknown_sign_in_email(user, ip, time)
@user = user @user = user
@ip = ip @ip = ip
......
...@@ -44,6 +44,7 @@ class Key < ApplicationRecord ...@@ -44,6 +44,7 @@ class Key < ApplicationRecord
scope :for_user, -> (user) { where(user: user) } scope :for_user, -> (user) { where(user: user) }
scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) } scope :order_last_used_at_desc, -> { reorder(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) } scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
scope :expiring_soon_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') > CURRENT_DATE AND date(expires_at AT TIME ZONE 'UTC') < ? AND before_expiry_notification_delivered_at IS NULL", DAYS_TO_EXPIRE.days.from_now.to_date]) }
def self.regular_keys def self.regular_keys
where(type: ['Key', nil]) where(type: ['Key', nil])
......
...@@ -104,6 +104,7 @@ class User < ApplicationRecord ...@@ -104,6 +104,7 @@ class User < ApplicationRecord
# Profile # Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :expired_today_and_unnotified_keys, -> { expired_today_and_not_notified }, class_name: 'Key' has_many :expired_today_and_unnotified_keys, -> { expired_today_and_not_notified }, class_name: 'Key'
has_many :expiring_soon_and_unnotified_keys, -> { expiring_soon_and_not_notified }, class_name: 'Key'
has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
has_many :group_deploy_keys has_many :group_deploy_keys
has_many :gpg_keys has_many :gpg_keys
...@@ -402,6 +403,14 @@ class User < ApplicationRecord ...@@ -402,6 +403,14 @@ class User < ApplicationRecord
.where('keys.user_id = users.id') .where('keys.user_id = users.id')
.expired_today_and_not_notified) .expired_today_and_not_notified)
end end
scope :with_ssh_key_expiring_soon, -> do
includes(:expiring_soon_and_unnotified_keys)
.where('EXISTS (?)',
::Key
.select(1)
.where('keys.user_id = users.id')
.expiring_soon_and_not_notified)
end
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) } scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) }
......
...@@ -2,17 +2,38 @@ ...@@ -2,17 +2,38 @@
module Keys module Keys
class ExpiryNotificationService < ::Keys::BaseService class ExpiryNotificationService < ::Keys::BaseService
attr_accessor :keys attr_accessor :keys, :expiring_soon
def initialize(user, params) def initialize(user, params)
@keys = params[:keys] @keys = params[:keys]
@expiring_soon = params[:expiring_soon]
super super
end end
def execute def execute
return unless user.can?(:receive_notifications) return unless allowed?
if expiring_soon
trigger_expiring_soon_notification
else
trigger_expired_notification
end
end
private
def allowed?
user.can?(:receive_notifications)
end
def trigger_expiring_soon_notification
notification_service.ssh_key_expiring_soon(user, keys.map(&:fingerprint))
keys.update_all(before_expiry_notification_delivered_at: Time.current.utc)
end
def trigger_expired_notification
notification_service.ssh_key_expired(user, keys.map(&:fingerprint)) notification_service.ssh_key_expired(user, keys.map(&:fingerprint))
keys.update_all(expiry_notification_delivered_at: Time.current.utc) keys.update_all(expiry_notification_delivered_at: Time.current.utc)
......
...@@ -86,6 +86,13 @@ class NotificationService ...@@ -86,6 +86,13 @@ class NotificationService
mailer.ssh_key_expired_email(user, fingerprints).deliver_later mailer.ssh_key_expired_email(user, fingerprints).deliver_later
end end
# Notify the user when at least one of their ssh key is expiring soon
def ssh_key_expiring_soon(user, fingerprints)
return unless user.can?(:receive_notifications)
mailer.ssh_key_expiring_soon_email(user, fingerprints).deliver_later
end
# Notify a user when a previously unknown IP or device is used to # Notify a user when a previously unknown IP or device is used to
# sign in to their account # sign in to their account
def unknown_sign_in(user, ip, time) def unknown_sign_in(user, ip, time)
......
<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
<%= _('Your SSH keys with the following fingerprints are scheduled to expire soon:') %>
<% @fingerprints.each do |fingerprint| %>
- <%= fingerprint %>
<% end %>
<%= _('You can create a new one or check them in your SSH keys settings %{ssh_key_link}.') % { ssh_key_link: @target_url } %>
%p
= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
%p
= _('Your SSH keys with the following fingerprints are scheduled to expire soon:')
%table
%tbody
- @fingerprints.each do |fingerprint|
%tr
%td= fingerprint
%p
- ssh_key_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 %{ssh_key_link_start}SSH keys%{ssh_key_link_end} settings.')) % { ssh_key_link_start: ssh_key_link_start, ssh_key_link_end: '</a>'.html_safe }
...@@ -451,6 +451,14 @@ ...@@ -451,6 +451,14 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: cronjob:ssh_keys_expiring_soon_notification
:feature_category: :compliance_management
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:stuck_ci_jobs - :name: cronjob:stuck_ci_jobs
:feature_category: :continuous_integration :feature_category: :continuous_integration
:has_external_dependencies: :has_external_dependencies:
......
...@@ -17,7 +17,7 @@ module SshKeys ...@@ -17,7 +17,7 @@ module SshKeys
keys = user.expired_today_and_unnotified_keys keys = user.expired_today_and_unnotified_keys
Keys::ExpiryNotificationService.new(user, { keys: keys }).execute Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: false }).execute
end end
end end
end end
......
# frozen_string_literal: true
module SshKeys
class ExpiringSoonNotificationWorker
include ApplicationWorker
include CronjobQueue
feature_category :compliance_management
idempotent!
def perform
return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml)
User.with_ssh_key_expiring_soon.find_each do |user|
with_context(user: user) do
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring soon ssh key(s)"
keys = user.expiring_soon_and_unnotified_keys
Keys::ExpiryNotificationService.new(user, { keys: keys, expiring_soon: true }).execute
end
end
end
end
end
---
title: User notification when SSH key is set to expire soon
merge_request: 58171
author:
type: added
...@@ -569,6 +569,9 @@ Settings.cron_jobs['ssh_keys_expired_notification_worker']['job_class'] = 'SshKe ...@@ -569,6 +569,9 @@ Settings.cron_jobs['ssh_keys_expired_notification_worker']['job_class'] = 'SshKe
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *' Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *'
Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker' Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker'
Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker']['cron'] ||= '0 1 * * *'
Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker']['job_class'] = 'SshKeys::ExpiringSoonNotificationWorker'
Gitlab.com do Gitlab.com do
Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({})
......
# frozen_string_literal: true
class AddBeforeExpiryNotificationDeliveredToKeys < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :keys, :before_expiry_notification_delivered_at, :datetime_with_timezone
end
end
# frozen_string_literal: true
class AddIndexToKeysOnExpiresAtAndBeforeExpiryNotificationUndelivered < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'idx_keys_expires_at_and_before_expiry_notification_undelivered'
disable_ddl_transaction!
def up
add_concurrent_index :keys,
"date(timezone('UTC', expires_at)), before_expiry_notification_delivered_at",
where: 'before_expiry_notification_delivered_at IS NULL', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name(:keys, INDEX_NAME)
end
end
07d527134f776dbed2199f1717c34b3a6c41caadcaa3c50e6e5866f2cfad31b0
\ No newline at end of file
1cd4799ed7df41bfb9d96a7d18faaa9cbb2dc03f2a804c2bc3c1a6bba15d6d3d
\ No newline at end of file
...@@ -13969,7 +13969,8 @@ CREATE TABLE keys ( ...@@ -13969,7 +13969,8 @@ CREATE TABLE keys (
last_used_at timestamp without time zone, last_used_at timestamp without time zone,
fingerprint_sha256 bytea, fingerprint_sha256 bytea,
expires_at timestamp with time zone, expires_at timestamp with time zone,
expiry_notification_delivered_at timestamp with time zone expiry_notification_delivered_at timestamp with time zone,
before_expiry_notification_delivered_at timestamp with time zone
); );
CREATE SEQUENCE keys_id_seq CREATE SEQUENCE keys_id_seq
...@@ -21729,6 +21730,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON jira_connect_s ...@@ -21729,6 +21730,8 @@ CREATE INDEX idx_jira_connect_subscriptions_on_installation_id ON jira_connect_s
CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id); CREATE UNIQUE INDEX idx_jira_connect_subscriptions_on_installation_id_namespace_id ON jira_connect_subscriptions USING btree (jira_connect_installation_id, namespace_id);
CREATE INDEX idx_keys_expires_at_and_before_expiry_notification_undelivered ON keys USING btree (date(timezone('UTC'::text, expires_at)), before_expiry_notification_delivered_at) WHERE (before_expiry_notification_delivered_at IS NULL);
CREATE INDEX idx_members_created_at_user_id_invite_token ON members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL)); CREATE INDEX idx_members_created_at_user_id_invite_token ON members USING btree (created_at) WHERE ((invite_token IS NOT NULL) AND (user_id IS NULL));
CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4)); CREATE INDEX idx_merge_requests_on_id_and_merge_jid ON merge_requests USING btree (id, merge_jid) WHERE ((merge_jid IS NOT NULL) AND (state_id = 4));
...@@ -218,7 +218,8 @@ To use SSH with GitLab, copy your public key to your GitLab account. ...@@ -218,7 +218,8 @@ To use SSH with GitLab, copy your public key to your GitLab account.
The expiration date is informational only, and does not prevent you from using The expiration date is informational only, and does not prevent you from using
the key. However, administrators can view expiration dates and the key. However, administrators can view expiration dates and
use them for guidance when [deleting keys](../user/admin_area/credentials_inventory.md#delete-a-users-ssh-key). use them for guidance when [deleting keys](../user/admin_area/credentials_inventory.md#delete-a-users-ssh-key).
GitLab checks all SSH keys at 02:00 AM UTC every day. It emails an expiration notice for all SSH keys that expire on the current date. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.) - GitLab checks all SSH keys at 02:00 AM UTC every day. It emails an expiration notice for all SSH keys that expire on the current date. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.)
- GitLab checks all SSH keys at 01:00 AM UTC every day. It emails an expiration notice for all SSH keys that are scheduled to expire seven days from now. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/322637) in GitLab 13.11.)
1. Select **Add key**. 1. Select **Add key**.
## Verify that you can connect ## Verify that you can connect
......
...@@ -35640,12 +35640,18 @@ msgstr "" ...@@ -35640,12 +35640,18 @@ msgstr ""
msgid "Your SSH key has expired" msgid "Your SSH key has expired"
msgstr "" msgstr ""
msgid "Your SSH key is expiring soon."
msgstr ""
msgid "Your SSH key was deleted" msgid "Your SSH key was deleted"
msgstr "" msgstr ""
msgid "Your SSH keys (%{count})" msgid "Your SSH keys (%{count})"
msgstr "" msgstr ""
msgid "Your SSH keys with the following fingerprints are scheduled to expire soon:"
msgstr ""
msgid "Your SSH keys with the following fingerprints has expired:" msgid "Your SSH keys with the following fingerprints has expired:"
msgstr "" msgstr ""
......
...@@ -212,52 +212,101 @@ RSpec.describe Emails::Profile do ...@@ -212,52 +212,101 @@ RSpec.describe Emails::Profile do
end end
end end
describe 'notification email for expired ssh key' do describe 'SSH key notification' do
let_it_be(:user) { create(:user) } let_it_be_with_reload(:user) { create(:user) }
let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] } let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] }
context 'when valid' do shared_examples 'is sent to the user' do
subject { Notify.ssh_key_expired_email(user, fingerprints) } it { is_expected.to deliver_to user.email }
end
shared_examples 'has the correct subject' do |subject_text|
it { is_expected.to have_subject subject_text }
end
shared_examples 'has the correct body text' do |body_text|
it { is_expected.to have_body_text body_text }
end
shared_examples 'includes a link to ssh key page' do
it { is_expected.to have_body_text /#{profile_keys_url}/ }
end
shared_examples 'includes the email reason' do
it { is_expected.to have_body_text /You're receiving this email because of your account on localhost/ }
end
shared_examples 'valid use case' do
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'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'is sent to the user'
it_behaves_like 'includes a link to ssh key page'
it_behaves_like 'includes the email reason'
end
it 'is sent to the user' do shared_examples 'does not send email' do
is_expected.to deliver_to user.email it do
expect { subject }.not_to change { ActionMailer::Base.deliveries.count }
end end
end
it 'has the correct subject' do shared_context 'block user' do
is_expected.to have_subject /Your SSH key has expired/ before do
user.block!
end end
end
it 'mentions the ssh keu has expired' do context 'notification email for expired ssh key' do
is_expected.to have_body_text /Your SSH keys with the following fingerprints has expired/ context 'when valid' do
end subject { Notify.ssh_key_expired_email(user, fingerprints) }
it 'includes a link to ssh key page' do include_examples 'valid use case'
is_expected.to have_body_text /#{profile_keys_url}/
it_behaves_like 'has the correct subject', /Your SSH key has expired/
it_behaves_like 'has the correct body text', /Your SSH keys with the following fingerprints has expired/
end end
it 'includes the email reason' do context 'when invalid' do
is_expected.to have_body_text /You're receiving this email because of your account on localhost/ context 'when user does not exist' do
subject { Notify.ssh_key_expired_email(nil, fingerprints) }
it_behaves_like 'does not send email'
end
context 'when user is not active' do
subject { Notify.ssh_key_expired_email(user, fingerprints) }
include_context 'block user'
it_behaves_like 'does not send email'
end
end end
end end
context 'when invalid' do context 'notification email for expiring ssh key' do
context 'when user does not exist' do context 'when valid' do
it do subject { Notify.ssh_key_expiring_soon_email(user, fingerprints) }
expect { Notify.ssh_key_expired_email(nil) }.not_to change { ActionMailer::Base.deliveries.count }
end include_examples 'valid use case'
it_behaves_like 'has the correct subject', /Your SSH key is expiring soon/
it_behaves_like 'has the correct body text', /Your SSH keys with the following fingerprints are scheduled to expire soon/
end end
context 'when user is not active' do context 'when invalid' do
before do context 'when user does not exist' do
user.block! subject { Notify.ssh_key_expiring_soon_email(nil, fingerprints) }
it_behaves_like 'does not send email'
end end
it do context 'when user is not active' do
expect { Notify.ssh_key_expired_email(user) }.not_to change { ActionMailer::Base.deliveries.count } subject { Notify.ssh_key_expiring_soon_email(user, fingerprints) }
include_context 'block user'
it_behaves_like 'does not send email'
end end
end end
end end
......
...@@ -76,15 +76,25 @@ RSpec.describe Key, :mailer do ...@@ -76,15 +76,25 @@ RSpec.describe Key, :mailer do
end end
end end
describe '.expired_today_and_not_notified' do context 'expiration scopes' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user) } let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user) }
let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user, expiry_notification_delivered_at: Time.current) } let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user, expiry_notification_delivered_at: Time.current) }
let_it_be(:expired_yesterday) { create(:key, expires_at: 1.day.ago, user: user) } let_it_be(:expired_yesterday) { create(:key, expires_at: 1.day.ago, user: user) }
let_it_be(:future_expiry) { create(:key, expires_at: 1.day.from_now, user: user) } let_it_be(:expiring_soon_unotified) { create(:key, expires_at: 3.days.from_now, user: user) }
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 4.days.from_now, user: user, before_expiry_notification_delivered_at: Time.current) }
let_it_be(:future_expiry) { create(:key, expires_at: 1.month.from_now, user: user) }
describe '.expired_today_and_not_notified' do
it 'returns keys that expire today' do
expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified)
end
end
it 'returns tokens that have expired today' do describe '.expiring_soon_and_not_notified' do
expect(described_class.expired_today_and_not_notified).to contain_exactly(expired_today_not_notified) it 'returns keys that will expire soon' do
expect(described_class.expiring_soon_and_not_notified).to contain_exactly(expiring_soon_unotified)
end
end end
end end
end end
......
...@@ -1001,15 +1001,24 @@ RSpec.describe User do ...@@ -1001,15 +1001,24 @@ RSpec.describe User do
end end
end end
describe '.with_ssh_key_expired_today' do context 'SSH key expiration scopes' do
let_it_be(:user1) { create(:user) } let_it_be(:user1) { create(:user) }
let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user1) }
let_it_be(:user2) { create(:user) } let_it_be(:user2) { create(:user) }
let_it_be(:expired_today_not_notified) { create(:key, expires_at: Time.current, user: user1) }
let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user2, expiry_notification_delivered_at: Time.current) } let_it_be(:expired_today_already_notified) { create(:key, expires_at: Time.current, user: user2, expiry_notification_delivered_at: Time.current) }
let_it_be(:expiring_soon_not_notified) { create(:key, expires_at: 2.days.from_now, user: user2) }
let_it_be(:expiring_soon_notified) { create(:key, expires_at: 2.days.from_now, user: user1, before_expiry_notification_delivered_at: Time.current) }
it 'returns users whose token has expired today' do describe '.with_ssh_key_expired_today' do
expect(described_class.with_ssh_key_expired_today).to contain_exactly(user1) it 'returns users whose key has expired today' do
expect(described_class.with_ssh_key_expired_today).to contain_exactly(user1)
end
end
describe '.with_ssh_key_expiring_soon' do
it 'returns users whose keys will expire soon' do
expect(described_class.with_ssh_key_expiring_soon).to contain_exactly(user2)
end
end end
end end
......
...@@ -4,48 +4,93 @@ require 'spec_helper' ...@@ -4,48 +4,93 @@ require 'spec_helper'
RSpec.describe Keys::ExpiryNotificationService do RSpec.describe Keys::ExpiryNotificationService do
let_it_be_with_reload(:user) { create(:user) } let_it_be_with_reload(:user) { create(:user) }
let_it_be_with_reload(:expired_key) { create(:key, expires_at: Time.current, user: user) }
let(:params) { { keys: keys } } let(:params) { { keys: user.keys, expiring_soon: expiring_soon } }
subject { described_class.new(user, params) } subject { described_class.new(user, params) }
context 'with expired key', :mailer do shared_examples 'sends a notification' do
let(:keys) { user.keys } it do
it 'sends a notification' do
perform_enqueued_jobs do perform_enqueued_jobs do
subject.execute subject.execute
end end
should_email(user) should_email(user)
end end
end
it 'uses notification service to send email to the user' do shared_examples 'uses notification service to send email to the user' do |notification_method|
it do
expect_next_instance_of(NotificationService) do |notification_service| expect_next_instance_of(NotificationService) do |notification_service|
expect(notification_service).to receive(:ssh_key_expired).with(expired_key.user, [expired_key.fingerprint]) expect(notification_service).to receive(notification_method).with(key.user, [key.fingerprint])
end end
subject.execute subject.execute
end end
end
it 'updates notified column' do shared_examples 'does not send notification' do
expect { subject.execute }.to change { expired_key.reload.expiry_notification_delivered_at } it do
perform_enqueued_jobs do
subject.execute
end
should_not_email(user)
end end
end
context 'when user does not have permission to receive notification' do shared_context 'block user' do
before do before do
user.block! user.block!
end
end
context 'with key expiring today', :mailer do
let_it_be_with_reload(:key) { create(:key, expires_at: Time.current, user: user) }
let(:expiring_soon) { false }
context 'when user has permission to receive notification' do
it_behaves_like 'sends a notification'
it_behaves_like 'uses notification service to send email to the user', :ssh_key_expired
it 'updates notified column' do
expect { subject.execute }.to change { key.reload.expiry_notification_delivered_at }
end end
end
it 'does not send notification' do context 'when user does NOT have permission to receive notification' do
perform_enqueued_jobs do include_context 'block user'
subject.execute
end it_behaves_like 'does not send notification'
should_not_email(user)
it 'does not update notified column' do
expect { subject.execute }.not_to change { key.reload.expiry_notification_delivered_at }
end
end
end
context 'with key expiring soon', :mailer do
let_it_be_with_reload(:key) { create(:key, expires_at: 3.days.from_now, user: user) }
let(:expiring_soon) { true }
context 'when user has permission to receive notification' do
it_behaves_like 'sends a notification'
it_behaves_like 'uses notification service to send email to the user', :ssh_key_expiring_soon
it 'updates notified column' do
expect { subject.execute }.to change { key.reload.before_expiry_notification_delivered_at }
end end
end
context 'when user does NOT have permission to receive notification' do
include_context 'block user'
it_behaves_like 'does not send notification'
it 'does not update notified column' do it 'does not update notified column' do
expect { subject.execute }.not_to change { expired_key.reload.expiry_notification_delivered_at } expect { subject.execute }.not_to change { key.reload.before_expiry_notification_delivered_at }
end end
end end
end end
......
...@@ -288,11 +288,19 @@ RSpec.describe NotificationService, :mailer do ...@@ -288,11 +288,19 @@ RSpec.describe NotificationService, :mailer do
end end
end end
end end
end
describe '#ssh_key_expired' do describe 'SSH Keys' do
let_it_be(:user) { create(:user) } let_it_be_with_reload(:user) { create(:user) }
let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] } let_it_be(:fingerprints) { ["aa:bb:cc:dd:ee:zz"] }
shared_context 'block user' do
before do
user.block!
end
end
describe '#ssh_key_expired' do
subject { notification.ssh_key_expired(user, fingerprints) } subject { notification.ssh_key_expired(user, fingerprints) }
it 'sends email to the token owner' do it 'sends email to the token owner' do
...@@ -300,15 +308,29 @@ RSpec.describe NotificationService, :mailer do ...@@ -300,15 +308,29 @@ RSpec.describe NotificationService, :mailer do
end end
context 'when user is not allowed to receive notifications' do context 'when user is not allowed to receive notifications' do
before do include_context 'block user'
user.block!
end
it 'does not send email to the token owner' do it 'does not send email to the token owner' do
expect { subject }.not_to have_enqueued_email(user, fingerprints, mail: "ssh_key_expired_email") expect { subject }.not_to have_enqueued_email(user, fingerprints, mail: "ssh_key_expired_email")
end end
end end
end end
describe '#ssh_key_expiring_soon' do
subject { notification.ssh_key_expiring_soon(user, fingerprints) }
it 'sends email to the token owner' do
expect { subject }.to have_enqueued_email(user, fingerprints, mail: "ssh_key_expiring_soon_email")
end
context 'when user is not allowed to receive notifications' do
include_context 'block user'
it 'does not send email to the token owner' do
expect { subject }.not_to have_enqueued_email(user, fingerprints, mail: "ssh_key_expiring_soon_email")
end
end
end
end end
describe '#unknown_sign_in' do describe '#unknown_sign_in' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe SshKeys::ExpiringSoonNotificationWorker, type: :worker do
subject(:worker) { described_class.new }
it 'uses a cronjob queue' do
expect(worker.sidekiq_options_hash).to include(
'queue' => 'cronjob:ssh_keys_expiring_soon_notification',
'queue_namespace' => :cronjob
)
end
describe '#perform' do
let_it_be(:user) { create(:user) }
context 'with key expiring soon' do
let_it_be_with_reload(:expiring_soon) { create(:key, expires_at: 6.days.from_now, user: user) }
it 'invoke the notification service' do
expect_next_instance_of(Keys::ExpiryNotificationService) do |expiry_service|
expect(expiry_service).to receive(:execute)
end
worker.perform
end
it 'updates notified column' do
expect { worker.perform }.to change { expiring_soon.reload.before_expiry_notification_delivered_at }
end
include_examples 'an idempotent worker' do
subject do
perform_multiple(worker: worker)
end
end
context 'when feature is not enabled' do
before do
stub_feature_flags(ssh_key_expiration_email_notification: false)
end
it 'does not update notified column' do
expect { worker.perform }.not_to change { expiring_soon.reload.before_expiry_notification_delivered_at }
end
end
end
context 'when key has expired in the past' do
let_it_be(:expired_past) { create(:key, expires_at: 1.day.ago, user: user) }
it 'does not update notified column' do
expect { worker.perform }.not_to change { expired_past.reload.before_expiry_notification_delivered_at }
end
end
context 'when key is not expiring soon' do
let_it_be(:expires_future) { create(:key, expires_at: 8.days.from_now, user: user) }
it 'does not update notified column' do
expect { worker.perform }.not_to change { expires_future.reload.before_expiry_notification_delivered_at }
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