diff --git a/Gemfile b/Gemfile index 06adfaf4e08..a09f4f2f93e 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,10 @@ gem 'autoprefixer-rails', '~> 10.0' gem 'aws-sdk-kms', '~> 1.4' gem 'aws-sdk-lambda' gem 'aws-sdk-ses', '~> 1.6' +gem 'aws-sdk-sqs' gem 'base32-crockford' +gem 'daemons', '~> 1.3' +gem 'delayed_job_active_record', '~> 4.1' gem 'device_detector' gem 'devise', '~> 4.7.2' gem 'dotiw', '>= 4.0.1' @@ -32,6 +35,7 @@ gem 'jwt' gem 'local_time' gem 'lograge', '>= 0.11.2' gem 'maxminddb' +gem 'mimemagic', '0.3.5', github: 'mimemagicrb/mimemagic', ref: '40dd02bb6b442535f97c35326c0383bc67' gem 'net-sftp' gem 'newrelic_rpm' gem 'pg' @@ -120,5 +124,5 @@ end group :production do gem 'aamva', github: '18F/identity-aamva-api-client-gem', tag: 'v4.1.0' - gem 'lexisnexis', github: '18F/identity-lexisnexis-api-client-gem', tag: 'v3.1.0' + gem 'lexisnexis', github: '18F/identity-lexisnexis-api-client-gem', tag: 'v3.1.1' end diff --git a/Gemfile.lock b/Gemfile.lock index e3e5c55de59..0b7deaee845 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,10 +29,10 @@ GIT GIT remote: https://github.com/18F/identity-idp-functions.git - revision: 051a8c3dca143b215838176a07b1f0545ffec0a2 - ref: 051a8c3dca143b215838176a07b1f0545ffec0a2 + revision: d9241bdfea85a76c170e456a89ec6601549f4c4a + ref: d9241bdfea85a76c170e456a89ec6601549f4c4a specs: - identity-idp-functions (0.15.0) + identity-idp-functions (0.15.2) aamva (>= 4.0.0) aws-sdk-s3 (>= 1.73) aws-sdk-ssm (>= 1.55) @@ -41,10 +41,10 @@ GIT GIT remote: https://github.com/18F/identity-lexisnexis-api-client-gem.git - revision: d6e73358ab899e8740e0008aff0e03e26bd4eb56 - tag: v3.1.0 + revision: 0e22ac2518a724b63a928feb68197b203ea47660 + tag: v3.1.1 specs: - lexisnexis (3.1.0) + lexisnexis (3.1.1) activesupport faraday @@ -94,6 +94,13 @@ GIT aws-sdk-pinpointsmsvoice i18n +GIT + remote: https://github.com/mimemagicrb/mimemagic.git + revision: 40dd02bb6b442535f97c35326c0383bc67146ac4 + ref: 40dd02bb6b442535f97c35326c0383bc67 + specs: + mimemagic (0.3.5) + GEM remote: https://rubygems.org/ specs: @@ -199,6 +206,9 @@ GEM aws-sdk-ses (1.36.0) aws-sdk-core (~> 3, >= 3.109.0) aws-sigv4 (~> 1.1) + aws-sdk-sqs (1.35.0) + aws-sdk-core (~> 3, >= 3.109.0) + aws-sigv4 (~> 1.1) aws-sdk-ssm (1.103.0) aws-sdk-core (~> 3, >= 3.109.0) aws-sigv4 (~> 1.1) @@ -269,7 +279,13 @@ GEM crass (1.0.6) css_parser (1.7.1) addressable + daemons (1.3.1) debug_inspector (0.0.3) + delayed_job (4.1.9) + activesupport (>= 3.0, < 6.2) + delayed_job_active_record (4.1.5) + activerecord (>= 3.0, < 6.2) + delayed_job (>= 3.0, < 5) derailed_benchmarks (1.8.1) benchmark-ips (~> 2) get_process_mem (~> 0) @@ -418,7 +434,6 @@ GEM mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2021.0225) - mimemagic (0.3.5) mini_histogram (0.3.1) mini_mime (1.0.2) mini_portile2 (2.5.0) @@ -432,7 +447,7 @@ GEM net-ssh (5.2.0) newrelic_rpm (6.14.0) nio4r (2.5.5) - nokogiri (1.11.1) + nokogiri (1.11.2) mini_portile2 (~> 2.5.0) racc (~> 1.4) notiffany (0.1.3) @@ -738,6 +753,7 @@ DEPENDENCIES aws-sdk-kms (~> 1.4) aws-sdk-lambda aws-sdk-ses (~> 1.6) + aws-sdk-sqs axe-matchers (~> 2.6.0) base32-crockford better_errors (>= 2.5.1) @@ -748,6 +764,8 @@ DEPENDENCIES capybara-screenshot (>= 1.0.23) capybara-selenium (>= 0.0.6) codeclimate-test-reporter + daemons (~> 1.3) + delayed_job_active_record (~> 4.1) derailed_benchmarks (~> 1.8) device_detector devise (~> 4.7.2) @@ -778,6 +796,7 @@ DEPENDENCIES local_time lograge (>= 0.11.2) maxminddb + mimemagic (= 0.3.5)! net-sftp newrelic_rpm nokogiri (~> 1.11.0) diff --git a/app/assets/stylesheets/components/_btn.scss b/app/assets/stylesheets/components/_btn.scss index 054a7132a79..352a4d16bc5 100644 --- a/app/assets/stylesheets/components/_btn.scss +++ b/app/assets/stylesheets/components/_btn.scss @@ -111,3 +111,30 @@ text-decoration: none; } } + +.usa-button.usa-button--unstyled:visited { + // Temporary: Links in the IdP do not currently conform to the design system and instead retain + // their color even if visited. Part of the work of LG-3877 should be to remove these styles, and + // instead allow both unstyled buttons and links inherit the default design system visited color. + // Alternatively, consider removing unstyled button classes from links, since the intention of an + // unstyled button is to take the visual appearance of a link. + color: $link-color; +} + +.usa-button.usa-button--unstyled:hover, +.usa-button.usa-button--hover.usa-button--unstyled { + // Temporary: Links in the IdP do not currently conform to the design system and instead retain + // their color while hovered. Part of the work of LG-3877 should be to remove these styles, and + // instead allow both unstyled buttons and links inherit the default design system hover color. + color: $link-color; +} + +.usa-button--unstyled { + &:hover, + &:active { + // Temporary: These styles should be ported upstream to the design system, optionally as part of + // future reconciliation effort with uswds/uswds#4077. + -moz-osx-font-smoothing: inherit; + -webkit-font-smoothing: inherit; + } +} diff --git a/app/controllers/account_reset/recover_controller.rb b/app/controllers/account_reset/recover_controller.rb index 76ac54170af..7548e115f0c 100644 --- a/app/controllers/account_reset/recover_controller.rb +++ b/app/controllers/account_reset/recover_controller.rb @@ -22,7 +22,7 @@ def send_notifications current_user.confirmed_email_addresses.each do |email_address| UserMailer.confirm_email_and_reverify(current_user, email_address, - current_user.account_recovery_request).deliver_later + current_user.account_recovery_request).deliver_now end end diff --git a/app/controllers/concerns/two_factor_authenticatable_methods.rb b/app/controllers/concerns/two_factor_authenticatable_methods.rb index 045ecad7c80..336ce5a79d8 100644 --- a/app/controllers/concerns/two_factor_authenticatable_methods.rb +++ b/app/controllers/concerns/two_factor_authenticatable_methods.rb @@ -178,7 +178,7 @@ def send_phone_added_email event = create_user_event_with_disavowal(:phone_added, current_user) current_user.confirmed_email_addresses.each do |email_address| UserMailer.phone_added(current_user, email_address, disavowal_token: event.disavowal_token). - deliver_later + deliver_now end end diff --git a/app/controllers/idv/usps_controller.rb b/app/controllers/idv/usps_controller.rb index beaa89e84e6..0ffdd7bf07b 100644 --- a/app/controllers/idv/usps_controller.rb +++ b/app/controllers/idv/usps_controller.rb @@ -168,7 +168,7 @@ def error_message def send_reminder current_user.confirmed_email_addresses.each do |email_address| - UserMailer.letter_reminder(current_user, email_address.email).deliver_later + UserMailer.letter_reminder(current_user, email_address.email).deliver_now end end diff --git a/app/controllers/users/email_confirmations_controller.rb b/app/controllers/users/email_confirmations_controller.rb index f53ad220814..775941cf380 100644 --- a/app/controllers/users/email_confirmations_controller.rb +++ b/app/controllers/users/email_confirmations_controller.rb @@ -40,7 +40,7 @@ def process_successful_confirmation(email_address) def confirm_and_notify_user(email_address) email_address.update!(confirmed_at: Time.zone.now) email_address.user.confirmed_email_addresses.each do |confirmed_email_address| - UserMailer.email_added(email_address.user, confirmed_email_address.email).deliver_later + UserMailer.email_added(email_address.user, confirmed_email_address.email).deliver_now end end diff --git a/app/controllers/users/emails_controller.rb b/app/controllers/users/emails_controller.rb index 5ecd3453dea..c8837bc130a 100644 --- a/app/controllers/users/emails_controller.rb +++ b/app/controllers/users/emails_controller.rb @@ -100,7 +100,7 @@ def retain_confirmed_emails def send_delete_email_notification @current_confirmed_emails.each do |confirmed_email| - UserMailer.email_deleted(current_user, confirmed_email).deliver_later + UserMailer.email_deleted(current_user, confirmed_email).deliver_now end end end diff --git a/app/controllers/users/piv_cac_authentication_setup_controller.rb b/app/controllers/users/piv_cac_authentication_setup_controller.rb index f7d3f7f41ec..8d6fc3f754a 100644 --- a/app/controllers/users/piv_cac_authentication_setup_controller.rb +++ b/app/controllers/users/piv_cac_authentication_setup_controller.rb @@ -15,9 +15,6 @@ class PivCacAuthenticationSetupController < ApplicationController def new if params.key?(:token) process_piv_cac_setup - # this branch is deprecated, remove it - elsif flash[:error_type].present? - render_error else render_prompt end @@ -74,16 +71,6 @@ def piv_cac_service_url_with_redirect ) end - def render_error - @presenter = PivCacErrorPresenter.new( - error: flash[:error_type], - view: view_context, - try_again_url: setup_piv_cac_url, - ) - - render :error - end - def process_piv_cac_setup result = user_piv_cac_form.submit analytics.track_event(Analytics::MULTI_FACTOR_AUTH_SETUP, result.to_h) diff --git a/app/controllers/users/verify_account_controller.rb b/app/controllers/users/verify_account_controller.rb index 500972a7f04..552d116eec0 100644 --- a/app/controllers/users/verify_account_controller.rb +++ b/app/controllers/users/verify_account_controller.rb @@ -10,22 +10,36 @@ def index usps_mail = Idv::UspsMail.new(current_user) @mail_spammed = usps_mail.mail_spammed? @verify_account_form = VerifyAccountForm.new(user: current_user) - return unless FeatureManagement.reveal_usps_code? - @code = session[:last_usps_confirmation_code] + @code = session[:last_usps_confirmation_code] if FeatureManagement.reveal_usps_code? + + if Throttler::IsThrottled.call(current_user.id, :verify_gpo_key) + render :throttled + else + render :index + end end def create @verify_account_form = build_verify_account_form - result = @verify_account_form.submit - analytics.track_event(Analytics::ACCOUNT_VERIFICATION_SUBMITTED, result.to_h) + throttled = Throttler::IsThrottledElseIncrement.call( + current_user.id, + :verify_gpo_key, + ) - if result.success? - create_user_event(:account_verified) - flash[:success] = t('account.index.verification.success') - redirect_to sign_up_completed_url + if throttled + render :throttled else - render :index + result = @verify_account_form.submit + analytics.track_event(Analytics::ACCOUNT_VERIFICATION_SUBMITTED, result.to_h) + + if result.success? + create_user_event(:account_verified) + flash[:success] = t('account.index.verification.success') + redirect_to sign_up_completed_url + else + render :index + end end end diff --git a/app/controllers/users/verify_personal_key_controller.rb b/app/controllers/users/verify_personal_key_controller.rb index 853107688e4..c551ac77c01 100644 --- a/app/controllers/users/verify_personal_key_controller.rb +++ b/app/controllers/users/verify_personal_key_controller.rb @@ -12,15 +12,31 @@ def new user: current_user, personal_key: '', ) + + if Throttler::IsThrottled.call(current_user.id, :verify_personal_key) + render :throttled + else + render :new + end end def create - result = personal_key_form.submit - analytics.track_event(Analytics::PERSONAL_KEY_REACTIVATION_SUBMITTED, result.to_h) - if result.success? - handle_success(result) + throttled = Throttler::IsThrottledElseIncrement.call( + current_user.id, + :verify_personal_key, + ) + + if throttled + render :throttled else - handle_failure(result) + result = personal_key_form.submit + + analytics.track_event(Analytics::PERSONAL_KEY_REACTIVATION_SUBMITTED, result.to_h) + if result.success? + handle_success(result) + else + handle_failure(result) + end end end diff --git a/app/forms/idv/api_image_upload_form.rb b/app/forms/idv/api_image_upload_form.rb index dd166484310..59dbcda55f7 100644 --- a/app/forms/idv/api_image_upload_form.rb +++ b/app/forms/idv/api_image_upload_form.rb @@ -21,7 +21,7 @@ def initialize(params, liveness_checking_enabled:, issuer:, analytics: nil) def submit throttled_else_increment - @form_response = validate_form + form_response = validate_form client_response = nil doc_pii_response = nil @@ -29,11 +29,7 @@ def submit if form_response.success? client_response = post_images_to_client - if client_response.success? - doc_pii_response = validate_pii_from_doc(client_response) - else - client_response = client_response.merge(form_response) - end + doc_pii_response = validate_pii_from_doc(client_response) if client_response.success? end return determine_response( @@ -59,10 +55,7 @@ def validate_form response = Idv::DocAuthFormResponse.new( success: valid?, errors: errors.messages, - extra: { - remaining_attempts: remaining_attempts, - user_id: user_uuid, - }, + extra: extra_attributes, ) track_event( @@ -80,6 +73,7 @@ def post_images_to_client selfie_image: selfie&.read, liveness_checking_enabled: liveness_checking_enabled?, ) + response = response.merge(extra_attributes_response) update_analytics(response) @@ -88,14 +82,30 @@ def post_images_to_client def validate_pii_from_doc(client_response) response = Idv::DocPiiForm.new(client_response.pii_from_doc).submit + response = response.merge(extra_attributes_response) + track_event( Analytics::IDV_DOC_AUTH_SUBMITTED_PII_VALIDATION, - response.to_h.merge(user_id: user_uuid), + response.to_h, ) store_pii(client_response) if client_response.success? && response.success? - # merge in the image_form_response to pick up the remaining_attempts - response.merge(form_response) + response + end + + def extra_attributes_response + @extra_attributes_response ||= Idv::DocAuthFormResponse.new( + success: true, + errors: {}, + extra: extra_attributes, + ) + end + + def extra_attributes + @extra_attributes ||= { + remaining_attempts: remaining_attempts, + user_id: user_uuid, + } end def remaining_attempts @@ -186,7 +196,7 @@ def update_analytics(client_response) update_funnel(client_response) track_event( Analytics::IDV_DOC_AUTH_SUBMITTED_IMAGE_UPLOAD_VENDOR, - client_response.to_h.merge(user_id: user_uuid), + client_response.to_h, ) end diff --git a/app/forms/register_user_email_form.rb b/app/forms/register_user_email_form.rb index 0870b995bde..c28b47de984 100644 --- a/app/forms/register_user_email_form.rb +++ b/app/forms/register_user_email_form.rb @@ -128,7 +128,7 @@ def send_sign_up_unconfirmed_email(request_id) def send_sign_up_confirmed_email @throttled = Throttler::IsThrottledElseIncrement.call(existing_user.id, :reg_confirmed_email) - UserMailer.signup_with_your_email(existing_user, email).deliver_later unless @throttled + UserMailer.signup_with_your_email(existing_user, email).deliver_now unless @throttled end def user_unconfirmed? diff --git a/app/javascript/packages/document-capture/components/acuant-capture.jsx b/app/javascript/packages/document-capture/components/acuant-capture.jsx index 1bf5badbf2e..0a94998680b 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.jsx @@ -363,7 +363,7 @@ function AcuantCapture(
{isMobile && (
<% end %> diff --git a/app/views/accounts/actions/_manage_personal_key.html.erb b/app/views/accounts/actions/_manage_personal_key.html.erb index 02121680783..e4f593bcc06 100644 --- a/app/views/accounts/actions/_manage_personal_key.html.erb +++ b/app/views/accounts/actions/_manage_personal_key.html.erb @@ -1,7 +1,7 @@ <%= button_to( create_new_personal_key_url, method: :post, - class: 'btn btn-link margin-left-1', + class: 'usa-button usa-button--unstyled margin-left-1', form_class: 'inline-block', ) do %> diff --git a/app/views/idv/cac/_start_over_or_cancel.html.erb b/app/views/idv/cac/_start_over_or_cancel.html.erb index 6015c2d0477..b047a04edcc 100644 --- a/app/views/idv/cac/_start_over_or_cancel.html.erb +++ b/app/views/idv/cac/_start_over_or_cancel.html.erb @@ -1,4 +1,4 @@
<%= button_to(t('doc_auth.buttons.start_over'), idv_cac_step_path(:reset), method: :put, - class: 'btn btn-link', form_class: 'inline-block') %> + class: 'usa-button usa-button--unstyled', form_class: 'inline-block') %> <%= render 'shared/cancel', link: idv_cancel_path %> diff --git a/app/views/idv/cac/verify.html.erb b/app/views/idv/cac/verify.html.erb index 4ff6795e3f0..71b47d821b2 100644 --- a/app/views/idv/cac/verify.html.erb +++ b/app/views/idv/cac/verify.html.erb @@ -22,7 +22,7 @@
<%= button_to(t('in_person_proofing.buttons.change_address'), - idv_cac_step_path(step: :redo_enter_info), method: :put, class: 'btn btn-link') %> + idv_cac_step_path(step: :redo_enter_info), method: :put, class: 'usa-button usa-button--unstyled') %>
<%= "#{t('in_person_proofing.forms.address1')}: #{flow_session[:pii_from_doc][:address1]}" %> @@ -39,7 +39,7 @@
<%= button_to(t('in_person_proofing.buttons.change_ssn'), - idv_cac_step_path(step: :redo_enter_info), method: :put, class: 'btn btn-link') %> + idv_cac_step_path(step: :redo_enter_info), method: :put, class: 'usa-button usa-button--unstyled') %>
<%= "#{t('in_person_proofing.forms.dob')}: #{flow_session[:pii_from_doc][:dob]}" %> diff --git a/app/views/idv/doc_auth/_back.html.erb b/app/views/idv/doc_auth/_back.html.erb index 82823efec88..2f1cd0745aa 100644 --- a/app/views/idv/doc_auth/_back.html.erb +++ b/app/views/idv/doc_auth/_back.html.erb @@ -24,7 +24,7 @@ classes << local_assigns[:class] if local_assigns[:class] text, path, method: :put, - class: [*classes, 'btn btn-link'] + class: [*classes, 'usa-button usa-button--unstyled'] ) %> <% else %> <%= link_to(text, path, class: classes) %> diff --git a/app/views/idv/doc_auth/_start_over.html.erb b/app/views/idv/doc_auth/_start_over.html.erb index 45f1417e7cd..a2b5c6ae3ef 100644 --- a/app/views/idv/doc_auth/_start_over.html.erb +++ b/app/views/idv/doc_auth/_start_over.html.erb @@ -2,6 +2,6 @@ t('doc_auth.buttons.start_over'), idv_doc_auth_step_path(step: :reset), method: :put, - class: 'btn btn-link', + class: 'usa-button usa-button--unstyled', form_class: 'inline-block margin-top-4' ) %> diff --git a/app/views/idv/doc_auth/upload.html.erb b/app/views/idv/doc_auth/upload.html.erb index 250b15a2eb8..7870feac198 100644 --- a/app/views/idv/doc_auth/upload.html.erb +++ b/app/views/idv/doc_auth/upload.html.erb @@ -92,7 +92,7 @@ method: 'PUT', html: { autocomplete: 'off', role: 'form', class: 'inline' } ) do %> - <% end %> diff --git a/app/views/idv/doc_auth/verify.html.erb b/app/views/idv/doc_auth/verify.html.erb index c04e329f7ed..004a1bb71e9 100644 --- a/app/views/idv/doc_auth/verify.html.erb +++ b/app/views/idv/doc_auth/verify.html.erb @@ -37,7 +37,7 @@ t('doc_auth.buttons.change_ssn'), idv_doc_auth_step_path(step: :redo_ssn), method: :put, - class: 'btn btn-link', + class: 'usa-button usa-button--unstyled', ) %>
<%= t('doc_auth.forms.ssn') %> diff --git a/app/views/idv/in_person/_start_over_or_cancel.html.erb b/app/views/idv/in_person/_start_over_or_cancel.html.erb index e513b14faef..e4a810b180b 100644 --- a/app/views/idv/in_person/_start_over_or_cancel.html.erb +++ b/app/views/idv/in_person/_start_over_or_cancel.html.erb @@ -1,4 +1,4 @@
<%= button_to(t('doc_auth.buttons.start_over'), idv_in_person_step_path(:reset), method: :put, - class: 'btn btn-link', form_class: 'inline-block') %> + class: 'usa-button usa-button--unstyled', form_class: 'inline-block') %> <%= render 'shared/cancel', link: idv_cancel_path %> diff --git a/app/views/idv/in_person/encrypt.html.erb b/app/views/idv/in_person/encrypt.html.erb index 6cf4114ded0..7b96cad918e 100644 --- a/app/views/idv/in_person/encrypt.html.erb +++ b/app/views/idv/in_person/encrypt.html.erb @@ -24,7 +24,7 @@
<%= t('idv.forgot_password.link_html', link: link_to(t('idv.forgot_password.link_text'), idv_forgot_password_url, - class: 'btn btn-link margin-left-1', form_class: 'inline-block')) %> + class: 'usa-button usa-button--unstyled margin-left-1', form_class: 'inline-block')) %>
<%= f.button :submit, t('forms.buttons.continue'), class: 'btn btn-primary btn-wide sm-col-6 col-12' %> diff --git a/app/views/idv/in_person/verify.html.erb b/app/views/idv/in_person/verify.html.erb index e66445e8dd2..b794b472891 100644 --- a/app/views/idv/in_person/verify.html.erb +++ b/app/views/idv/in_person/verify.html.erb @@ -46,7 +46,7 @@
<%= button_to(t('in_person_proofing.buttons.change_ssn'), - idv_doc_auth_step_path(step: :redo_ssn), method: :put, class: 'btn btn-link') %> + idv_doc_auth_step_path(step: :redo_ssn), method: :put, class: 'usa-button usa-button--unstyled') %>
<%= t('in_person_proofing.forms.ssn') %>: 111-11-1111
diff --git a/app/views/idv/otp_verification/show.html.erb b/app/views/idv/otp_verification/show.html.erb index a964ba2780b..27aad01242c 100644 --- a/app/views/idv/otp_verification/show.html.erb +++ b/app/views/idv/otp_verification/show.html.erb @@ -24,7 +24,7 @@ <%= button_to(t('links.two_factor_authentication.get_another_code'), idv_resend_otp_path, method: :post, - class: 'btn btn-link btn-border ico ico-refresh text-decoration-none', + class: 'usa-button usa-button--unstyled btn-border ico ico-refresh text-decoration-none', form_class: 'inline-block') %>

