Skip to content
Merged
43 changes: 43 additions & 0 deletions app/jobs/reports/authentication_report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require 'reporting/authentication_report'

module Reports
class AuthenticationReport < BaseReport
REPORT_NAME = 'authentication-report'

attr_accessor :report_date

def perform(report_date)
return unless IdentityConfig.store.s3_reports_enabled
Comment thread
Sgtpluck marked this conversation as resolved.
Outdated

self.report_date = report_date
message = "Report: #{REPORT_NAME} #{report_date}"
subject = "Weekly Authentication Report - #{report_date}"

report_configs.each do |report_hash|
tables = weekly_authentication_report_tables(report_hash['issuers'])

report_hash['emails'].each do |email|
ReportMailer.tables_report(
email:,
subject:,
message:,
tables:,
).deliver_now
end
end
end

private

def weekly_authentication_report_tables(issuers)
Reporting::AuthenticationReport.new(
issuers:,
time_range: report_date.all_week,
).as_tables_with_options
end

def report_configs
IdentityConfig.store.weekly_auth_funnel_report_config
end
end
end
1 change: 1 addition & 0 deletions config/application.yml.default
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ voice_otp_pause_time: '0.5s'
voice_otp_speech_rate: 'slow'
voip_block: true
voip_allowed_phones: '[]'
weekly_auth_funnel_report_config: '[]'

development:
aamva_private_key: 123abc
Expand Down
6 changes: 6 additions & 0 deletions config/initializers/job_configurations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@
statement_timeout: IdentityConfig.store.multi_region_kms_migration_jobs_user_timeout,
},
},
# Send weekly authentication reports to partners
weekly_authentication_report: {
class: 'Reports::AuthenticationReport',
cron: cron_1w,
args: -> { [Time.zone.now] },
},
}.compact
end
# rubocop:enable Metrics/BlockLength
Expand Down
1 change: 1 addition & 0 deletions lib/identity_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ def self.build_store(config_map)
config.add(:voice_otp_speech_rate)
config.add(:voip_allowed_phones, type: :json)
config.add(:voip_block, type: :boolean)
config.add(:weekly_auth_funnel_report_config, type: :json)

@key_types = config.key_types
@store = RedactedStruct.new('IdentityConfig', *config.written_env.keys, keyword_init: true).
Expand Down
126 changes: 75 additions & 51 deletions lib/reporting/authentication_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,58 +54,29 @@ def progress?
@progress
end

# rubocop:disable Metrics/BlockLength
def to_csv
CSV.generate do |csv|
csv << ['Report Timeframe', "#{time_range.begin} to #{time_range.end}"]
csv << ['Report Generated', Date.today.to_s] # rubocop:disable Rails/Date
csv << ['Issuer', issuers.join(', ')]
csv << []
csv << ['Metric', 'Number of accounts', '% of total from start']
csv << [
'New Users Started IAL1 Verification',
email_confirmation,
format_as_percent(numerator: email_confirmation, denominator: email_confirmation),
]

csv << [
'New Users Completed IAL1 Password Setup',
two_fa_setup_visited,
format_as_percent(numerator: two_fa_setup_visited, denominator: email_confirmation),
]

csv << [
'New Users Completed IAL1 MFA',
user_fully_registered,
format_as_percent(numerator: user_fully_registered, denominator: email_confirmation),
]
csv << [
'New IAL1 Users Consented to Partner',
sp_redirect_initiated_new_users,
format_as_percent(
numerator: sp_redirect_initiated_new_users,
denominator: email_confirmation,
),
]
csv << []
csv << ['Total # of IAL1 Users', sp_redirect_initiated_all]
csv << []
csv << [
'AAL2 Authentication Requests from Partner',
oidc_auth_request,
format_as_percent(numerator: oidc_auth_request, denominator: oidc_auth_request),
]
csv << [
'AAL2 Authenticated Requests',
sp_redirect_initiated_after_oidc,
format_as_percent(
numerator: sp_redirect_initiated_after_oidc,
denominator: oidc_auth_request,
),
]
def as_tables
[
overview_table,
funnel_metrics_table,
]
end

def as_tables_with_options
[
[{ title: 'Overview' }, *overview_table],
[{ title: 'Authentication Funnel Metrics' }, *funnel_metrics_table],
]
end

def to_csvs
as_tables.map do |table|
CSV.generate do |csv|
table.each do |row|
csv << row
end
end
Comment thread
Sgtpluck marked this conversation as resolved.
end
end
# rubocop:enable Metrics/BlockLength

# event name => set(user ids)
# @return Hash<String,Set<String>>
Expand Down Expand Up @@ -188,6 +159,57 @@ def cloudwatch_client
)
end

