From 349a67bee60aea8a6e6a1bb924b13d7e7b954a9b Mon Sep 17 00:00:00 2001 From: Krzysztof Kotlarek Date: Wed, 27 May 2020 08:13:47 +1000 Subject: [PATCH] FEATURE: notify admins about old credentials (#9854) * FEATURE: notify admins about old credentials Security and API keys should be renewed periodically. This additional notification should help admins keep their Discourse safe and secure. --- app/jobs/scheduled/old_keys_reminder.rb | 62 +++++++++++++++++++++++++ config/locales/server.en.yml | 10 ++++ config/site_settings.yml | 8 ++++ spec/jobs/old_keys_reminder_spec.rb | 55 ++++++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 app/jobs/scheduled/old_keys_reminder.rb create mode 100644 spec/jobs/old_keys_reminder_spec.rb diff --git a/app/jobs/scheduled/old_keys_reminder.rb b/app/jobs/scheduled/old_keys_reminder.rb new file mode 100644 index 00000000000..4355a043661 --- /dev/null +++ b/app/jobs/scheduled/old_keys_reminder.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module Jobs + class OldKeysReminder < ::Jobs::Scheduled + every 1.month + + def execute(_args) + return if SiteSetting.notify_about_secrets_older_than == 'never' + return if old_site_settings_keys.blank? && old_api_keys.blank? + admins.each do |admin| + PostCreator.create!( + Discourse.system_user, + title: title, + raw: body, + archetype: Archetype.private_message, + target_usernames: admin.username, + validate: false + ) + end + end + + private + + def old_site_settings_keys + @old_site_settings_keys ||= SiteSetting.secret_settings.each_with_object([]) do |secret_name, old_keys| + site_setting = SiteSetting.find_by(name: secret_name) + next if site_setting&.value.blank? + next if site_setting.updated_at + calculate_period > Time.zone.now + old_keys << site_setting + end.sort_by { |key| key.updated_at } + end + + def old_api_keys + @old_api_keys ||= ApiKey.all.order(created_at: :asc).each_with_object([]) do |api_key, old_keys| + next if api_key.created_at + calculate_period > Time.zone.now + old_keys << api_key + end + end + + def calculate_period + SiteSetting.notify_about_secrets_older_than.to_i.years + end + + def admins + User.real.admins + end + + def title + I18n.t('old_keys_reminder.title', keys_count: old_site_settings_keys.count + old_api_keys.count) + end + + def body + I18n.t('old_keys_reminder.body', keys: keys_list) + end + + def keys_list + messages = old_site_settings_keys.map { |key| "#{key.name} - #{key.updated_at}" } + old_api_keys.each_with_object(messages) { |key, array| array << "#{[key.description, key.user&.username, key.created_at].compact.join(" - ")}" } + messages.join("\n") + end + end +end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0433fd08b58..e9918e2f494 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1583,6 +1583,7 @@ en: moderators_view_emails: "Allow moderators to view user emails" prioritize_username_in_ux: "Show username first on user page, user card and posts (when disabled name is shown first)" enable_rich_text_paste: "Enable automatic HTML to Markdown conversion when pasting text into the composer. (Experimental)" + notify_about_secrets_older_than: "Notify about credentials older than" email_token_valid_hours: "Forgot password / activate account tokens are valid for (n) hours." @@ -4817,3 +4818,12 @@ en: discord: not_in_allowed_guild: "Authentication failed. You are not a member of a permitted Discord guild." + + old_keys_reminder: + title: "You have %{keys_count} old credentials" + body: | + We have detected you have the following credentials that are 2 years or older + + %{keys} + + These credentials are sensitive and we recommend resetting them every 2 years to avoid impact of any future data breaches diff --git a/config/site_settings.yml b/config/site_settings.yml index cfeb71108e7..265a9977a55 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1458,6 +1458,14 @@ security: allow_embedding_site_in_an_iframe: default: false hidden: true + notify_about_secrets_older_than: + default: "2 years" + type: enum + choices: + - never + - "1 year" + - "2 years" + - "3 years" onebox: enable_flash_video_onebox: false diff --git a/spec/jobs/old_keys_reminder_spec.rb b/spec/jobs/old_keys_reminder_spec.rb new file mode 100644 index 00000000000..850aea2cd63 --- /dev/null +++ b/spec/jobs/old_keys_reminder_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "rails_helper" + +describe Jobs::OldKeysReminder do + let!(:google_secret) { SiteSetting.create!(name: 'google_oauth2_client_secret', value: '123', data_type: 1) } + let!(:instagram_secret) { SiteSetting.create!(name: 'instagram_consumer_secret', value: '123', data_type: 1) } + let!(:api_key) { Fabricate(:api_key, description: 'api key description') } + let!(:admin) { Fabricate(:admin) } + + let!(:recent_twitter_secret) { SiteSetting.create!(name: 'twitter_consumer_secret', value: '123', data_type: 1, updated_at: 2.years.from_now) } + let!(:recent_api_key) { Fabricate(:api_key, description: 'recent api key description', created_at: 2.years.from_now) } + + it 'sends message to admin with old credentials' do + freeze_time 2.years.from_now + expect { described_class.new.execute({}) }.to change { Post.count }.by(1) + post = Post.last + expect(post.archetype).to eq(Archetype.private_message) + expect(post.topic.topic_allowed_users.map(&:user_id).sort).to eq([Discourse.system_user.id, admin.id].sort) + expect(post.topic.title).to eq("You have 3 old credentials") + expect(post.raw).to eq(<<-MSG.rstrip) +We have detected you have the following credentials that are 2 years or older + +google_oauth2_client_secret - #{google_secret.updated_at} +instagram_consumer_secret - #{instagram_secret.updated_at} +api key description - #{api_key.created_at} + +These credentials are sensitive and we recommend resetting them every 2 years to avoid impact of any future data breaches + MSG + + freeze_time 4.years.from_now + described_class.new.execute({}) + post = Post.last + expect(post.topic.title).to eq("You have 5 old credentials") + expect(post.raw).to eq(<<-MSG.rstrip) +We have detected you have the following credentials that are 2 years or older + +google_oauth2_client_secret - #{google_secret.updated_at} +instagram_consumer_secret - #{instagram_secret.updated_at} +twitter_consumer_secret - #{recent_twitter_secret.updated_at} +api key description - #{api_key.created_at} +recent api key description - #{recent_api_key.created_at} + +These credentials are sensitive and we recommend resetting them every 2 years to avoid impact of any future data breaches + MSG + end + + it 'does not send message when notification set to never or no old keys' do + SiteSetting.notify_about_secrets_older_than = "never" + freeze_time 2.years.from_now + expect { described_class.new.execute({}) }.to change { Post.count }.by(0) + SiteSetting.notify_about_secrets_older_than = "3 years" + expect { described_class.new.execute({}) }.to change { Post.count }.by(0) + end +end