diff --git a/app/views/idv/review/new.html.erb b/app/views/idv/review/new.html.erb index 643100b550e..aedf7dc1526 100644 --- a/app/views/idv/review/new.html.erb +++ b/app/views/idv/review/new.html.erb @@ -18,7 +18,7 @@

<%= t('idv.forgot_password.link_html', link: link_to(t('idv.forgot_password.link_text'), idv_forgot_password_url, - class: 'btn btn-link margin-left-1', form_class: 'inline-block')) %> + class: 'usa-button usa-button--unstyled margin-left-1', form_class: 'inline-block')) %>
<%= accordion('review-verified-info', t('idv.messages.review.intro')) do %> diff --git a/app/views/idv/shared/_back_to_sp_link.html.erb b/app/views/idv/shared/_back_to_sp_link.html.erb index 7332d22164e..a3c8c9dd2bd 100644 --- a/app/views/idv/shared/_back_to_sp_link.html.erb +++ b/app/views/idv/shared/_back_to_sp_link.html.erb @@ -3,10 +3,10 @@
<%= link_to image_tag(asset_url('carat-right.svg'), size: '10'), - return_to_sp_failure_to_proof_path, class: 'bold block btn-link text-decoration-none' %> + return_to_sp_failure_to_proof_path, class: 'bold block usa-button usa-button--unstyled text-decoration-none' %>
<%= link_to t('idv.failure.help.get_help_html', sp_name: decorated_session.sp_name), - return_to_sp_failure_to_proof_path, class: 'block btn-link text-decoration-none' %> + return_to_sp_failure_to_proof_path, class: 'block usa-button usa-button--unstyled text-decoration-none' %>