def overview_table
[
['Report Timeframe', "#{time_range.begin} to #{time_range.end}"],
['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date
['Issuer', issuers.join(', ')],
['Total # of IAL1 Users', sp_redirect_initiated_all],
]
end

def funnel_metrics_table
[
['Metric', 'Number of accounts', '% of total from start'],
[
'New Users Started IAL1 Verification',
email_confirmation,
format_as_percent(numerator: email_confirmation, denominator: email_confirmation),
],
[
'New Users Completed IAL1 Password Setup',
two_fa_setup_visited,
format_as_percent(numerator: two_fa_setup_visited, denominator: email_confirmation),
],
[
'New Users Completed IAL1 MFA',
user_fully_registered,
format_as_percent(numerator: user_fully_registered, denominator: email_confirmation),
],
[
'New IAL1 Users Consented to Partner',
sp_redirect_initiated_new_users,
format_as_percent(
numerator: sp_redirect_initiated_new_users,
denominator: email_confirmation,
),
],
[
'AAL2 Authentication Requests from Partner',
oidc_auth_request,
format_as_percent(numerator: oidc_auth_request, denominator: oidc_auth_request),
],
[
'AAL2 Authenticated Requests',
sp_redirect_initiated_after_oidc,
format_as_percent(
numerator: sp_redirect_initiated_after_oidc,
denominator: oidc_auth_request,
),
],
]
end

# @return [String]
def format_as_percent(numerator:, denominator:)
(100 * numerator.to_f / denominator.to_f).round(2).to_s + '%'
Expand All @@ -199,6 +221,8 @@ def format_as_percent(numerator:, denominator:)
if __FILE__ == $PROGRAM_NAME
options = Reporting::CommandLineOptions.new.parse!(ARGV)

puts Reporting::AuthenticationReport.new(**options).to_csv
Reporting::AuthenticationReport.new(**options).to_csvs.each do |csv|
puts csv
end
end
# rubocop:enable Rails/Output
70 changes: 70 additions & 0 deletions spec/jobs/reports/authentication_report_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require 'rails_helper'

RSpec.describe Reports::AuthenticationReport do
let(:issuer) { 'issuer1' }
let(:issuers) { [issuer] }
let(:report_date) { Date.new(2023, 12, 25) }
let(:email) { 'partner.name@example.com' }
let(:name) { 'Partner Name' }

let(:report_configs) do
[
{
'name' => name,
'issuers' => issuers,
'emails' => [email],
},
]
end

before do
allow(IdentityConfig.store).to receive(:s3_reports_enabled).and_return(true)
allow(IdentityConfig.store).to receive(:weekly_auth_funnel_report_config) { report_configs }
end

describe '#perform' do
let(:tables) do
[
[
{ title: 'Overview' },
['Report Timeframe', '2023-10-01 00:00:00 UTC to 2023-10-01 23:59:59 UTC'],
['Report Generated', '2023-10-02'],
['Issuer', 'some:issuer'],
['Total # of IAL1 Users', '75'],
],
[
{ title: 'Authentication Metrics Report' },
['Metric', 'Number of accounts', '% of total from start'],
['New Users Started IAL1 Verification', '100', '100%'],
['New Users Completed IAL1 Password Setup', '85', '85%'],
['New Users Completed IAL1 MFA', '80', '80%'],
['New IAL1 Users Consented to Partner', '75', '75%'],
['AAL2 Authentication Requests from Partner', '12', '12%'],
['AAL2 Authenticated Requests', '50', '50%'],
],
]
end

let(:weekly_report) { double(Reporting::AuthenticationReport, as_tables_with_options: tables) }

before do
expect(Reporting::AuthenticationReport).to receive(:new).with(
issuers:,
time_range: report_date.all_week,
) { weekly_report }

allow(ReportMailer).to receive(:tables_report).and_call_original
end

it 'emails the csv' do
expect(ReportMailer).to receive(:tables_report).with(
email:,
subject: "Weekly Authentication Report - #{report_date}",
message: "Report: authentication-report #{report_date}",
tables:,
)

subject.perform(report_date)
end
end
end
61 changes: 42 additions & 19 deletions spec/lib/reporting/authentication_report_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,32 @@
allow(report).to receive(:cloudwatch_client).and_return(cloudwatch_client)
end

describe '#to_csv' do
describe '#as_tables' do
it 'generates the tabular csv data' do
expect(report.as_tables).to eq expected_tables
end
end

describe '#as_tables_with_names' do
it 'adds a "first row" hash with a title for tables_report mailer' do
tables = report.as_tables_with_options
aggregate_failures do
tables.each do |table|
expect(table[0][:title]).to_not be nil
end
end
end
end

describe '#to_csvs' do
it 'generates a csv' do
csv = CSV.parse(report.to_csv, headers: false)
csv_string_list = report.to_csvs
expect(csv_string_list.count).to be 2

expected_csv = [
['Report Timeframe', "#{time_range.begin} to #{time_range.end}"],
['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date
['Issuer', issuer],
[],
['Metric', 'Number of accounts', '% of total from start'],
['New Users Started IAL1 Verification', '4', '100.0%'],
['New Users Completed IAL1 Password Setup', '3', '75.0%'],
['New Users Completed IAL1 MFA', '2', '50.0%'],
['New IAL1 Users Consented to Partner', '1', '25.0%'],
[],
['Total # of IAL1 Users', '2'],
[],
['AAL2 Authentication Requests from Partner', '5', '100.0%'],
['AAL2 Authenticated Requests', '2', '40.0%'],
]
csvs = csv_string_list.map { |csv| CSV.parse(csv) }

aggregate_failures do
csv.map(&:to_a).zip(expected_csv).each do |actual, expected|
csvs.map(&:to_a).zip(expected_tables(strings: true)).each do |actual, expected|
expect(actual).to eq(expected)
end
end
Expand Down Expand Up @@ -141,4 +144,24 @@
end
end
end

def expected_tables(strings: false)
[
[
['Report Timeframe', "#{time_range.begin} to #{time_range.end}"],
['Report Generated', Date.today.to_s], # rubocop:disable Rails/Date
['Issuer', issuer],
['Total # of IAL1 Users', strings ? '2' : 2],
],
[
['Metric', 'Number of accounts', '% of total from start'],
['New Users Started IAL1 Verification', strings ? '4' : 4, '100.0%'],
['New Users Completed IAL1 Password Setup', strings ? '3' : 3, '75.0%'],
['New Users Completed IAL1 MFA', strings ? '2' : 2, '50.0%'],
['New IAL1 Users Consented to Partner', strings ? '1' : 1, '25.0%'],
['AAL2 Authentication Requests from Partner', strings ? '5' : 5, '100.0%'],
['AAL2 Authenticated Requests', strings ? '2' : 2, '40.0%'],
],
]
end
end