<% end %> diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index be1d1036017..1d17a83a44d 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -146,7 +146,7 @@ <% if session['idv/capture_doc'] %>
<%= button_to(cancel_link_text, idv_capture_doc_step_url(step: :cancel), method: :put, - class: 'btn btn-link') %> + class: 'usa-button usa-button--unstyled') %>
<% else %> <%= render 'shared/cancel', link: idv_cancel_path %> diff --git a/app/views/idv/shared/_reset_your_account.html.erb b/app/views/idv/shared/_reset_your_account.html.erb index 2f932175bc1..4be74a88614 100644 --- a/app/views/idv/shared/_reset_your_account.html.erb +++ b/app/views/idv/shared/_reset_your_account.html.erb @@ -2,7 +2,7 @@
<%= link_to image_tag(asset_url('carat-right.svg'), size: '10'), - return_to_sp_failure_to_proof_path, class: 'bold block btn-link text-decoration-none' %> + return_to_sp_failure_to_proof_path, class: 'bold block usa-button usa-button--unstyled text-decoration-none' %>
<%= link_to t('two_factor_authentication.account_reset.reset_your_account'), account_reset_request_path %> diff --git a/app/views/shared/_cancel.html.erb b/app/views/shared/_cancel.html.erb index 2be7fa5dfa3..787a23433f0 100644 --- a/app/views/shared/_cancel.html.erb +++ b/app/views/shared/_cancel.html.erb @@ -1,6 +1,6 @@
<% if user_signing_up? %> - <%= link_to cancel_link_text, sign_up_cancel_path, method: :get, class: 'btn btn-link' %> + <%= link_to cancel_link_text, sign_up_cancel_path, method: :get, class: 'usa-button usa-button--unstyled' %> <% else %> <%= link_to cancel_link_text, link || return_to_sp_cancel_path %> <% end %> diff --git a/app/views/two_factor_authentication/otp_verification/show.html.erb b/app/views/two_factor_authentication/otp_verification/show.html.erb index 31e7bb1a1b9..74c6f08f39a 100644 --- a/app/views/two_factor_authentication/otp_verification/show.html.erb +++ b/app/views/two_factor_authentication/otp_verification/show.html.erb @@ -36,7 +36,7 @@ otp_delivery_preference: @presenter.otp_delivery_preference, resend: true }), - class: 'btn btn-link btn-border ico ico-refresh text-decoration-none flex-no-shrink margin-right-205', + class: 'usa-button usa-button--unstyled btn-border ico ico-refresh text-decoration-none flex-no-shrink margin-right-205', form_class: 'inline-block') %> diff --git a/app/views/users/emails/verify.html.erb b/app/views/users/emails/verify.html.erb index f8b0129da29..5167b2b4541 100644 --- a/app/views/users/emails/verify.html.erb +++ b/app/views/users/emails/verify.html.erb @@ -23,7 +23,7 @@
<%= t('notices.signed_up_and_confirmed.no_email_sent_explanation_start') %> -<%= button_to(t('links.resend'), add_email_resend_path, method: :post, class: 'btn btn-link margin-left-1', form_class: 'inline-block') %> +<%= button_to(t('links.resend'), add_email_resend_path, method: :post, class: 'usa-button usa-button--unstyled margin-left-1', form_class: 'inline-block') %>

<% link = link_to(t('notices.use_diff_email.link'), add_email_path) %>

diff --git a/app/views/users/piv_cac_login/account_not_found.html.erb b/app/views/users/piv_cac_login/account_not_found.html.erb deleted file mode 100644 index 53ab944f840..00000000000 --- a/app/views/users/piv_cac_login/account_not_found.html.erb +++ /dev/null @@ -1,8 +0,0 @@ -<% title t('headings.piv_cac_login.account_not_found') %> - -

<%= t('headings.piv_cac_login.account_not_found') %>

-<%= t('instructions.mfa.piv_cac.account_not_found_html', - sign_in: link_to(t('headings.sign_in_without_sp'), root_url), - create_account: link_to(t('links.create_account'), sign_up_email_url), - ) -%> diff --git a/app/views/users/piv_cac_login/did_not_work.html.erb b/app/views/users/piv_cac_login/did_not_work.html.erb deleted file mode 100644 index b5f349a0f9d..00000000000 --- a/app/views/users/piv_cac_login/did_not_work.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<% title t('headings.piv_cac.did_not_work') %> - -

- <%= t('headings.piv_cac.did_not_work') %> -

- -

- <%= t('instructions.mfa.piv_cac.did_not_work_html', - please_try_again: link_to(I18n.t('instructions.mfa.piv_cac.please_try_again'), login_piv_cac_url)) %> -

- -
- <%= link_to t('instructions.mfa.piv_cac.back_to_sign_in'), root_url, class: 'usa-button' %> -
- -<%= render 'shared/cancel', link: new_user_session_url %> diff --git a/app/views/users/piv_cac_login/temporary_error.html.erb b/app/views/users/piv_cac_login/temporary_error.html.erb deleted file mode 100644 index 6f95291478c..00000000000 --- a/app/views/users/piv_cac_login/temporary_error.html.erb +++ /dev/null @@ -1,11 +0,0 @@ -<% title t('headings.piv_cac_login.temporary_error') %> -

- <%= t('headings.piv_cac_login.temporary_error') %> -

-

- <%= t('instructions.mfa.piv_cac.temporary_error') %> -

-
- <%= link_to t('instructions.mfa.piv_cac.back_to_sign_in'), root_url, class: 'usa-button' %> -
-<%= render 'shared/cancel', link: new_user_session_url %> diff --git a/app/views/users/verify_account/index.html.erb b/app/views/users/verify_account/index.html.erb index 2b7c3a5c2bd..11663d4febe 100644 --- a/app/views/users/verify_account/index.html.erb +++ b/app/views/users/verify_account/index.html.erb @@ -20,7 +20,7 @@ <%= link_to t('idv.messages.usps.resend'), idv_usps_path, class: 'block margin-bottom-2' %> <% end %> -<%= button_to t('idv.messages.clear_and_start_over'), idv_session_path, method: :delete, class: 'btn btn-link' %> +<%= button_to t('idv.messages.clear_and_start_over'), idv_session_path, method: :delete, class: 'usa-button usa-button--unstyled' %>
<%= link_to t('idv.buttons.cancel'), account_path %> diff --git a/app/views/users/verify_account/throttled.html.erb b/app/views/users/verify_account/throttled.html.erb new file mode 100644 index 00000000000..314043fd908 --- /dev/null +++ b/app/views/users/verify_account/throttled.html.erb @@ -0,0 +1,9 @@ +<% title t('titles.verify_profile') %> + +

+ <%= t('forms.verify_profile.title') %> +

+ +

+ <%= t('errors.verify_profile.throttled') %> <%= link_to(t('links.go_back'), account_path) %>. +

diff --git a/app/views/users/verify_personal_key/new.html.erb b/app/views/users/verify_personal_key/new.html.erb index d392b53e797..1a86b89708a 100644 --- a/app/views/users/verify_personal_key/new.html.erb +++ b/app/views/users/verify_personal_key/new.html.erb @@ -20,7 +20,7 @@
<%= t('forms.personal_key.alternative') %> <%= button_to(t('links.reverify'), reactivate_account_path, method: :put, - class: 'btn btn-link margin-left-1', form_class: 'inline-block') %> + class: 'usa-button usa-button--unstyled margin-left-1', form_class: 'inline-block') %>
<%= render 'shared/cancel', link: account_path %> diff --git a/app/views/users/verify_personal_key/throttled.html.erb b/app/views/users/verify_personal_key/throttled.html.erb new file mode 100644 index 00000000000..8622cbdfe75 --- /dev/null +++ b/app/views/users/verify_personal_key/throttled.html.erb @@ -0,0 +1,9 @@ +<% title t('headings.verify_personal_key') %> + +

+ <%= t('headings.verify_personal_key') %> +

+ +

+ <%= t('errors.verify_personal_key.throttled') %> <%= link_to(t('links.go_back'), account_path) %>. +

diff --git a/bin/delayed_job b/bin/delayed_job new file mode 100755 index 00000000000..edf195985f6 --- /dev/null +++ b/bin/delayed_job @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) +require 'delayed/command' +Delayed::Command.new(ARGV).daemonize diff --git a/config/application.rb b/config/application.rb index e832dd60796..42d87769502 100644 --- a/config/application.rb +++ b/config/application.rb @@ -23,7 +23,7 @@ class Application < Rails::Application config.active_record.belongs_to_required_by_default = false config.assets.unknown_asset_fallback = true - config.active_job.queue_adapter = 'inline' + config.active_job.queue_adapter = :inline config.time_zone = 'UTC' # Generate CSRF tokens that are encoded in URL-safe Base64. diff --git a/config/application.yml.default b/config/application.yml.default index 04cd28a4677..7664c0636e6 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -125,6 +125,8 @@ remember_device_expiration_hours_aal_1: '720' remember_device_expiration_hours_aal_2: '12' report_timeout: requests_per_ip_track_only_mode: 'false' +risc_notifications_sqs_enabled: 'false' +ruby_workers_enabled: 'false' saml_secret_rotation_enabled: service_provider_request_ttl_hours: '24' session_check_delay: '30' @@ -140,6 +142,10 @@ usps_download_sftp_timeout: '5' usps_upload_enabled: 'false' usps_upload_sftp_timeout: '5' valid_authn_contexts: '["http://idmanagement.gov/ns/assurance/loa/1", "http://idmanagement.gov/ns/assurance/loa/3", "http://idmanagement.gov/ns/assurance/ial/1", "http://idmanagement.gov/ns/assurance/ial/2", "http://idmanagement.gov/ns/assurance/ial/0", "http://idmanagement.gov/ns/assurance/ial/2?strict=true", "urn:gov:gsa:ac:classes:sp:PasswordProtectedTransport:duo", "http://idmanagement.gov/ns/assurance/aal/2", "http://idmanagement.gov/ns/assurance/aal/3", "http://idmanagement.gov/ns/assurance/aal/3?hspd12=true"]' +verify_gpo_key_attempt_window_in_minutes: '10' +verify_gpo_key_max_attempts: '3' +verify_personal_key_attempt_window_in_minutes: '15' +verify_personal_key_max_attempts: '5' usps_ipp_password: '' usps_ipp_root_url: '' usps_ipp_sponsor_id: '' @@ -443,6 +449,7 @@ test: reset_password_email_max_attempts: '5' reset_password_email_window_in_minutes: '80' resolution_proof_result_lambda_token: test_token_resolution + ruby_workers_enabled: 'true' s3_report_bucket_prefix: s3_reports_enabled: 'false' saml_endpoint_configs: '[{"suffix":"2021","secret_key_passphrase":"trust-but-verify"}]' @@ -451,6 +458,10 @@ test: session_encryption_key: 27bad3c25711099429c1afdfd1890910f3b59f5a4faec1c85e945cb8b02b02f261ba501d99cfbb4fab394e0102de6fecf8ffe260f322f610db3e96b2a775c120 sps_over_quota_limit_notify_email_list: '["test1@test.com"]' telephony_adapter: test + verify_gpo_key_attempt_window_in_minutes: '3' + verify_gpo_key_max_attempts: '2' + verify_personal_key_attempt_window_in_minutes: '3' + verify_personal_key_max_attempts: '1' use_dashboard_service_providers: 'false' use_kms: 'false' usps_confirmation_max_days: '10' diff --git a/config/environments/development.rb b/config/environments/development.rb index 4f191c8e5d0..cab7bbe5389 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -2,6 +2,7 @@ # Verifies that versions and hashed value of the package contents in the project's package.json config.webpacker.check_yarn_integrity = true + config.active_job.queue_adapter = :delayed_job config.cache_classes = false config.eager_load = false config.consider_all_requests_local = true diff --git a/config/initializers/active_job_logger_patch.rb b/config/initializers/active_job_logger_patch.rb deleted file mode 100644 index 37ebf6ab0aa..00000000000 --- a/config/initializers/active_job_logger_patch.rb +++ /dev/null @@ -1,41 +0,0 @@ -# This overrides the ActiveJob logger to remove sensitive info from the logs, -# such as Devise tokens, OTP codes, phone numbers, emails, and data sent to -# Google Analytics. -ActiveSupport.on_load :active_job do # rubocop:disable Metrics/BlockLength - module ActiveJob - module Logging - class LogSubscriber - def enqueue(event) - info { json_for(event: event, event_type: 'Enqueued') } - end - - def perform_start(event) - info { json_for(event: event, event_type: 'Performing') } - end - - def perform(event) - info { json_for(event: event, event_type: 'Performed') } - end - - private - - def json_for(event:, event_type:) - job = event.payload[:job] - - { - timestamp: Time.zone.now, - event_type: event_type, - job_class: job.class.name, - job_queue: queue_name(event), - job_id: job.job_id, - duration: "#{event.duration.round(2)}ms", - }.to_json - end - - def args_info(_job) - '' - end - end - end - end -end diff --git a/config/initializers/delayed_job.rb b/config/initializers/delayed_job.rb new file mode 100644 index 00000000000..92087607ca3 --- /dev/null +++ b/config/initializers/delayed_job.rb @@ -0,0 +1,6 @@ +Delayed::Worker.destroy_failed_jobs = true +Delayed::Worker.sleep_delay = 5 +Delayed::Worker.max_attempts = 1 +Delayed::Worker.max_run_time = 5.minutes +Delayed::Worker.read_ahead = 10 +Delayed::Worker.default_queue_name = 'default' diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index 62932e4d783..4aba2ab4bff 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -7,7 +7,7 @@ config.x_permitted_cross_domain_policies = 'none' connect_src = ["'self'", '*.newrelic.com', '*.nr-data.net', '*.google-analytics.com', - 'us.acas.acuant.net', 'services.assureid.net'] + 'us.acas.acuant.net'] connect_src << %w[ws://localhost:3035 http://localhost:3035] if Rails.env.development? default_csp_config = { default_src: ["'self'"], diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index f4b59bbd235..08b0bd36509 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -118,6 +118,10 @@ en: unique_name: That name is already taken. Please choose a different name. two_factor_auth_setup: must_select_option: Select an authentication method. + verify_personal_key: + throttled: You tried too many times, please try again in 15 minutes. + verify_profile: + throttled: You tried too many times, please try again in 10 minutes. webauthn_setup: already_registered: Security key already registered. Please try a different security key. diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index a8e4a0d352c..871d35082d1 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -121,6 +121,10 @@ es: unique_name: El nombre ya fue escogido. Por favor, elija un nombre diferente. two_factor_auth_setup: must_select_option: Seleccione un método de autenticación. + verify_personal_key: + throttled: Lo intentaste muchas veces, vuelve a intentarlo en 15 minutos. + verify_profile: + throttled: Lo intentaste muchas veces, vuelve a intentarlo en 10 minutos. webauthn_setup: already_registered: Clave de seguridad ya registrada. Por favor, intente una clave de seguridad diferente. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index e8758d5e196..bc91ef4b73a 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -128,6 +128,10 @@ fr: unique_name: Ce nom est déjà pris. Veuillez choisir un autre nom. two_factor_auth_setup: must_select_option: Sélectionnez une méthode d'authentification. + verify_personal_key: + throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans 15 minutes. + verify_profile: + throttled: Vous avez essayé plusieurs fois, essayez à nouveau dans 10 minutes. webauthn_setup: already_registered: Clé de sécurité déjà enregistrée. Veuillez essayer une clé de sécurité différente. diff --git a/config/locales/headings/en.yml b/config/locales/headings/en.yml index 83756e5fd0e..533e58f6474 100644 --- a/config/locales/headings/en.yml +++ b/config/locales/headings/en.yml @@ -55,7 +55,6 @@ en: use it to sign in. new: Sign in with your PIV or CAC success: You successfully set up PIV/CAC as an authentication method. - temporary_error: We're having technical difficulties using your PIV/CAC piv_cac_setup: already_associated: The PIV/CAC you presented is associated with another user. new: Use your PIV/CAC card to secure your account @@ -68,5 +67,6 @@ en: totp_setup: new: Add an authentication app verify_email: Check your email + verify_personal_key: Verify your personal key webauthn_setup: new: Add your security key diff --git a/config/locales/headings/es.yml b/config/locales/headings/es.yml index cb1a02ac233..201af5760e9 100644 --- a/config/locales/headings/es.yml +++ b/config/locales/headings/es.yml @@ -55,7 +55,6 @@ es: para que pueda usarlo para iniciar sesión. new: Use su PIV / CAC para iniciar sesión en su cuenta success: Configuró correctamente PIV/CAC como método de autenticación. - temporary_error: Estamos teniendo dificultades técnicas para usar su PIV/CAC piv_cac_setup: already_associated: La PIV/CAC que has presentado está asociada a otro usuario. new: Use su tarjeta PIV/CAC para asegurar su cuenta @@ -68,5 +67,6 @@ es: totp_setup: new: Agregar una aplicación de autenticación verify_email: Revise su email + verify_personal_key: Verifica tu clave personal webauthn_setup: new: Añade tu clave de seguridad diff --git a/config/locales/headings/fr.yml b/config/locales/headings/fr.yml index c71ca574c1c..a85b7c7301b 100644 --- a/config/locales/headings/fr.yml +++ b/config/locales/headings/fr.yml @@ -56,7 +56,6 @@ fr: à deux facteurs pour pouvoir l'utiliser pour vous connecter. new: Utilisez votre PIV / CAC pour vous connecter à votre compte success: Vous avez correctement configuré PIV/CAC en tant que méthode d’authentification. - temporary_error: Nous rencontrons des difficultés techniques avec votre PIV/CAC piv_cac_setup: already_associated: La carte PIV/CAC que vous avez présentée est associée à un autre utilisateur. @@ -70,5 +69,6 @@ fr: totp_setup: new: Ajouter une application d'authentification verify_email: Consultez vos courriels + verify_personal_key: Vérifier votre clé personnelle webauthn_setup: new: Ajoutez votre clé de sécurité diff --git a/config/locales/instructions/en.yml b/config/locales/instructions/en.yml index 3b82527d1fb..8788379af54 100644 --- a/config/locales/instructions/en.yml +++ b/config/locales/instructions/en.yml @@ -59,8 +59,6 @@ en: step_3_info_html: You'll need to choose a certificate (the right one likely has your name in it) and enter your PIN (your PIN was created when you set up your PIV/CAC). - temporary_error: Please try again later or sign in with your email and password - instead. try_again: try again sms: confirm_code_html: Need another code? %{resend_code_link}. Message rates may diff --git a/config/locales/instructions/es.yml b/config/locales/instructions/es.yml index f7896f31eb4..1468c0f15f9 100644 --- a/config/locales/instructions/es.yml +++ b/config/locales/instructions/es.yml @@ -61,8 +61,6 @@ es: step_3_info_html: Tendrá que elegir un certificado (el correcto probablemente tenga su nombre) e ingrese su PIN (su PIN se creó cuando configuró su PIV/CAC) - temporary_error: Vuelve a intentarlo más tarde o inicia sesión con tu correo - electrónico y contraseña. try_again: inténtelo de nuevo sms: confirm_code_html: "¿Necesita otro código? %{resend_code_link}. Puede estar diff --git a/config/locales/instructions/fr.yml b/config/locales/instructions/fr.yml index f88969a39d5..3bb1f520869 100644 --- a/config/locales/instructions/fr.yml +++ b/config/locales/instructions/fr.yml @@ -69,8 +69,6 @@ fr: de droite contient probablement votre nom) et entrer votre code PIN (votre code PIN a été créé lors de la configuration de votre PIV / CAC ). - temporary_error: Veuillez réessayer ultérieurement ou connectez-vous avec - votre adresse électronique et votre mot de passe. try_again: réessayer sms: confirm_code_html: Vous avez besoin d'un autre code? %{resend_code_link}. diff --git a/config/routes.rb b/config/routes.rb index 592f69ce28f..5ccd474a894 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,10 +77,6 @@ get '/login/piv_cac' => 'users/piv_cac_login#new' get '/login/piv_cac_error' => 'users/piv_cac_login#error' - # these routes are deprecated - get '/login/piv_cac_account_not_found' => 'users/piv_cac_login#account_not_found' - get '/login/piv_cac_did_not_work' => 'users/piv_cac_login#did_not_work' - get '/login/piv_cac_temporary_error' => 'users/piv_cac_login#temporary_error' get '/login/present_piv_cac' => 'users/piv_cac_login#redirect_to_piv_cac_service' get '/login/password' => 'password_capture#new', as: :capture_password diff --git a/db/migrate/20210315144559_create_delayed_jobs.rb b/db/migrate/20210315144559_create_delayed_jobs.rb new file mode 100644 index 00000000000..d55c28bc2b9 --- /dev/null +++ b/db/migrate/20210315144559_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration[6.1] + def self.up + create_table :delayed_jobs do |table| + table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue + table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. + table.text :handler, null: false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps null: true + end + + add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/schema.rb b/db/schema.rb index 2adea805d83..3bca0fea0ce 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_03_03_182041) do +ActiveRecord::Schema.define(version: 2021_03_15_144559) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -79,6 +79,21 @@ t.index ["user_id", "created_at"], name: "index_backup_code_configurations_on_user_id_and_created_at" end + create_table "delayed_jobs", force: :cascade do |t| + t.integer "priority", default: 0, null: false + t.integer "attempts", default: 0, null: false + t.text "handler", null: false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at", precision: 6 + t.datetime "updated_at", precision: 6 + t.index ["priority", "run_at"], name: "delayed_jobs_priority" + end + create_table "deleted_users", force: :cascade do |t| t.integer "user_id", null: false t.string "uuid", null: false diff --git a/lib/deploy/activate.rb b/lib/deploy/activate.rb index da9c4b2f03e..5f57fe4a253 100644 --- a/lib/deploy/activate.rb +++ b/lib/deploy/activate.rb @@ -28,6 +28,7 @@ def run s3_path: '/%s/idp/v1/application.yml', local_path: env_yaml_path, ) + download_web_or_worker_yml_if_exists deep_merge_s3_data_with_example_application_yml set_proper_file_permissions_for_application_yml @@ -103,6 +104,17 @@ def deep_merge_s3_data_with_example_application_yml File.open(result_yaml_path, 'w') { |file| file.puts YAML.dump(application_config) } end + def download_web_or_worker_yml_if_exists + return unless web_or_worker_yml + + app_secrets_s3.download_file( + s3_path: "/%s/idp/v1/#{web_or_worker_yml}", + local_path: web_or_worker_yaml_path, + ) + rescue Aws::S3::Errors::NoSuchKey => err + logger.warn("did not load #{web_or_worker_yml}, continuing") + end + def set_proper_file_permissions_for_application_yml FileUtils.chmod(0o640, [env_yaml_path, result_yaml_path]) end @@ -147,7 +159,14 @@ def root end def application_config - YAML.load_file(example_application_yaml_path).deep_merge(YAML.load_file(env_yaml_path)) + config = YAML.load_file(example_application_yaml_path). + deep_merge(YAML.load_file(env_yaml_path)) + + if web_or_worker_yml && File.exist?(web_or_worker_yaml_path) + config.deep_merge(YAML.load_file(web_or_worker_yaml_path)) + else + config + end end def example_application_yaml_path @@ -169,5 +188,19 @@ def geolocation_db_path def pwned_passwords_path File.join(root, 'pwned_passwords/pwned_passwords.txt') end + + def web_or_worker_yaml_path + File.join(root, "config/#{web_or_worker_yml}") + end + + # @return [String, nil] + def web_or_worker_yml + case Identity::Hostdata.instance_role + when 'idp' + 'web.yml' + when 'worker' + 'worker.yml' + end + end end end diff --git a/lib/feature_management.rb b/lib/feature_management.rb index 63fcc92eea4..9aa43a1dd9c 100644 --- a/lib/feature_management.rb +++ b/lib/feature_management.rb @@ -142,6 +142,10 @@ def self.voip_block? AppConfig.env.voip_block == 'true' end + def self.ruby_workers_enabled? + AppConfig.env.ruby_workers_enabled == 'true' + end + # Manual allowlist for VOIPs, should only include known VOIPs that we use for smoke tests # @return [Set] set of phone numbers normalized to e164 def self.voip_allowed_phones diff --git a/lib/lambda_jobs/git_ref.rb b/lib/lambda_jobs/git_ref.rb index 9197ef86209..160d76af46d 100644 --- a/lib/lambda_jobs/git_ref.rb +++ b/lib/lambda_jobs/git_ref.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module LambdaJobs - GIT_REF = '051a8c3dca143b215838176a07b1f0545ffec0a2' + GIT_REF = 'd9241bdfea85a76c170e456a89ec6601549f4c4a' end diff --git a/lib/tasks/newrelic.rake b/lib/tasks/newrelic.rake new file mode 100644 index 00000000000..d7e879e0b03 --- /dev/null +++ b/lib/tasks/newrelic.rake @@ -0,0 +1,18 @@ +namespace :newrelic do + ## + # This task is essentially the same as running the following: + # + # bundle exec newrelic deployment -r $(git rev-parse HEAD) + # + # The reason for the rake task is that our `newrelic.yml` file contains ERB + # blocks that expect the AppConfig to be setup and for identity-hostdata to be + # loaded. This rake task loads the rails environment before reporting the + # deployment so the NewRelic config is loaded correctly. + # + desc 'Report a new deployment to NewRelic' + task deployment: :environment do + require 'new_relic/cli/command' + revision = `git rev-parse HEAD`.chomp + NewRelic::Cli::Deployments.new(revision: `git rev-parse HEAD`).run + end +end diff --git a/package.json b/package.json index 2d1bbf42951..03efaab7536 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "clipboard": "^2.0.6", "domready": "^1.0.8", "focus-trap": "^6.2.3", - "identity-style-guide": "^5.0.1", + "identity-style-guide": "^5.0.3", "intl-tel-input": "^17.0.8", "libphonenumber-js": "^1.9.6", "postcss-clean": "^1.1.0", diff --git a/spec/config/initializers/active_job_logger_patch_spec.rb b/spec/config/initializers/active_job_logger_patch_spec.rb deleted file mode 100644 index aade0319acb..00000000000 --- a/spec/config/initializers/active_job_logger_patch_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'rails_helper' - -# Covers config/initializers/active_job_logger_patch.rb, which overrides -# ActiveJob::Logging::LogSubscriber to standardize output and prevent sensitive -# user data from being logged. -describe ActiveJob::Logging::LogSubscriber do - it 'overrides the default job logger to output only specified parameters in JSON format' do - # rubocop:disable Rails/ApplicationJob - class FakeJob < ActiveJob::Base - def perform(sensitive_param:); end - end - # rubocop:enable Rails/ApplicationJob - - # This list corresponds to the initializer's output - permitted_attributes = %w[ - timestamp - event_type - job_class - job_queue - job_id - duration - ] - - # In this case, we need to assert before the action which logs, block-style to - # match the initializer - expect(Rails.logger).to receive(:info) do |&blk| - output = JSON.parse(blk.call) - - # [Sidenote: The nested assertions don't seem to be reflected in the spec - # count--perhaps because of the uncommon block format?--but reversing them - # will show them failing as expected.] - output.each_key { |k| expect(permitted_attributes).to include(k) } - expect(output.keys).to_not include('sensitive_param') - end - - FakeJob.perform_later(sensitive_param: '111-22-3333') - end -end diff --git a/spec/controllers/idv/doc_auth_controller_spec.rb b/spec/controllers/idv/doc_auth_controller_spec.rb index 7716edd7322..4f4ea1267d6 100644 --- a/spec/controllers/idv/doc_auth_controller_spec.rb +++ b/spec/controllers/idv/doc_auth_controller_spec.rb @@ -185,6 +185,7 @@ let(:front_image_iv) { SecureRandom.random_bytes(12) } let(:back_image_iv) { SecureRandom.random_bytes(12) } let(:selfie_image_iv) { SecureRandom.random_bytes(12) } + encryption_helper = IdentityIdpFunctions::EncryptionHelper.new before do mock_document_capture_step diff --git a/spec/controllers/idv/image_uploads_controller_spec.rb b/spec/controllers/idv/image_uploads_controller_spec.rb index 6fba548a80b..b7dbe127952 100644 --- a/spec/controllers/idv/image_uploads_controller_spec.rb +++ b/spec/controllers/idv/image_uploads_controller_spec.rb @@ -213,6 +213,7 @@ exception: nil, result: 'Passed', user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) expect(@analytics).to receive(:track_event).with( @@ -220,6 +221,7 @@ success: true, errors: {}, user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) action @@ -272,6 +274,7 @@ exception: nil, result: 'Passed', user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) expect(@analytics).to receive(:track_event).with( @@ -281,6 +284,7 @@ pii: [I18n.t('doc_auth.errors.lexis_nexis.full_name_check')], }, user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) action @@ -309,6 +313,7 @@ exception: nil, result: 'Passed', user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) expect(@analytics).to receive(:track_event).with( @@ -318,6 +323,7 @@ pii: [I18n.t('doc_auth.errors.lexis_nexis.general_error_no_liveness')], }, user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) action @@ -346,6 +352,7 @@ exception: nil, result: 'Passed', user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) expect(@analytics).to receive(:track_event).with( @@ -355,6 +362,7 @@ pii: [I18n.t('doc_auth.errors.lexis_nexis.birth_date_checks')], }, user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) action @@ -404,6 +412,7 @@ front: ['Too blurry', 'Wrong document'], }, user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, exception: nil, ) @@ -449,6 +458,7 @@ result: 'Caution', exception: nil, user_id: user.uuid, + remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1, ) action diff --git a/spec/controllers/lambda_callback/address_proof_result_controller_spec.rb b/spec/controllers/lambda_callback/address_proof_result_controller_spec.rb index b51f98677b9..09adbc6ff93 100644 --- a/spec/controllers/lambda_callback/address_proof_result_controller_spec.rb +++ b/spec/controllers/lambda_callback/address_proof_result_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe LambdaCallback::AddressProofResultController do + include IdvHelper + describe '#create' do let(:document_capture_session) { DocumentCaptureSession.new(user: create(:user)) } let(:trace_id) { SecureRandom.uuid } @@ -12,7 +14,6 @@ it 'accepts and stores successful address proofing results' do applicant = { phone: Faker::PhoneNumber.cell_phone } - document_capture_session.create_proofing_session Idv::Agent.new(applicant).proof_address(document_capture_session, trace_id: trace_id) proofer_result = document_capture_session.load_proofing_result[:result] @@ -28,7 +29,6 @@ phone: IdentityIdpFunctions::AddressMockClient::UNVERIFIABLE_PHONE_NUMBER, } - document_capture_session.create_proofing_session Idv::Agent.new(applicant).proof_address(document_capture_session, trace_id: trace_id) proofer_result = document_capture_session.load_proofing_result[:result] diff --git a/spec/controllers/lambda_callback/resolution_proof_result_controller_spec.rb b/spec/controllers/lambda_callback/resolution_proof_result_controller_spec.rb index 7890cbb1581..fc89e2bf474 100644 --- a/spec/controllers/lambda_callback/resolution_proof_result_controller_spec.rb +++ b/spec/controllers/lambda_callback/resolution_proof_result_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe LambdaCallback::ResolutionProofResultController do + include IdvHelper + describe '#create' do let(:document_capture_session) { DocumentCaptureSession.new(user: create(:user)) } let(:trace_id) { SecureRandom.uuid } @@ -14,12 +16,12 @@ applicant = { first_name: Faker::Name.first_name, ssn: Faker::IDNumber.valid, zipcode: Faker::Address.zip_code, state_id_number: '123', state_id_type: 'drivers_license', state_id_jurisdiction: 'WI' } - document_capture_session.create_proofing_session Idv::Agent.new(applicant).proof_resolution( document_capture_session, should_proof_state_id: true, trace_id: trace_id, ) + proofer_result = document_capture_session.load_proofing_result[:result] post :create, params: { result_id: document_capture_session.result_id, @@ -32,7 +34,6 @@ it 'accepts and stores unsuccessful resolution proofing results' do applicant = { first_name: 'Bad Name', ssn: Faker::IDNumber.valid, zipcode: Faker::Address.zip_code } - document_capture_session.create_proofing_session Idv::Agent.new(applicant).proof_resolution( document_capture_session, should_proof_state_id: false, @@ -57,7 +58,6 @@ applicant = { first_name: 'Time', ssn: Faker::IDNumber.valid, zipcode: Faker::Address.zip_code } - document_capture_session.create_proofing_session Idv::Agent.new(applicant).proof_resolution( document_capture_session, should_proof_state_id: false, diff --git a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb index 2a0b3f90a7c..c31b249793c 100644 --- a/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb +++ b/spec/controllers/two_factor_authentication/otp_verification_controller_spec.rb @@ -281,7 +281,7 @@ stub_analytics allow(@analytics).to receive(:track_event) allow(subject).to receive(:create_user_event) - @mailer = instance_double(ActionMailer::MessageDelivery, deliver_later: true) + @mailer = instance_double(ActionMailer::MessageDelivery, deliver_now: true) subject.current_user.email_addresses.each do |email_address| allow(UserMailer).to receive(:phone_added). with(subject.current_user, email_address, disavowal_token: instance_of(String)). @@ -328,7 +328,7 @@ expect(UserMailer).to have_received(:phone_added). with(subject.current_user, email_address, disavowal_token: instance_of(String)) end - expect(@mailer).to have_received(:deliver_later) + expect(@mailer).to have_received(:deliver_now) end end diff --git a/spec/controllers/users/passwords_controller_spec.rb b/spec/controllers/users/passwords_controller_spec.rb index 680984d02b2..2ee897512d3 100644 --- a/spec/controllers/users/passwords_controller_spec.rb +++ b/spec/controllers/users/passwords_controller_spec.rb @@ -50,7 +50,7 @@ it 'sends the user an email' do user = create(:user) mail = double - expect(mail).to receive(:deliver_later) + expect(mail).to receive(:deliver_now) expect(UserMailer).to receive(:password_changed). with(user, user.email_addresses.first, hash_including(:disavowal_token)). and_return(mail) diff --git a/spec/controllers/users/reset_passwords_controller_spec.rb b/spec/controllers/users/reset_passwords_controller_spec.rb index a0e0cf7b2ca..73de6eb811a 100644 --- a/spec/controllers/users/reset_passwords_controller_spec.rb +++ b/spec/controllers/users/reset_passwords_controller_spec.rb @@ -461,7 +461,7 @@ end def stub_user_mailer(user) - mailer = instance_double(ActionMailer::MessageDelivery, deliver_later: true) + mailer = instance_double(ActionMailer::MessageDelivery, deliver_now: true) user.email_addresses.each do |email_address| allow(UserMailer).to receive(:password_changed). with(user, email_address, disavowal_token: instance_of(String)). diff --git a/spec/controllers/users/verify_account_controller_spec.rb b/spec/controllers/users/verify_account_controller_spec.rb index ad664ea265d..a27bc36a9f2 100644 --- a/spec/controllers/users/verify_account_controller_spec.rb +++ b/spec/controllers/users/verify_account_controller_spec.rb @@ -9,7 +9,8 @@ before do stub_analytics - user = stub_sign_in + user = create(:user) + stub_sign_in(user) decorated_user = stub_decorated_user_with_pending_profile(user) create( :usps_confirmation_code, @@ -33,6 +34,14 @@ expect(response).to render_template('users/verify_account/index') end + + it 'shows throttled page is user is throttled' do + allow(Throttler::IsThrottled).to receive(:call).once.and_return(true) + + action + + expect(response).to render_template(:throttled) + end end context 'user does not have pending profile' do @@ -87,5 +96,31 @@ expect(response).to render_template('users/verify_account/index') end end + + context 'with throttle reached' do + let(:submitted_otp) { 'a-wrong-otp' } + + it 'renders the index page to show errors' do + max_attempts = AppConfig.env.verify_gpo_key_max_attempts.to_i + + expect(@analytics).to receive(:track_event).with( + Analytics::ACCOUNT_VERIFICATION_SUBMITTED, + success: false, errors: { otp: [t('errors.messages.confirmation_code_incorrect')]}, + ).exactly(max_attempts).times + + (max_attempts + 1).times do |i| + post( + :create, + params: { + verify_account_form: { + otp: submitted_otp, + }, + }, + ) + end + + expect(response).to render_template('users/verify_account/throttled') + end + end end end diff --git a/spec/controllers/users/verify_personal_key_controller_spec.rb b/spec/controllers/users/verify_personal_key_controller_spec.rb index 9a4d7e2f328..ae6a1168e41 100644 --- a/spec/controllers/users/verify_personal_key_controller_spec.rb +++ b/spec/controllers/users/verify_personal_key_controller_spec.rb @@ -35,6 +35,13 @@ expect(subject.flash[:info]).to eq(t('notices.account_reactivation')) end + + it 'shows throttled page after being throttled' do + allow(Throttler::IsThrottled).to receive(:call).once.and_return(true) + get :new + + expect(response).to render_template(:throttled) + end end end @@ -86,5 +93,28 @@ expect(response).to render_template(:new) end end + + context 'with throttle reached' do + let(:bad_key) { 'baaad' } + before do + allow(VerifyPersonalKeyForm).to receive(:new). + with(user: subject.current_user, personal_key: bad_key). + and_return(form) + allow(form).to receive(:submit).and_return(response_bad) + end + + it 'renders throttled page' do + stub_analytics + expect(@analytics).to receive(:track_event).with( + Analytics::PERSONAL_KEY_REACTIVATION_SUBMITTED, + { errors: { personal_key: ['bad_key'] }, success: false }, + ).once + + post :create, params: { personal_key: bad_key } + post :create, params: { personal_key: bad_key } + + expect(response).to render_template(:throttled) + end + end end end diff --git a/spec/features/idv/doc_auth/document_capture_step_spec.rb b/spec/features/idv/doc_auth/document_capture_step_spec.rb index 5fbf53d9bec..ff97df963f2 100644 --- a/spec/features/idv/doc_auth/document_capture_step_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_step_spec.rb @@ -274,6 +274,8 @@ allow(LambdaJobs::Runner).to receive(:new). with(hash_including(job_class: Idv::Proofer.document_job_class)). and_call_original + allow(AppConfig.env).to receive(:ruby_workers_enabled). + and_return('false') end it 'proceeds to the next page with valid info' do diff --git a/spec/features/multiple_emails/reset_password_spec.rb b/spec/features/multiple_emails/reset_password_spec.rb index 84feff56cc4..5d646b7754f 100644 --- a/spec/features/multiple_emails/reset_password_spec.rb +++ b/spec/features/multiple_emails/reset_password_spec.rb @@ -40,7 +40,7 @@ ) mail = double - expect(mail).to receive(:deliver_later) + expect(mail).to receive(:deliver_now) expect(UserMailer).to receive(:unconfirmed_email_instructions).with( instance_of(User), unconfirmed_email_address.email, diff --git a/spec/forms/idv/api_image_upload_form_spec.rb b/spec/forms/idv/api_image_upload_form_spec.rb index b8c8d201384..83d81ccc068 100644 --- a/spec/forms/idv/api_image_upload_form_spec.rb +++ b/spec/forms/idv/api_image_upload_form_spec.rb @@ -100,7 +100,11 @@ context 'posting images to client fails' do let(:failed_response) do - IdentityDocAuth::Response.new(success: false, errors: { front: 'glare' }) + IdentityDocAuth::Response.new( + success: false, + errors: { front: 'glare' }, + extra: { remaining_attempts: AppConfig.env.acuant_max_attempts.to_i - 1 }, + ) end before do allow(subject).to receive(:post_images_to_client).and_return(failed_response) diff --git a/spec/forms/register_user_email_form_spec.rb b/spec/forms/register_user_email_form_spec.rb index 6f8d7607986..d4f2962641d 100644 --- a/spec/forms/register_user_email_form_spec.rb +++ b/spec/forms/register_user_email_form_spec.rb @@ -13,7 +13,7 @@ mailer = instance_double(ActionMailer::MessageDelivery) allow(UserMailer).to receive(:signup_with_your_email). with(existing_user, existing_user.email).and_return(mailer) - allow(mailer).to receive(:deliver_later) + allow(mailer).to receive(:deliver_now) extra = { email_already_exists: true, @@ -28,7 +28,7 @@ with(success: true, errors: {}, extra: extra).and_return(result) expect(subject.submit(email: 'TAKEN@gmail.com')).to eq result expect(subject.email).to eq 'taken@gmail.com' - expect(mailer).to have_received(:deliver_later) + expect(mailer).to have_received(:deliver_now) end end diff --git a/spec/javascripts/packages/document-capture/components/button-spec.jsx b/spec/javascripts/packages/document-capture/components/button-spec.jsx index 557c4088739..45d2c747f72 100644 --- a/spec/javascripts/packages/document-capture/components/button-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/button-spec.jsx @@ -13,7 +13,7 @@ describe('document-capture/components/button', () => { expect(button.nodeName).to.equal('BUTTON'); expect(button.type).to.equal('button'); expect(button.classList.contains('btn')).to.be.true(); - expect(button.classList.contains('btn-primary')).to.be.false(); + expect(button.classList.contains('btn-primary')).to.be.true(); expect(button.classList.contains('btn-secondary')).to.be.false(); expect(button.classList.contains('btn-wide')).to.be.false(); expect(button.classList.contains('btn-link')).to.be.false(); @@ -32,8 +32,8 @@ describe('document-capture/components/button', () => { expect(onClick.getCall(0).args[0]).to.equal('click'); }); - it('renders as primary', () => { - const { getByText } = render(); + it('renders as wide', () => { + const { getByText } = render(); const button = getByText('Click me'); @@ -43,8 +43,8 @@ describe('document-capture/components/button', () => { expect(button.classList.contains('btn-link')).to.be.false(); }); - it('renders as secondary', () => { - const { getByText } = render(); + it('renders as outline', () => { + const { getByText } = render(); const button = getByText('Click me'); diff --git a/spec/jobs/address_proofing_job_spec.rb b/spec/jobs/address_proofing_job_spec.rb new file mode 100644 index 00000000000..5b5fcfb7bac --- /dev/null +++ b/spec/jobs/address_proofing_job_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +RSpec.describe AddressProofingJob, type: :job do + it 'stores results' do + document_capture_session = DocumentCaptureSession.new(result_id: SecureRandom.hex) + encrypted_arguments = Encryption::Encryptors::SessionEncryptor.new.encrypt( + { applicant_pii: { phone: Faker::PhoneNumber.cell_phone } }.to_json, + ) + + AddressProofingJob.perform_now( + result_id: document_capture_session.result_id, + encrypted_arguments: encrypted_arguments, callback_url: nil, trace_id: nil + ) + + result = document_capture_session.load_proofing_result[:result] + expect(result).to be_present + end +end diff --git a/spec/jobs/document_proofing_job_spec.rb b/spec/jobs/document_proofing_job_spec.rb new file mode 100644 index 00000000000..84a142116dc --- /dev/null +++ b/spec/jobs/document_proofing_job_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +RSpec.describe DocumentProofingJob, type: :job do + it 'stores results' do + encryption_helper = IdentityIdpFunctions::EncryptionHelper.new + encryption_key = SecureRandom.random_bytes(32) + front_image_iv = SecureRandom.random_bytes(12) + back_image_iv = SecureRandom.random_bytes(12) + + + stub_request(:get, 'http://example.com/front'). + to_return(body: encryption_helper.encrypt( + data: '{}', key: encryption_key, iv: front_image_iv, + )) + stub_request(:get, 'http://example.com/back'). + to_return(body: encryption_helper.encrypt( + data: '{}', key: encryption_key, iv: back_image_iv, + )) + document_arguments = { + encryption_key: Base64.encode64(encryption_key), + front_image_iv: Base64.encode64(front_image_iv), + back_image_iv: Base64.encode64(back_image_iv), + front_image_url: 'http://example.com/front', + back_image_url: 'http://example.com/back', + } + + document_capture_session = DocumentCaptureSession.new(result_id: SecureRandom.hex) + encrypted_arguments = Encryption::Encryptors::SessionEncryptor.new.encrypt( + { document_arguments: document_arguments }.to_json, + ) + + DocumentProofingJob.perform_now( + result_id: document_capture_session.result_id, + liveness_checking_enabled: false, encrypted_arguments: encrypted_arguments, + callback_url: nil, trace_id: nil + ) + + result = document_capture_session.load_doc_auth_async_result + expect(result).to be_present + end +end diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb new file mode 100644 index 00000000000..49d67e71d5c --- /dev/null +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +RSpec.describe ResolutionProofingJob, type: :job do + it 'stores results' do + pii = { + ssn: '444-55-8888', + first_name: Faker::Name.first_name, + zipcode: Faker::Address.zip_code, + state_id_number: '123456789', + state_id_type: 'drivers_license', + state_id_jurisdiction: Faker::Address.state_abbr, + } + + document_capture_session = DocumentCaptureSession.new(result_id: SecureRandom.hex) + encrypted_arguments = Encryption::Encryptors::SessionEncryptor.new.encrypt( + { applicant_pii: pii }.to_json, + + ) + + ResolutionProofingJob.perform_now( + result_id: document_capture_session.result_id, should_proof_state_id: false, + dob_year_only: false, encrypted_arguments: encrypted_arguments, + callback_url: nil, trace_id: nil + ) + + result = document_capture_session.load_proofing_result[:result] + expect(result).to be_present + end +end diff --git a/spec/lib/deploy/activate_spec.rb b/spec/lib/deploy/activate_spec.rb index 100b7c6a91c..854aa2e62ee 100644 --- a/spec/lib/deploy/activate_spec.rb +++ b/spec/lib/deploy/activate_spec.rb @@ -17,6 +17,7 @@ let(:logger) { Logger.new('/dev/null') } let(:s3_client) { Aws::S3::Client.new(stub_responses: true) } let(:set_up_files!) {} + let(:instance_role) { 'idp' } let(:result_yaml_path) { File.join(root, 'config', 'application.yml') } let(:env_yaml_path) { File.join(root, 'config', 'application_s3_env.yml') } @@ -35,6 +36,7 @@ context 'in a deployed production environment' do before do allow(Identity::Hostdata).to receive(:env).and_return('int') + allow(Identity::Hostdata).to receive(:instance_role).and_return(instance_role) stub_request(:get, 'http://169.254.169.254/2016-09-02/dynamic/instance-identity/document'). to_return(body: { @@ -42,11 +44,15 @@ 'accountId' => '12345', }.to_json) - s3_client.stub_responses( - :get_object, - { body: application_yml }, - { body: geolite_content }, - { body: pwned_passwords_content }, + s3_client.stub_responses(:get_object, proc do |context| + key = context.params[:key] + body = s3_contents[key] + if body.present? + { body: body } + else + raise Aws::S3::Errors::NoSuchKey.new(nil, nil) + end + end ) allow(s3_client).to receive(:get_object).and_call_original @@ -55,6 +61,14 @@ allow(subject).to receive(:setup_idp_config_symlinks) end + let(:s3_contents) do + { + 'int/idp/v1/application.yml' => application_yml, + 'common/GeoIP2-City.mmdb' => geolite_content, + 'common/pwned-passwords.txt' => pwned_passwords_content, + } + end + let(:application_yml) do <<~YAML production: @@ -89,6 +103,74 @@ expect(combined_application_yml['production']['lockout_period_in_minutes']).to eq('10') end + context 'on a web instance' do + let(:instance_role) { 'idp' } + + context 'when web.yml exists in s3' do + before do + s3_contents['int/idp/v1/web.yml'] = <<~YAML + web_yaml_value: 'true' + YAML + end + + it 'merges web.yml into application.yml' do + subject.run + + expect(File.exist?("#{root}/config/web.yml")).to eq(true) + + combined_application_yml = YAML.load_file(result_yaml_path) + expect(combined_application_yml['web_yaml_value']).to eq('true') + end + end + + context 'when web.yml does not exist in s3' do + it 'warns and leaves application.yml as-is' do + expect(logger).to receive(:warn).with(/web.yml/) + + expect { subject.run }.to_not raise_error + + expect(File.exist?("#{root}/config/web.yml")).to eq(false) + + combined_application_yml = YAML.load_file(result_yaml_path) + expect(combined_application_yml).to_not have_key('web_yaml_value') + end + end + end + + context 'on a worker instance' do + let(:instance_role) { 'worker' } + + context 'when worker.yml exists in s3' do + before do + s3_contents['int/idp/v1/worker.yml'] = <<~YAML + worker_yaml_value: 'true' + YAML + end + + it 'merges worker.yml into application.yml' do + subject.run + + expect(File.exist?("#{root}/config/worker.yml")).to eq(true) + + combined_application_yml = YAML.load_file(result_yaml_path) + expect(combined_application_yml['worker_yaml_value']).to eq('true') + end + end + + context 'when worker.yml does not exist in s3' do + it 'warns and leaves application.yml as-is' do + expect(logger).to receive(:warn).with(/worker.yml/) + + expect { subject.run }.to_not raise_error + + expect(File.exist?("#{root}/config/worker.yml")).to eq(false) + + combined_application_yml = YAML.load_file(result_yaml_path) + expect(combined_application_yml).to_not have_key('worker_yaml_value') + end + end + end + it 'sets the correct permissions on the YAML files' do subject.run diff --git a/spec/presenters/backup_code_create_presenter_spec.rb b/spec/presenters/backup_code_create_presenter_spec.rb index a83cafc400d..09100abf721 100644 --- a/spec/presenters/backup_code_create_presenter_spec.rb +++ b/spec/presenters/backup_code_create_presenter_spec.rb @@ -44,7 +44,7 @@ describe '#continue_bttn_class' do it 'displays as a link to continue' do - expect(presenter.continue_bttn_class).to eq 'btn btn-link' + expect(presenter.continue_bttn_class).to eq 'usa-button usa-button--unstyled' end end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index c55644f2fbc..5d7b39d2304 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -88,6 +88,11 @@ config.before(:each) do IdentityDocAuth::Mock::DocAuthMockClient.reset! + original_queue_adapter = ActiveJob::Base.queue_adapter + descendants = ActiveJob::Base.descendants + [ActiveJob::Base] + + ActiveJob::Base.queue_adapter = :inline + descendants.each(&:disable_test_adapter) end config.around(:each, type: :feature) do |example| diff --git a/spec/services/account_reset/cancel_spec.rb b/spec/services/account_reset/cancel_spec.rb index 836eb2daf5d..b01842e372f 100644 --- a/spec/services/account_reset/cancel_spec.rb +++ b/spec/services/account_reset/cancel_spec.rb @@ -57,7 +57,7 @@ it 'notifies the user via email of the account reset cancellation' do token = create_account_reset_request_for(user) - @mailer = instance_double(ActionMailer::MessageDelivery, deliver_later: true) + @mailer = instance_double(ActionMailer::MessageDelivery, deliver_now: true) user.email_addresses.each do |email_address| expect(UserMailer).to receive(:account_reset_cancel).with(user, email_address). and_return(@mailer) diff --git a/spec/services/account_reset/notify_user_of_request_cancellation_spec.rb b/spec/services/account_reset/notify_user_of_request_cancellation_spec.rb index 8fa6a347772..8e4af7dc388 100644 --- a/spec/services/account_reset/notify_user_of_request_cancellation_spec.rb +++ b/spec/services/account_reset/notify_user_of_request_cancellation_spec.rb @@ -18,8 +18,8 @@ expect(UserMailer).to receive(:account_reset_cancel). with(user, email_address2).and_return(mail2) - expect(mail1).to receive(:deliver_later) - expect(mail2).to receive(:deliver_later) + expect(mail1).to receive(:deliver_now) + expect(mail2).to receive(:deliver_now) subject.call end diff --git a/spec/services/attribute_asserter_spec.rb b/spec/services/attribute_asserter_spec.rb index bd12fa99490..efa8712e2be 100644 --- a/spec/services/attribute_asserter_spec.rb +++ b/spec/services/attribute_asserter_spec.rb @@ -14,7 +14,7 @@ ) end let(:name_id_format) { Saml::Idp::Constants::NAME_ID_FORMAT_PERSISTENT } - let(:service_provider_ial) { 2 } + let(:service_provider_ial) { 1 } let(:service_provider_aal) { nil } let(:service_provider) do instance_double( diff --git a/spec/services/email_notifier_spec.rb b/spec/services/email_notifier_spec.rb index 3ac71d04903..17d82925749 100644 --- a/spec/services/email_notifier_spec.rb +++ b/spec/services/email_notifier_spec.rb @@ -19,7 +19,7 @@ UpdateUser.new(user: user, attributes: { email: 'new@example.com' }).call expect(UserMailer).to receive(:email_changed).with(old_email).and_return(mailer) - expect(mailer).to receive(:deliver_later) + expect(mailer).to receive(:deliver_now) EmailNotifier.new(user).send_email_changed_email end diff --git a/spec/services/ial_context_spec.rb b/spec/services/ial_context_spec.rb index 4f573dac3a7..57ac16f23c4 100644 --- a/spec/services/ial_context_spec.rb +++ b/spec/services/ial_context_spec.rb @@ -55,6 +55,56 @@ end end + describe '#default_to_ial2?' do + context 'when the service provider is ial1 and ial1 is requested' do + let(:sp_ial) { Idp::Constants::IAL1 } + let(:ial) { Idp::Constants::IAL1 } + it { expect(ial_context.default_to_ial2?).to eq(false) } + end + + context 'when the service provider is ial1 and ial is not requested' do + let(:sp_ial) { Idp::Constants::IAL1 } + let(:ial) { nil } + it { expect(ial_context.default_to_ial2?).to eq(false) } + end + + context 'when the service provider is ial2 and ial2 is requested' do + let(:sp_ial) { Idp::Constants::IAL2 } + let(:ial) { Idp::Constants::IAL2 } + it { expect(ial_context.default_to_ial2?).to eq(false) } + end + + context 'when the service provider is ial2 and ial is not requested' do + let(:sp_ial) { Idp::Constants::IAL2 } + let(:ial) { nil } + it { expect(ial_context.default_to_ial2?).to eq(true) } + end + + context 'when the service provider is ial2 and ial1 is requested' do + let(:sp_ial) { Idp::Constants::IAL2 } + let(:ial) { Idp::Constants::IAL1 } + it { expect(ial_context.default_to_ial2?).to eq(false) } + end + + context 'when the service provider is ial2 strict and ial1 is requested' do + let(:sp_ial) { Idp::Constants::IAL2_STRICT } + let(:ial) { Idp::Constants::IAL1 } + it { expect(ial_context.default_to_ial2?).to eq(false) } + end + + context 'when the service provider is ial2 strict and ial2 is requested' do + let(:sp_ial) { Idp::Constants::IAL2_STRICT } + let(:ial) { Idp::Constants::IAL2 } + it { expect(ial_context.default_to_ial2?).to eq(false) } + end + + context 'when the service provider is ial2 strict and ial2 is requested' do + let(:sp_ial) { Idp::Constants::IAL2_STRICT } + let(:ial) { nil } + it { expect(ial_context.default_to_ial2?).to eq(true) } + end + end + describe '#ialmax_requested?' do context 'when ialmax is requested' do let(:ial) { Idp::Constants::IAL_MAX } @@ -78,6 +128,54 @@ end describe '#ial2_or_greater?' do + context 'when the service provider is ial1 and ial1 is requested' do + let(:sp_ial) { Idp::Constants::IAL1 } + let(:ial) { Idp::Constants::IAL1 } + it { expect(ial_context.ial2_or_greater?).to eq(false) } + end + + context 'when the service provider is ial1 and ial is not requested' do + let(:sp_ial) { Idp::Constants::IAL1 } + let(:ial) { nil } + it { expect(ial_context.ial2_or_greater?).to eq(false) } + end + + context 'when the service provider is ial2 and ial2 is requested' do + let(:sp_ial) { Idp::Constants::IAL2 } + let(:ial) { Idp::Constants::IAL2 } + it { expect(ial_context.ial2_or_greater?).to eq(true) } + end + + context 'when the service provider is ial2 and ial is not requested' do + let(:sp_ial) { Idp::Constants::IAL2 } + let(:ial) { nil } + it { expect(ial_context.ial2_or_greater?).to eq(true) } + end + + context 'when the service provider is ial2 and ial1 is requested' do + let(:sp_ial) { Idp::Constants::IAL2 } + let(:ial) { Idp::Constants::IAL1 } + it { expect(ial_context.ial2_or_greater?).to eq(false) } + end + + context 'when the service provider is ial2 strict and ial1 is requested' do + let(:sp_ial) { Idp::Constants::IAL2_STRICT } + let(:ial) { Idp::Constants::IAL1 } + it { expect(ial_context.ial2_or_greater?).to eq(false) } + end + + context 'when the service provider is ial2 strict and ial2 is requested' do + let(:sp_ial) { Idp::Constants::IAL2_STRICT } + let(:ial) { Idp::Constants::IAL2 } + it { expect(ial_context.ial2_or_greater?).to eq(true) } + end + + context 'when the service provider is ial2 strict and ial2 is requested' do + let(:sp_ial) { Idp::Constants::IAL2_STRICT } + let(:ial) { nil } + it { expect(ial_context.ial2_or_greater?).to eq(true) } + end + context 'when ialmax is requested' do let(:ial) { Idp::Constants::IAL_MAX } it { expect(ial_context.ial2_or_greater?).to eq(false) } diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index 1e5fb6f7397..39f30242f63 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -2,6 +2,8 @@ require 'ostruct' describe Idv::Agent do + include IdvHelper + let(:bad_phone) do IdentityIdpFunctions::AddressMockClient::UNVERIFIABLE_PHONE_NUMBER end @@ -22,6 +24,7 @@ agent.proof_resolution( document_capture_session, should_proof_state_id: true, trace_id: trace_id ) + result = document_capture_session.load_proofing_result.result expect(result[:errors][:ssn]).to eq ['Unverified SSN.'] expect(result[:context][:stages]).to_not include( @@ -52,6 +55,8 @@ context 'proofing partial date of birth' do before do allow(AppConfig.env).to receive(:proofing_send_partial_dob).and_return('true') + allow(AppConfig.env).to receive(:ruby_workers_enabled). + and_return('false') end it 'passes dob_year_only to the proofing function' do @@ -87,6 +92,7 @@ agent.proof_resolution( document_capture_session, should_proof_state_id: false, trace_id: trace_id ) + result = document_capture_session.load_proofing_result.result expect(result[:context][:stages]).to_not include( state_id: 'StateIdMock', @@ -110,20 +116,6 @@ timed_out: true, ) end - - it 'passes the right lexisnexis configs' do - expect(LambdaJobs::Runner).to receive(:new).and_wrap_original do |impl, args| - lexisnexis_config = args.dig(:in_process_config, :lexisnexis_config) - expect(lexisnexis_config).to include(:instant_verify_workflow) - expect(lexisnexis_config).to_not include(:phone_finder_workflow) - - impl.call(args) - end - - agent.proof_resolution( - document_capture_session, should_proof_state_id: true, trace_id: trace_id - ) - end end describe '#proof_address' do @@ -144,18 +136,6 @@ expect(result[:context][:stages]).to include({ address: 'AddressMock' }) expect(result[:success]).to eq false end - - it 'passes the right lexisnexis configs' do - expect(LambdaJobs::Runner).to receive(:new).and_wrap_original do |impl, args| - lexisnexis_config = args.dig(:in_process_config, :lexisnexis_config) - expect(lexisnexis_config).to_not include(:instant_verify_workflow) - expect(lexisnexis_config).to include(:phone_finder_workflow) - - impl.call(args) - end - - agent.proof_address(document_capture_session, trace_id: trace_id) - end end end end diff --git a/spec/services/push_notification/http_push_spec.rb b/spec/services/push_notification/http_push_spec.rb index 13a99a693fe..efa4b44ac36 100644 --- a/spec/services/push_notification/http_push_spec.rb +++ b/spec/services/push_notification/http_push_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require 'rails_helper' RSpec.describe PushNotification::HttpPush do include Rails.application.routes.url_helpers @@ -22,20 +22,73 @@ ) end let(:now) { Time.zone.now } + let(:risc_notifications_sqs_enabled) { 'false' } + let(:sqs_client) { Aws::SQS::Client.new(stub_responses: true) } + let(:sqs_queue_url) { 'https://some-queue-url.example.com/example' } subject(:http_push) { PushNotification::HttpPush.new(event, now: now) } + before do + allow(AppConfig.env).to receive(:risc_notifications_sqs_enabled). + and_return(risc_notifications_sqs_enabled) + allow(Identity::Hostdata).to receive(:env).and_return('dev') + + allow(http_push).to receive(:sqs_client).and_return(sqs_client) + + sqs_client.stub_responses( + :get_queue_url, + { queue_url: sqs_queue_url }, + ) + end + describe '#deliver' do subject(:deliver) { http_push.deliver } - it 'makes an HTTP post to service providers with a push_notification_url' do - stub_request(:post, sp_with_push_url.push_notification_url). - with do |request| - expect(request.headers['Content-Type']).to eq('application/secevent+jwt') - expect(request.headers['Accept']).to eq('application/json') + context 'when the SQS queue is disabled' do + let(:risc_notifications_sqs_enabled) { 'false' } + + it 'makes an HTTP post to service providers with a push_notification_url' do + stub_request(:post, sp_with_push_url.push_notification_url). + with do |request| + expect(request.headers['Content-Type']).to eq('application/secevent+jwt') + expect(request.headers['Accept']).to eq('application/json') + + payload, headers = JWT.decode( + request.body, + AppArtifacts.store.oidc_public_key, + true, + algorithm: 'RS256', + ) + + expect(headers['typ']).to eq('secevent+jwt') + + expect(payload['iss']).to eq(root_url) + expect(payload['iat']).to eq(now.to_i) + expect(payload['exp']).to eq((now + 12.hours).to_i) + expect(payload['aud']).to eq(sp_with_push_url.push_notification_url) + expect(payload['events']).to eq(event.event_type => event.payload.as_json) + end + + deliver + end + end + + context 'when the SQS queue is enabled' do + let(:risc_notifications_sqs_enabled) { 'true' } + + it 'posts to the SQS queue' do + expect(sqs_client).to receive(:get_queue_url). + with(queue_name: 'dev-risc-notifications'). + and_call_original + + expect(sqs_client).to receive(:send_message) do |queue_url:, message_body:| + expect(queue_url).to eq(sqs_queue_url) + + message = JSON.parse(message_body, symbolize_names: true) + expect(message[:push_notification_url]).to eq(sp_with_push_url.push_notification_url) payload, headers = JWT.decode( - request.body, + message[:jwt], AppArtifacts.store.oidc_public_key, true, algorithm: 'RS256', @@ -50,7 +103,8 @@ expect(payload['events']).to eq(event.event_type => event.payload.as_json) end - deliver + deliver + end end context 'with an event that sends agency-specific iss_sub' do @@ -58,11 +112,35 @@ let(:agency_uuid) { AgencyIdentityLinker.new(sp_with_push_url_identity).link_identity.uuid } - it 'sends the agency-specific uuid' do - stub_request(:post, sp_with_push_url.push_notification_url). - with do |request| + context 'when the SQS queue is disabled' do + let(:risc_notifications_sqs_enabled) { 'false' } + + it 'sends the agency-specific uuid' do + stub_request(:post, sp_with_push_url.push_notification_url). + with do |request| + payload, _headers = JWT.decode( + request.body, + AppArtifacts.store.oidc_public_key, + true, + algorithm: 'RS256', + ) + + expect(payload['events'][event.event_type]['subject']['sub']).to eq(agency_uuid) + end + + deliver + end + end + + context 'when the SQS queue is enabled' do + let(:risc_notifications_sqs_enabled) { 'true' } + + it 'sends the agency-specific uuid' do + expect(sqs_client).to receive(:send_message) do |queue_url:, message_body:| + message = JSON.parse(message_body, symbolize_names: true) + payload, _headers = JWT.decode( - request.body, + message[:jwt], AppArtifacts.store.oidc_public_key, true, algorithm: 'RS256', @@ -71,7 +149,8 @@ expect(payload['events'][event.event_type]['subject']['sub']).to eq(agency_uuid) end - deliver + deliver + end end end @@ -119,10 +198,24 @@ RevokeServiceProviderConsent.new(identity).call end - it 'does not notify that SP' do - deliver + context 'when the SQS queue is disabled' do + let(:risc_notifications_sqs_enabled) { 'false' } + + it 'does not notify that SP' do + deliver - expect(WebMock).not_to have_requested(:get, sp_with_push_url.push_notification_url) + expect(WebMock).not_to have_requested(:get, sp_with_push_url.push_notification_url) + end + end + + context 'when the SQS queue is enabled' do + let(:risc_notifications_sqs_enabled) { 'true' } + + it 'does not notify that SP' do + expect(sqs_client).to_not receive(:send_message) + + deliver + end end end end diff --git a/spec/services/send_sign_up_email_confirmation_spec.rb b/spec/services/send_sign_up_email_confirmation_spec.rb index 334327f299e..d1f82c7c831 100644 --- a/spec/services/send_sign_up_email_confirmation_spec.rb +++ b/spec/services/send_sign_up_email_confirmation_spec.rb @@ -23,7 +23,7 @@ it 'sends the user an email with a confirmation link and the request id' do email_address.update!(confirmed_at: Time.zone.now) mail = double - expect(mail).to receive(:deliver_later) + expect(mail).to receive(:deliver_now) expect(UserMailer).to receive(:email_confirmation_instructions).with( user, email_address.email, @@ -38,7 +38,7 @@ context 'when resetting a password' do it 'sends an email with a link to try another email if the current email is unconfirmed' do mail = double - expect(mail).to receive(:deliver_later) + expect(mail).to receive(:deliver_now) expect(UserMailer).to receive(:unconfirmed_email_instructions).with( user, email_address.email, @@ -79,7 +79,7 @@ user.reload mail = double - expect(mail).to receive(:deliver_later) + expect(mail).to receive(:deliver_now) expect(UserMailer).to receive(:email_confirmation_instructions).with( user, email_address.email, confirmation_token, instance_of(Hash) ).and_return(mail) diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index 68d338da51b..abe5b1cb704 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -9,6 +9,8 @@ def attach_and_submit_images if javascript_enabled? && !selfie_required? click_on 'Submit' # Wait for the background image job to finish and success flash to appear before continuing + expect(page).to have_content(t('doc_auth.headings.interstitial')) + Capybara.using_wait_time(3) do expect(page).to have_content(t('doc_auth.headings.capture_complete')) end diff --git a/spec/support/features/idv_helper.rb b/spec/support/features/idv_helper.rb index aa8012bf6a2..47a82fa609d 100644 --- a/spec/support/features/idv_helper.rb +++ b/spec/support/features/idv_helper.rb @@ -1,4 +1,6 @@ module IdvHelper + include ActiveJob::TestHelper + def self.included(base) base.class_eval { include JavascriptDriverHelper } end diff --git a/spec/views/idv/doc_auth/_back.html.erb_spec.rb b/spec/views/idv/doc_auth/_back.html.erb_spec.rb index d958670d545..75e397c1072 100644 --- a/spec/views/idv/doc_auth/_back.html.erb_spec.rb +++ b/spec/views/idv/doc_auth/_back.html.erb_spec.rb @@ -29,9 +29,7 @@ it 'renders' do expect(subject).to have_selector("form[action='#{idv_doc_auth_step_path(step: 'redo_ssn')}']") expect(subject).to have_selector('input[name="_method"][value="put"]', visible: false) - expect(subject).to have_selector( - ".btn[type='submit'][value='#{'‹ ' + t('forms.buttons.back')}']", - ) + expect(subject).to have_selector("[type='submit'][value='#{'‹ ' + t('forms.buttons.back')}']") end it_behaves_like 'back link with class' diff --git a/yarn.lock b/yarn.lock index fc2984c9ebd..f431ae34b34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4834,10 +4834,10 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -identity-style-guide@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/identity-style-guide/-/identity-style-guide-5.0.1.tgz#ada95337b503ed8ef46091696ba8a44a6061f0ac" - integrity sha512-eailVodxMJCw51dFFUXOwqNvvKgCHnZt6xXYn+9Cizx2YrbzFcOkGu/OIiOGes/jBINnq4upxyUZStkv9IhVFw== +identity-style-guide@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/identity-style-guide/-/identity-style-guide-5.0.3.tgz#2cc1391683a729749a8606dcc6a2bf69b58f7e45" + integrity sha512-OSoYQ+2yCE6zERwjSzkvp9XR/aEAvQzzPUQGCUnGskroa2O/FSM9p38w0YO0zH5+GGZUZqMWKYzeph96r01j2g== dependencies: uswds "^2.9.0"