From 7ea655cd6f60200b8b37f95cb807b5aa522e4fae Mon Sep 17 00:00:00 2001 From: Jonathan Hooper Date: Tue, 14 Jan 2025 12:25:14 -0500 Subject: [PATCH] Revert "Merge pull request #11748 from 18F/stages/rc-2025-01-14" This reverts commit 4e1fbd4a2647ac10c5d089edd8b2c78e5509c6d7, reversing changes made to 6ece32f6fba11722a8550be7c3940dd5da311400. --- Brewfile | 1 + Gemfile | 2 +- Gemfile.lock | 8 +- Makefile | 1 - .../concerns/idv/verify_info_concern.rb | 7 - app/controllers/saml_idp_controller.rb | 22 + app/controllers/users/sessions_controller.rb | 40 +- app/forms/gpo_verify_form.rb | 1 - .../components/acuant-capture.tsx | 17 +- .../packages/phone-input/package.json | 2 +- app/jobs/resolution_proofing_job.rb | 39 +- app/models/document_capture_session.rb | 1 - app/services/analytics_events.rb | 47 +- .../doc_auth/lexis_nexis/doc_pii_reader.rb | 2 +- .../encrypted_doc_storage/doc_writer.rb | 35 -- .../encrypted_doc_storage/local_storage.rb | 19 - .../encrypted_doc_storage/s3_storage.rb | 27 - .../idv/aamva_state_maintenance_window.rb | 138 ++--- app/services/idv/agent.rb | 6 +- app/services/idv/proofing_components.rb | 5 +- app/services/proofing/aamva/applicant.rb | 9 +- .../in_person/ready_to_verify/show.html.erb | 2 +- .../_in_person_ready_to_verify.html.erb | 2 +- bin/summarize-user-events | 286 ---------- config/application.yml.default | 7 +- config/initializers/ab_tests.rb | 4 +- config/locales/en.yml | 12 +- config/locales/es.yml | 14 +- config/locales/fr.yml | 16 +- config/locales/zh.yml | 16 +- ...06232958_drop_proofing_components_table.rb | 23 - db/schema.rb | 22 +- dockerfiles/nginx.Dockerfile | 2 +- lib/analytics_events_documenter.rb | 18 +- .../account_deletion_matcher.rb | 74 --- lib/event_summarizer/example_matcher.rb | 28 - lib/event_summarizer/idv_matcher.rb | 538 ------------------ .../vendor_result_evaluators/aamva.rb | 129 ----- .../instant_verify.rb | 79 --- .../vendor_result_evaluators/true_id.rb | 29 - lib/identity_config.rb | 6 +- package.json | 4 + spec/bin/summarize-user-events_spec.rb | 187 ------ spec/config/initializers/ab_tests_spec.rb | 13 +- .../idv/by_mail/enter_code_controller_spec.rb | 8 - .../in_person/verify_info_controller_spec.rb | 3 - spec/controllers/saml_idp_controller_spec.rb | 86 ++- .../users/sessions_controller_spec.rb | 69 +-- spec/features/visitors/bad_password_spec.rb | 12 +- .../components/acuant-capture-spec.jsx | 40 -- spec/jobs/resolution_proofing_job_spec.rb | 188 ++---- spec/lib/analytics_events_documenter_spec.rb | 43 +- spec/lib/event_summarizer/idv_matcher_spec.rb | 56 -- .../instant_verify_spec.rb | 91 --- .../responses/true_id_response_spec.rb | 19 +- .../encrypted_doc_storage/doc_writer_spec.rb | 52 -- .../local_storage_spec.rb | 30 - .../encrypted_doc_storage/s3_storage_spec.rb | 31 - spec/services/idv/agent_spec.rb | 32 -- spec/services/idv/proofing_components_spec.rb | 21 +- .../services/proofing/aamva/applicant_spec.rb | 21 +- spec/support/shared_examples/sign_in.rb | 4 +- yarn.lock | 42 +- 63 files changed, 396 insertions(+), 2392 deletions(-) delete mode 100644 app/services/encrypted_doc_storage/doc_writer.rb delete mode 100644 app/services/encrypted_doc_storage/local_storage.rb delete mode 100644 app/services/encrypted_doc_storage/s3_storage.rb delete mode 100755 bin/summarize-user-events delete mode 100644 db/primary_migrate/20250106232958_drop_proofing_components_table.rb delete mode 100644 lib/event_summarizer/account_deletion_matcher.rb delete mode 100644 lib/event_summarizer/example_matcher.rb delete mode 100644 lib/event_summarizer/idv_matcher.rb delete mode 100644 lib/event_summarizer/vendor_result_evaluators/aamva.rb delete mode 100644 lib/event_summarizer/vendor_result_evaluators/instant_verify.rb delete mode 100644 lib/event_summarizer/vendor_result_evaluators/true_id.rb delete mode 100644 spec/bin/summarize-user-events_spec.rb delete mode 100644 spec/lib/event_summarizer/idv_matcher_spec.rb delete mode 100644 spec/lib/event_summarizer/vendor_result_evaluators/instant_verify_spec.rb delete mode 100644 spec/services/encrypted_doc_storage/doc_writer_spec.rb delete mode 100644 spec/services/encrypted_doc_storage/local_storage_spec.rb delete mode 100644 spec/services/encrypted_doc_storage/s3_storage_spec.rb diff --git a/Brewfile b/Brewfile index e62947e5bfc..f087878fea3 100644 --- a/Brewfile +++ b/Brewfile @@ -2,5 +2,6 @@ brew 'postgresql@14' brew 'redis' brew 'node@22' brew 'yarn' +brew 'openssl@1.1' brew 'jq' cask 'chromedriver' diff --git a/Gemfile b/Gemfile index bea078bd2b9..04e1f823c9d 100644 --- a/Gemfile +++ b/Gemfile @@ -74,7 +74,7 @@ gem 'rqrcode' gem 'ruby-progressbar' gem 'ruby-saml' gem 'safe_target_blank', '>= 1.0.2' -gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.5-18f' +gem 'saml_idp', github: '18F/saml_idp', tag: '0.23.4-18f' gem 'scrypt' gem 'simple_form', '>= 5.0.2' gem 'stringex', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 917879269d1..e152e8df347 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -36,10 +36,10 @@ GIT GIT remote: https://github.com/18F/saml_idp.git - revision: bdf8e1f93707e413ecbd0f48d803e18812e19f90 - tag: 0.23.5-18f + revision: e5d876cf10ce9b39bba0cc523d06c4dda1af5124 + tag: 0.23.4-18f specs: - saml_idp (0.23.5.pre.18f) + saml_idp (0.23.4.pre.18f) activesupport builder faraday @@ -477,7 +477,7 @@ GEM pg (1.5.9) pg_query (5.1.0) google-protobuf (>= 3.22.3) - phonelib (0.10.3) + phonelib (0.9.1) pkcs11 (0.3.4) premailer (1.27.0) addressable diff --git a/Makefile b/Makefile index 4ee599d6467..a2aba78f2db 100644 --- a/Makefile +++ b/Makefile @@ -309,7 +309,6 @@ public/api/_analytics-events.json: .yardoc .yardoc/objects/root.dat .yardoc .yardoc/objects/root.dat: app/services/analytics_events.rb bundle exec yard doc \ - --no-progress \ --fail-on-warning \ --type-tag identity.idp.previous_event_name:"Previous Event Name" \ --no-output \ diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index be02dac4b43..cfc9b4f37b7 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Idv - # @attr idv_session [Idv::Session] module VerifyInfoConcern extend ActiveSupport::Concern @@ -40,12 +39,6 @@ def shared_update threatmetrix_session_id: idv_session.threatmetrix_session_id, request_ip: request.remote_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress?, - proofing_components: ProofingComponents.new( - user: current_user, - idv_session:, - session:, - user_session:, - ), ) return true diff --git a/app/controllers/saml_idp_controller.rb b/app/controllers/saml_idp_controller.rb index bf3e3dbf9ca..5c7ab57cbe5 100644 --- a/app/controllers/saml_idp_controller.rb +++ b/app/controllers/saml_idp_controller.rb @@ -150,6 +150,13 @@ def capture_analytics if result.success? && saml_request.signed? analytics_payload[:cert_error_details] = saml_request.cert_errors + + # analytics to determine if turning on SHA256 validation will break + # existing partners + if certs_different? + analytics_payload[:certs_different] = true + analytics_payload[:sha256_matching_cert] = sha256_alg_matching_cert_serial + end end analytics.saml_auth(**analytics_payload) @@ -161,6 +168,21 @@ def matching_cert_serial nil end + def sha256_alg_matching_cert + # if sha256_alg_matching_cert is nil, fallback to the "first" cert + saml_request.sha256_validation_matching_cert || + saml_request_service_provider&.ssl_certs&.first + rescue SamlIdp::XMLSecurity::SignedDocument::ValidationError + end + + def sha256_alg_matching_cert_serial + sha256_alg_matching_cert&.serial&.to_s + end + + def certs_different? + encryption_cert != sha256_alg_matching_cert + end + def log_external_saml_auth_request return unless external_saml_request? diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 8b6a271fe7f..2b7570b4501 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -18,7 +18,7 @@ class SessionsController < Devise::SessionsController before_action :store_sp_metadata_in_session, only: [:new] before_action :check_user_needs_redirect, only: [:new] before_action :apply_secure_headers_override, only: [:new, :create] - before_action :clear_session_sign_in_failure_count_if_window_expired, only: [:create] + before_action :clear_session_bad_password_count_if_window_expired, only: [:create] before_action :set_analytics_user_from_params, only: :create before_action :allow_csp_recaptcha_src, if: :recaptcha_enabled? @@ -37,14 +37,12 @@ def new def create session[:sign_in_flow] = :sign_in - return process_rate_limited if session_sign_in_failure_count_max_exceeded? + return process_rate_limited if session_bad_password_count_max_exceeded? return process_locked_out_user if current_user && user_locked_out?(current_user) return process_rate_limited if rate_limited? - - rate_limit_password_failure = true - return process_failed_captcha unless recaptcha_response.success? || log_captcha_failures_only? + rate_limit_password_failure = true self.resource = warden.authenticate!(auth_options) handle_valid_authentication ensure @@ -67,21 +65,21 @@ def analytics_user private - def clear_session_sign_in_failure_count_if_window_expired - locked_at = session[:max_sign_in_failures_at] - window = IdentityConfig.store.max_sign_in_failures_window_in_seconds + def clear_session_bad_password_count_if_window_expired + locked_at = session[:max_bad_passwords_at] + window = IdentityConfig.store.max_bad_passwords_window_in_seconds return if locked_at.nil? || (locked_at + window) > Time.zone.now.to_i - [:max_sign_in_failures_at, :sign_in_failure_count].each { |x| session.delete(x) } + [:max_bad_passwords_at, :bad_password_count].each { |x| session.delete(x) } end - def session_sign_in_failure_count_max_exceeded? - session[:sign_in_failure_count].to_i >= IdentityConfig.store.max_sign_in_failures + def session_bad_password_count_max_exceeded? + session[:bad_password_count].to_i >= IdentityConfig.store.max_bad_passwords end - def increment_session_sign_in_failure_count - session[:sign_in_failure_count] = session[:sign_in_failure_count].to_i + 1 - return unless session_sign_in_failure_count_max_exceeded? - session[:max_sign_in_failures_at] ||= Time.zone.now.to_i + def increment_session_bad_password_count + session[:bad_password_count] = session[:bad_password_count].to_i + 1 + return unless session_bad_password_count_max_exceeded? + session[:max_bad_passwords_at] ||= Time.zone.now.to_i end def process_rate_limited @@ -89,16 +87,16 @@ def process_rate_limited warden.lock! flash[:error] = t( - 'errors.sign_in.sign_in_failure_limit', + 'errors.sign_in.bad_password_limit', time_left: locked_out_time_remaining, ) redirect_to root_url end def locked_out_time_remaining - if session[:max_sign_in_failures_at] - locked_at = session[:max_sign_in_failures_at] - window = IdentityConfig.store.max_sign_in_failures_window_in_seconds.seconds + if session[:max_bad_passwords_at] + locked_at = session[:max_bad_passwords_at] + window = IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds time_lockout_expires = Time.zone.at(locked_at) + window else time_lockout_expires = rate_limiter&.expires_at || Time.zone.now @@ -190,7 +188,7 @@ def process_locked_out_user def handle_invalid_authentication rate_limiter&.increment! - increment_session_sign_in_failure_count + increment_session_bad_password_count end def handle_valid_authentication @@ -225,7 +223,7 @@ def track_authentication_attempt rate_limited: rate_limited?, captcha_validation_performed: captcha_validation_performed?, valid_captcha_result: recaptcha_response.success?, - sign_in_failure_count: session[:sign_in_failure_count].to_i, + bad_password_count: session[:bad_password_count].to_i, sp_request_url_present: sp_session[:request_url].present?, remember_device: remember_device_cookie.present?, new_device: success ? new_device? : nil, diff --git a/app/forms/gpo_verify_form.rb b/app/forms/gpo_verify_form.rb index 6dbbf09b6e8..150ae095719 100644 --- a/app/forms/gpo_verify_form.rb +++ b/app/forms/gpo_verify_form.rb @@ -49,7 +49,6 @@ def submit(is_enhanced_ipp) pii_like_keypaths: [[:errors, :otp], [:error_details, :otp]], pending_in_person_enrollment: !!pending_profile&.in_person_enrollment&.pending?, fraud_check_failed: fraud_check_failed, - initiating_service_provider: pending_profile&.initiating_service_provider_issuer, }, ) end diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 9f4cc97d979..e40bee7fb58 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -140,6 +140,11 @@ interface AcuantCaptureProps { */ const NBSP_UNICODE = '\u00A0'; +/** + * A noop function. + */ +const noop = () => {}; + /** * Returns true if the given Acuant capture failure was caused by the user declining access to the * camera, or false otherwise. @@ -486,16 +491,6 @@ function AcuantCapture( } } - /** - * Responds to a drag and drop file upload by either preventing the default action - * or allowing the file to be uploaded - */ - function startDragDropUpload(event) { - if (!allowUpload) { - event.preventDefault(); - } - } - /** * Responds to a click by starting capture if supported in the environment, or triggering the * default file picker prompt. The click event may originate from the file input itself, or @@ -788,7 +783,7 @@ function AcuantCapture( errorMessage={ownErrorMessage ?? errorMessage} isValuePending={hasStartedCropping} onClick={withLoggedClick('placeholder')(startCaptureOrTriggerUpload)} - onDrop={withLoggedClick('placeholder', { isDrop: true })(startDragDropUpload)} + onDrop={withLoggedClick('placeholder', { isDrop: true })(noop)} onChange={onUpload} onError={() => setOwnErrorMessage(null)} /> diff --git a/app/javascript/packages/phone-input/package.json b/app/javascript/packages/phone-input/package.json index 49a02dcfdbc..76fccbd037b 100644 --- a/app/javascript/packages/phone-input/package.json +++ b/app/javascript/packages/phone-input/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "dependencies": { "intl-tel-input": "^24.5.0", - "libphonenumber-js": "^1.11.17" + "libphonenumber-js": "^1.11.4" }, "sideEffects": [ "./index.ts" diff --git a/app/jobs/resolution_proofing_job.rb b/app/jobs/resolution_proofing_job.rb index ce9e80c1469..66c9d635feb 100644 --- a/app/jobs/resolution_proofing_job.rb +++ b/app/jobs/resolution_proofing_job.rb @@ -25,7 +25,7 @@ def perform( service_provider_issuer: nil, threatmetrix_session_id: nil, request_ip: nil, - proofing_components: nil, + proofing_components: nil, # rubocop:disable Lint/UnusedMethodArgument # DEPRECATED ARGUMENTS should_proof_state_id: false # rubocop:disable Lint/UnusedMethodArgument ) @@ -75,7 +75,7 @@ def perform( timing: timer.results, ) - if use_shadow_mode?(user:, proofing_components:) + if use_shadow_mode?(user:) SocureShadowModeProofingJob.perform_later( document_capture_session_result_id: document_capture_session&.result_id, encrypted_arguments:, @@ -86,22 +86,15 @@ def perform( end end - # @param user [User] - # @param proofing_components [Hash,nil] - def use_shadow_mode?(user:, proofing_components:) - # Let idv_socure_shadow_mode_enabled setting control shadow mode globally - disabled_globally = !IdentityConfig.store.idv_socure_shadow_mode_enabled - return false if disabled_globally - - # If the user went through Socure docv, they are already a Socure user and - # are thus eligible for shadow mode. - enabled_for_docv_users = - IdentityConfig.store.idv_socure_shadow_mode_enabled_for_docv_users - is_docv_user = proofing_components&.dig(:document_check) == Idp::Constants::Vendors::SOCURE - return true if enabled_for_docv_users && is_docv_user - - # Otherwise fall back to A/B test - shadow_mode_ab_test_bucket(user:) == :socure_shadow_mode_for_non_docv_users + def use_shadow_mode?(user:) + IdentityConfig.store.idv_socure_shadow_mode_enabled && + AbTests::SOCURE_IDV_SHADOW_MODE.bucket( + request: nil, + service_provider: nil, + session: nil, + user:, + user_session: nil, + ) == :shadow_mode_enabled end private @@ -157,14 +150,4 @@ def logger_info_hash(hash) def progressive_proofer @progressive_proofer ||= Proofing::Resolution::ProgressiveProofer.new end - - def shadow_mode_ab_test_bucket(user:) - AbTests::SOCURE_IDV_SHADOW_MODE_FOR_NON_DOCV_USERS.bucket( - request: nil, - service_provider: nil, - session: nil, - user:, - user_session: nil, - ) - end end diff --git a/app/models/document_capture_session.rb b/app/models/document_capture_session.rb index 47b618a6bb6..7caa58febf5 100644 --- a/app/models/document_capture_session.rb +++ b/app/models/document_capture_session.rb @@ -11,7 +11,6 @@ def load_result EncryptedRedisStructStorage.load(result_id, type: DocumentCaptureSessionResult) end - # @param doc_auth_response [DocAuth::Response] def store_result_from_response(doc_auth_response) session_result = load_result || DocumentCaptureSessionResult.new( id: generate_result_id, diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 1ae73fc8b8e..17ee037292a 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -125,7 +125,7 @@ def account_reset_cancel_token_validation( # @param [String] user_id # @param [Integer, nil] account_age_in_days number of days since the account was confirmed # @param [Time] account_confirmed_at date that account creation was confirmed - # (rounded) or nil if the account was not confirmed + # (rounded) or nil if the account was not confirmed # @param [Hash] mfa_method_counts Hash of MFA method with the number of that method on the account # @param [Boolean] identity_verified if the deletion occurs on a verified account # @param [Hash] errors Errors resulting from form validation @@ -244,7 +244,7 @@ def account_visit # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [String] user_id User the email is linked to # @param [Boolean] from_select_email_flow Whether email was added as part of partner email - # selection. + # selection. # A user has clicked the confirmation link in an email def add_email_confirmation( user_id:, @@ -270,7 +270,7 @@ def add_email_confirmation( # @param [Hash] error_details Details for errors that occurred in unsuccessful submission # @param [String] domain_name Domain name of email address submitted # @param [Boolean] in_select_email_flow Whether email is being added as part of partner email - # selection. + # selection. # Tracks request for adding new emails to an account def add_email_request( success:, @@ -509,11 +509,11 @@ def edit_password_visit(required_password_change: false, **extra) # @param [Boolean] rate_limited Whether the user has exceeded user IP rate limiting # @param [Boolean] valid_captcha_result Whether user passed the reCAPTCHA check or was exempt # @param [Boolean] captcha_validation_performed Whether a reCAPTCHA check was performed - # @param [String] sign_in_failure_count represents number of prior login failures + # @param [String] bad_password_count represents number of prior login failures # @param [Boolean] sp_request_url_present if was an SP request URL in the session # @param [Boolean] remember_device if the remember device cookie was present # @param [Boolean, nil] new_device Whether the user is authenticating from a new device. Nil if - # the attempt was unsuccessful, since it cannot be known whether it's a new device. + # the attempt was unsuccessful, since it cannot be known whether it's a new device. # Tracks authentication attempts at the email/password screen def email_and_password_auth( success:, @@ -521,7 +521,7 @@ def email_and_password_auth( rate_limited:, valid_captcha_result:, captcha_validation_performed:, - sign_in_failure_count:, + bad_password_count:, sp_request_url_present:, remember_device:, new_device:, @@ -536,7 +536,7 @@ def email_and_password_auth( rate_limited:, valid_captcha_result:, captcha_validation_performed:, - sign_in_failure_count:, + bad_password_count:, sp_request_url_present:, remember_device:, new_device:, @@ -1086,7 +1086,7 @@ def idv_camera_info_error(error:, **_extra) # @param ["hybrid","standard"] flow_path Document capture user flow # @param [Array] camera_info Information on the users cameras max resolution - # as captured by the browser + # as captured by the browser def idv_camera_info_logged(flow_path:, camera_info:, **_extra) track_event( :idv_camera_info_logged, flow_path: flow_path, camera_info: camera_info @@ -1454,7 +1454,7 @@ def idv_doc_auth_exception_visited(step_name:, remaining_submit_attempts:, **ext # @param [String] side the side of the image submission # @param [Integer] submit_attempts Times that user has tried submitting (previously called - # "attempts") + # "attempts") # @param [Integer] remaining_submit_attempts (previously called "remaining_attempts") # @param ["hybrid","standard"] flow_path Document capture user flow # @param [String] liveness_checking_required Whether or not the selfie is required @@ -2984,7 +2984,7 @@ def idv_in_person_prepare_visited(flow_path:, opted_in_to_in_person_proofing:, * # @param [String] analytics_id # @param [Boolean] skip_hybrid_handoff Whether skipped hybrid handoff A/B test is active # @param [Boolean] opted_in_to_in_person_proofing User opted into in person proofing - # address page visited + # address page visited def idv_in_person_proofing_address_visited( flow_path:, step:, @@ -5253,7 +5253,6 @@ def idv_usps_auth_token_refresh_job_started(**extra) # @param [Integer] which_letter Sorted by enqueue time, which letter had this code # @param [Integer] letter_count How many letters did the user enqueue for this profile # @param [Integer] profile_age_in_seconds How many seconds have passed since profile created - # @param [String] initiating_service_provider The initiating service provider issuer # @param [Integer] submit_attempts Number of attempts to enter a correct code # (previously called "attempts") # @param [Boolean] pending_in_person_enrollment @@ -5268,7 +5267,6 @@ def idv_verify_by_mail_enter_code_submitted( which_letter:, letter_count:, profile_age_in_seconds:, - initiating_service_provider:, submit_attempts:, pending_in_person_enrollment:, fraud_check_failed:, @@ -5284,7 +5282,6 @@ def idv_verify_by_mail_enter_code_submitted( which_letter:, letter_count:, profile_age_in_seconds:, - initiating_service_provider:, submit_attempts:, pending_in_person_enrollment:, fraud_check_failed:, @@ -6442,7 +6439,7 @@ def phone_change_viewed # @param [Boolean] success # @param [Integer] phone_configuration_id - # Tracks a phone number deletion event + # tracks a phone number deletion event def phone_deletion(success:, phone_configuration_id:, **extra) track_event( 'Phone Number Deletion: Submitted', @@ -6485,7 +6482,7 @@ def piv_cac_delete_submitted( # @param [Hash] errors Errors resulting from form validation # @param [String, nil] key_id PIV/CAC key_id from PKI service # @param [Boolean] new_device Whether the user is authenticating from a new device - # Tracks piv cac login event + # tracks piv cac login event def piv_cac_login(success:, errors:, key_id:, new_device:, **extra) track_event( :piv_cac_login, @@ -6903,8 +6900,12 @@ def rules_of_use_visit # @param [Boolean] request_signed # @param [String] matching_cert_serial matches the request certificate in a successful, signed # request + # @param [Boolean] certs_different Whether the matching cert changes when SHA256 validations + # are turned on in the saml_idp gem # @param [Hash] cert_error_details Details for errors that occurred because of an invalid # signature + # @param [String] sha256_matching_cert serial of the cert that matches when sha256 validations + # are turned on # @param [String] unknown_authn_contexts space separated list of unknown contexts def saml_auth( success:, @@ -6922,6 +6923,8 @@ def saml_auth( matching_cert_serial:, error_details: nil, cert_error_details: nil, + certs_different: nil, + sha256_matching_cert: nil, unknown_authn_contexts: nil, **extra ) @@ -6942,6 +6945,8 @@ def saml_auth( request_signed:, matching_cert_serial:, cert_error_details:, + certs_different:, + sha256_matching_cert:, unknown_authn_contexts:, **extra, ) @@ -7035,17 +7040,17 @@ def security_event_received( ) end - # Tracks if the session is kept alive + # tracks if the session is kept alive def session_kept_alive track_event('Session Kept Alive') end - # Tracks if the session timed out + # tracks if the session timed out def session_timed_out track_event('Session Timed Out') end - # Tracks when a user's session is timed out + # tracks when a user's session is timed out def session_total_duration_timeout track_event('User Maximum Session Length Exceeded') end @@ -7056,7 +7061,7 @@ def sign_in_notification_timeframe_expired_absent end # @param [String] flash - # Tracks when a user visits the sign in page + # tracks when a user visits the sign in page def sign_in_page_visit(flash:, **extra) track_event('Sign in page visited', flash:, **extra) end @@ -7072,7 +7077,7 @@ def sign_in_security_check_failed_visited # @param [Boolean] new_user Whether this is an incomplete user (no associated MFA methods) # @param [Boolean] has_other_auth_methods Whether the user has other authentication methods # @param [Integer] phone_configuration_id Phone configuration associated with request - # Tracks when a user opts into SMS + # tracks when a user opts into SMS def sms_opt_in_submitted( success:, errors:, @@ -7097,7 +7102,7 @@ def sms_opt_in_submitted( # @param [Boolean] new_user # @param [Boolean] has_other_auth_methods # @param [Integer] phone_configuration_id - # Tracks when a user visits the sms opt in page + # tracks when a user visits the sms opt in page def sms_opt_in_visit( new_user:, has_other_auth_methods:, diff --git a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb index 26e172491fc..0c6e5b2556b 100644 --- a/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb +++ b/app/services/doc_auth/lexis_nexis/doc_pii_reader.rb @@ -106,7 +106,7 @@ def parse_sex_value(sex_attribute) end def parse_height_value(height_attribute) - height_match_data = height_attribute&.match(/(?\d)' ?(?\d{1,2})"/) + height_match_data = height_attribute&.match(/(?\d)'(?\d{1,2})"/) return unless height_match_data diff --git a/app/services/encrypted_doc_storage/doc_writer.rb b/app/services/encrypted_doc_storage/doc_writer.rb deleted file mode 100644 index 3d2e0555108..00000000000 --- a/app/services/encrypted_doc_storage/doc_writer.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocStorage - class DocWriter - Result = Struct.new( - :name, - :encryption_key, - ) - - def write(image:, data_store: LocalStorage) - name = SecureRandom.uuid - storage = data_store.new - - storage.write_image( - encrypted_image: aes_cipher.encrypt(image, key), - name:, - ) - - Result.new( - name:, - encryption_key: Base64.strict_encode64(key), - ) - end - - private - - def aes_cipher - @aes_cipher ||= Encryption::AesCipherV2.new - end - - def key - @key ||= SecureRandom.bytes(32) - end - end -end diff --git a/app/services/encrypted_doc_storage/local_storage.rb b/app/services/encrypted_doc_storage/local_storage.rb deleted file mode 100644 index d048677bdbb..00000000000 --- a/app/services/encrypted_doc_storage/local_storage.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocStorage - class LocalStorage - def write_image(encrypted_image:, name:) - FileUtils.mkdir_p(tmp_document_storage_dir) - - File.open(tmp_document_storage_dir.join(name), 'wb') do |f| - f.write(encrypted_image) - end - end - - private - - def tmp_document_storage_dir - Rails.root.join('tmp', 'encrypted_doc_storage') - end - end -end diff --git a/app/services/encrypted_doc_storage/s3_storage.rb b/app/services/encrypted_doc_storage/s3_storage.rb deleted file mode 100644 index 8c4a219d6d6..00000000000 --- a/app/services/encrypted_doc_storage/s3_storage.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module EncryptedDocStorage - class S3Storage - def write_image(encrypted_image:, name:) - s3_client.put_object( - bucket:, - body: encrypted_image, - key: name, - ) - end - - private - - def s3_client - Aws::S3::Client.new( - http_open_timeout: 5, - http_read_timeout: 5, - compute_checksums: false, - ) - end - - def bucket - IdentityConfig.store.encrypted_document_storage_s3_bucket - end - end -end diff --git a/app/services/idv/aamva_state_maintenance_window.rb b/app/services/idv/aamva_state_maintenance_window.rb index 357d5beca98..e22c667cb61 100644 --- a/app/services/idv/aamva_state_maintenance_window.rb +++ b/app/services/idv/aamva_state_maintenance_window.rb @@ -3,13 +3,10 @@ module Idv class AamvaStateMaintenanceWindow # All AAMVA maintenance windows are expressed in 'ET' (LG-14028), + # except Montana's which we converted here from MST to ET. TZ = 'America/New_York' MAINTENANCE_WINDOWS = { - 'AL' => [ - # First Monday of each month from 1 am – 7 am ET - { cron: '0 1 * * Mon#1', duration_minutes: 6 * 60 }, - ], 'CA' => [ # Daily, 4:00 - 5:30 am. ET. { cron: '0 4 * * *', duration_minutes: 90 }, @@ -19,17 +16,11 @@ class AamvaStateMaintenanceWindow { cron: '0 1 * * Mon#1', duration_minutes: 3.5 * 60 }, { cron: '0 1 * * Mon#3', duration_minutes: 3.5 * 60 }, ], - 'CO' => [ - # 02:00 - 08:00 AM ET on the first Tuesday of every month. - { cron: '0 2 * * Tue#1', duration_minutes: 6 * 60 }, - ], 'CT' => [ - # Daily, 3:00 am. to 4:00 am. ET. - { cron: '0 3 * * *', duration_minutes: 1 }, - # Sunday 5:00 am. to 7:00 am. ET - { cron: '0 6 * * Sun', duration_minutes: 2 * 60 }, - # second Sunday of month 4:00 am. to 8:00 am. ET - { cron: '0 4 * * Sun#2', duration_minutes: 4 * 60 }, + # Daily, 4:00 am. to 6:30 am. ET. + { cron: '0 4 * * *', duration_minutes: 90 }, + # Sunday 6:00 am. to 9:30 am. ET + { cron: '0 6 * * Mon', duration_minutes: 3.5 * 60 }, ], 'DC' => [ # Daily, Midnight to 6 am. ET. @@ -43,59 +34,40 @@ class AamvaStateMaintenanceWindow # Sunday 7:00 am. to 12:00 pm. ET { cron: '0 7 * * Sun', duration_minutes: 5 * 60 }, ], - 'GA' => [ - # Daily, 5:00 am. to 6:00 am. ET. - { cron: '0 5 * * *', duration_minutes: 60 }, - ], 'IA' => [ - # "Daily, normally at 4:45 am. to 5:15 am ET." + # "Daily system resets, normally at 4:45 am. to 5:15 am ET." { cron: '45 4 * * *', duration_minutes: 30 }, - # (Also "Sunday mornings but only seconds at a time.") - ], - 'ID' => [ - # "Every third Wednesday: 9:00 pm to midnight ET" - # This is impossible to model as a cron expression, and it's - # meaningless in English without identifying when it starts. - # I'm modeling this as _every_ Wednesday since we're really - # answering "Should we expect a maintenance window right now?", - # and we don't block the user from anything. - { cron: '0 21 * * Wed', duration_minutes: 3 * 60 }, - ], - 'IL' => [ - # Daily, 2:30 am. to 5:00 am. ET. - { cron: '30 2 * * *', duration_minutes: 2.5 * 60 }, ], 'IN' => [ - # Sunday 5:00 am. to 10:00 am. ET. - { cron: '0 5 * * Sun', duration_minutes: 5 * 60 }, + # Sunday morning maintenance from 6 am. to 10 am. ET. + { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, ], - 'KS' => [ - # Sunday: 7:00 am. to 1:00 pm. ET - { cron: '0 7 * * Sun', duration_minutes: 6 * 60 }, + 'IL' => [ + { cron: '30 2 * * *', duration_minutes: 2.5 * 60 }, # Daily, 2:30 am. to 5 am. ET. ], 'KY' => [ - # Daily maintenance from 2:35 am. to 6:40 am. ET - { cron: '35 2 * * *', duration_minutes: 245 }, - # "Monthly on Sunday, midnight to 10:00 am ET." - # (Okay, but _which_ Sunday?) + # Daily maintenance from 2:50 am. to 6:40 am. ET + { cron: '50 2 * * *', duration_minutes: 230 }, ], 'MA' => [ - # Daily 3:00 am. to 4:00 am. ET. - { cron: '0 3 * * *', duration_minutes: 60 }, + # Daily maintenance from 6 am. to 6:15 am. ET. + { cron: '0 6 * * *', duration_minutes: 15 }, + # Wednesday 7 am. to 7:30 am. ET. + { cron: '0 7 * * Wed', duration_minutes: 30 }, # Saturday 10:00 pm. to Sunday 10:00 am { cron: '0 22 * * Sat', duration_minutes: 12 * 60 }, - # Sunday 2:00 am to 5:00 am ET - { cron: '0 2 * * Sun', duration_minutes: 3 * 60 }, # First Friday of each month: 12 to 6 am. ET. { cron: '0 0 * * Fri#1', duration_minutes: 6 * 60 }, ], 'MD' => [ + # Daily maintenance from 3 am. to 3:15 am. ET. + { cron: '0 3 * * *', duration_minutes: 15 }, # Sunday maintenance may occur from 6 am. to 10 am. ET. { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, ], 'MI' => [ - # Daily maintenance from 9 pm. to 9:30 pm. ET. - { cron: '0 21 * * *', duration_minutes: 30 }, + # Daily maintenance from 9 pm. to 9:15 pm. ET. + { cron: '0 21 * * *', duration_minutes: 15 }, ], 'MO' => [ # Daily maintenance from 2 am. to 4:30 am. ... @@ -104,71 +76,45 @@ class AamvaStateMaintenanceWindow { cron: '30 6 * * *', duration_minutes: 15 }, # ... and 8:30 am. to 8:35 am ET. { cron: '30 8 * * *', duration_minutes: 5 }, + # Sundays from 9 am. to 10:30 am. ET... + { cron: '0 9 * * Sun', duration_minutes: 90 }, + # ...and 5 am to 5:45 am ET on 2nd Sunday of month. + { cron: '0 5 * * Sun#2', duration_minutes: 45 }, ], 'MT' => [ - # Third Saturday of odd numbered months from 12:00 am to 6:00 am ET - { cron: '0 2 * /2 Sat#3', duration_minutes: 6 * 60 }, + # Monthly maintenance occurs first Sunday of each month + # from 12:00 am to 6:00 am (Mountain Time zone). + { cron: '0 2 * * Sun#1', duration_minutes: 6 * 60 }, ], 'NC' => [ # Daily, Midnight to 7:00 am. ET. { cron: '0 0 * * *', duration_minutes: 7 * 60 }, + # Sundays from 5am. till Noon + { cron: '0 5 * * Sun', duration_minutes: 7 * 60 }, ], - 'ND' => [ - # Wednesday around 7:30 pm to 7:35 pm ET - { cron: '30 19 * * Wed', duration_minutes: 5 }, - # 3rd Sunday of month, 5 minutes anytime between midnight and noon. - ], - 'NM' => [ - # Sundays 8:00 am. to noon ET. - { cron: '0 8 * * Sun', duration_minutes: 4 * 60 }, - ], - 'NV' => [ - # Tuesdays to Sundays: 2:00 am. to 3:15 am. ET - { cron: '0 2 * * Tue-Sun', duration_minutes: 1.25 * 60 }, - ], + # NM: "Sunday mornings." (not modeling; too vague) 'NY' => [ # Sunday maintenance 8 pm. to 9 pm. ET. { cron: '0 20 * * Sun', duration_minutes: 60 }, ], - 'OH' => [ - # Daily 4:00 am. to 4:30 am. ET - { cron: '0 4 * * *', duration_minutes: 30 }, - ], - 'OR' => [ - # Sunday 7:30 am. to 9:00 am. ET. - { cron: '30 7 * * Sun', duration_minutes: 1.5 * 60 }, - ], 'PA' => [ - # Sunday 5:00 am. to 7:00 am. ET. - { cron: '0 5 * * Sun', duration_minutes: 2 * 60 }, - ], - 'RI' => [ - # Either 3rd or 4th Sunday of each month, 7:30 am. to 10:00 am. ET. - { cron: '30 7 * * Sun#3', duration_minutes: 2.5 * 60 }, - { cron: '30 7 * * Sun#4', duration_minutes: 2.5 * 60 }, + # Sunday maintenance may occur, often between 5:30 am. & 7:00 am. ET + { cron: '30 5 * * Sun', duration_minutes: 90 }, ], 'SC' => [ - # Sunday 6:00 pm. to 10:00 pm. ET. - { cron: '0 18 * * Sun', duration_minutes: 4 * 60 }, - ], - 'TN' => [ - # Last Sunday of every month from 11:00 pm Sunday to 2:00 am. Monday ET - { cron: '0 23 * * Sun#last', duration: 3 * 60 }, + # Sunday maintenance from 7:00 pm. to 10:00 pm. ET. + { cron: '0 19 * * Sun', duration_minutes: 3 * 60 }, ], 'TX' => [ - # Saturday 9:00 pm. to Sunday 7:00 am. ET. - { cron: '0 21 * * Sat', duration_minutes: 10 * 60 }, - ], - 'UT' => [ - # 3rd Sunday of every month 1:00 am. to 9:00 am. ET - { cron: '0 1 0 0 Sun#3', duration_minutes: 8 * 60 }, + # Downtime on weekends between 9 pm ET to 7 am ET. + { cron: '0 21 * * Sat,Sun', duration_minutes: 10 * 60 }, ], 'VA' => [ - # Daily 5:00 am. to 5:30 am. ET - { cron: '0 5 * * *', duration_minutes: 30 }, # Sunday morning maintenance 3:00 am. to 5 am. ET. - { cron: '0 3 * * Sun', duration_minutes: 2 * 60 }, - # "Might not respond for short spells, daily between 7 pm and 8:30 pm." (not modeling this) + { cron: '0 3 * * Sun', duration_minutes: 120 }, + # Daily maintenance from 5 am. to 5:30 am. + { cron: '0 5 * * *', duration_minutes: 30 }, + # "Might not respond for short spells, daily between 7 pm and 8:30 pm." (not modeling this) ], 'VT' => [ # Daily maintenance from midnight to 5 am. ET. @@ -185,8 +131,8 @@ class AamvaStateMaintenanceWindow { cron: '0 6 * * Sun', duration_minutes: 4 * 60 }, ], 'WV' => [ - # Sunday 6:00 am. to 6:20 am. ET - { cron: '0 6 * * Sun', duration_minutes: 20 }, + # Occasional Sunday maintenance from 6:00 am. to noon ET. + { cron: '0 6 * * Sun', duration_minutes: 6 * 60 }, ], 'WY' => [ # Daily, 2 am. to 5 am. ET. diff --git a/app/services/idv/agent.rb b/app/services/idv/agent.rb index b8908bd78db..6a0cb8bedd5 100644 --- a/app/services/idv/agent.rb +++ b/app/services/idv/agent.rb @@ -6,16 +6,13 @@ def initialize(applicant) @applicant = applicant.symbolize_keys end - # @param document_capture_session [DocumentCaptureSession] - # @param proofing_components [Idv::ProofingComponents] def proof_resolution( document_capture_session, trace_id:, user_id:, threatmetrix_session_id:, request_ip:, - ipp_enrollment_in_progress:, - proofing_components: + ipp_enrollment_in_progress: ) document_capture_session.create_proofing_session @@ -32,7 +29,6 @@ def proof_resolution( threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - proofing_components: proofing_components.to_h, } if IdentityConfig.store.ruby_workers_idv_enabled diff --git a/app/services/idv/proofing_components.rb b/app/services/idv/proofing_components.rb index b7432c5c4c1..172848b7b93 100644 --- a/app/services/idv/proofing_components.rb +++ b/app/services/idv/proofing_components.rb @@ -37,7 +37,10 @@ def residential_resolution_check end def resolution_check - idv_session.resolution_vendor if idv_session.verify_info_step_complete? + if idv_session.verify_info_step_complete? + # NOTE: Fallback to LexisNexis to handle 50/50 state, will be removed later + idv_session.resolution_vendor || Idp::Constants::Vendors::LEXIS_NEXIS + end end def address_check diff --git a/app/services/proofing/aamva/applicant.rb b/app/services/proofing/aamva/applicant.rb index 24dece1cb46..a0e697b0e8c 100644 --- a/app/services/proofing/aamva/applicant.rb +++ b/app/services/proofing/aamva/applicant.rb @@ -87,11 +87,10 @@ def self.from_proofer_applicant(applicant) return if height.nil? # From the AAMVA DLDV guide regarding formatting the height: - # > Height data should be 3 characters (i.e. 5 foot 7 inches is submitted as 507) - feet = (height / 12).floor - inches = (height % 12).floor - - "#{feet}#{format('%02d', inches)}" + # + # The height is provided in feet-inches (i.e. 5 foot 10 inches is presented as "510"). + # + [(height / 12).to_s, (height % 12).to_s].join('') end end.freeze end diff --git a/app/views/idv/in_person/ready_to_verify/show.html.erb b/app/views/idv/in_person/ready_to_verify/show.html.erb index 109f9cdd468..55f01e51174 100644 --- a/app/views/idv/in_person/ready_to_verify/show.html.erb +++ b/app/views/idv/in_person/ready_to_verify/show.html.erb @@ -171,7 +171,7 @@ <% end %> -<%# What to do at the Post Office %> +<%# What to expect at the Post Office %>

<%= t('in_person_proofing.body.barcode.what_to_expect') %>

<%= render ProcessListComponent.new(heading_level: :h3) do |c| %> diff --git a/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb b/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb index 3db2200a903..46e0a0090e6 100644 --- a/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb +++ b/app/views/user_mailer/shared/_in_person_ready_to_verify.html.erb @@ -207,7 +207,7 @@
<% end %> -<%# What to do at the Post Office %> +<%# What to expect at the Post Office %>

<%= t('in_person_proofing.body.barcode.what_to_expect') %> diff --git a/bin/summarize-user-events b/bin/summarize-user-events deleted file mode 100755 index 0650f35c840..00000000000 --- a/bin/summarize-user-events +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -Dir.chdir(__dir__) { require 'bundler/setup' } - -require 'active_support' -require 'active_support/core_ext/integer/time' -require 'active_support/core_ext/object/blank' -require 'active_support/time' -require 'aws-sdk-cloudwatchlogs' -require 'concurrent-ruby' -require 'optparse' - -$LOAD_PATH.unshift(File.expand_path(File.join(__dir__, '../lib'))) -require 'reporting/cloudwatch_client' -require 'reporting/cloudwatch_query_quoting' - -# Require all *_matcher.rb files in lib/event_summarizer -Dir[File.expand_path( - File.join(__dir__, '../lib/event_summarizer', '**', '*_matcher.rb'), -)].sort.each do |f| - require f -end - -class SummarizeUserEvents - attr_reader :file_name, :uuid, :from_date, :stderr, :stdout, :to_date, :zone - - NICE_DATE_AND_TIME_FORMAT = '%B %d, %Y at %I:%M %p %Z' - TIME_ONLY_FORMAT = '%I:%M %p' - - def initialize( - file_name: nil, - user_uuid: nil, - start_time: nil, - end_time: nil, - zone: 'UTC', - stdout: STDOUT, - stderr: STDERR - ) - @file_name = file_name - @zone = zone - @uuid = user_uuid - @from_date = parse_time(start_time) || 1.week.ago - @to_date = parse_time(end_time) || ( - start_time.present? ? - from_date + 1.week : - ActiveSupport::TimeZone[zone].now - ) - @stdout = stdout - @stderr = stderr - end - - def parse_time(time_str) - return nil if time_str.nil? - - parsed = ActiveSupport::TimeZone['UTC'].parse(time_str) - parsed = parsed.in_time_zone(zone) if zone && parsed - - parsed - end - - def matchers - @matchers ||= [ - EventSummarizer::ExampleMatcher.new, - EventSummarizer::AccountDeletionMatcher.new, - EventSummarizer::IdvMatcher.new, - ] - end - - def self.parse_command_line_options(argv) - options = { - zone: 'America/New_York' - } - basename = File.basename($0) - - # rubocop:disable Metrics/LineLength - optparse = OptionParser.new do |opts| - opts.banner = <<~EOM - - Summarize user events in a human-readable format - - Cloudwatch logs can be read from a file as newline-delimited JSON (ndjson), - or fetched directly via aws-vault. - - Usage: #{basename} [OPTIONS] - - Examples: - #{basename} -f events.ndjson - aws-vault exec prod-power -- #{basename} -u 1234-5678-90ab-cdef -s 2024-12-09T10:00:00 -e 2024-12-09T14:30:00 -z America/New_York - - EOM - - opts.on('-f', '--file_name FILE_NAME', 'filename from which to read the events') do |val| - options[:file_name] = val - end - - opts.on('-h', '--help', 'Display this message') do - warn opts - exit - end - - opts.on('-u', '--user-uuid USER_UUID', 'UUID of the protagonist of the story') do |val| - options[:user_uuid] = val - end - - opts.on('-s', '--start-time START_TIME', 'Time of the start of the query period (e.g. 2024-12-09T10:00:00Z), default: 1 week ago') do |val| - options[:start_time] = val - end - - opts.on('-e', '--end-time END_TIME', 'Time of the end of the query period (e.g. 2024-12-09T14:30:00Z), default: 1 week from start') do |val| - options[:end_time] = val - end - - opts.on('-z', '--timezone TIMEZONE', 'Timezone to use (e.g. America/New_York), default: UTC') do |val| - options[:zone] = val - end - end - # rubocop:enable Metrics/LineLength - - optparse.parse!(argv) - - options - end - - - def run - in_correct_time_zone do - find_cloudwatch_events do |event| - # Time.zone is thread-local, and CloudwatchClient may use multiple - # threads to make requests. So we have to make double-sure we're - # in the right Timezone. - in_correct_time_zone do - normalize_event!(event) - - matchers.each do |matcher| - matcher.handle_cloudwatch_event(event) - end - end - end - - overall_results = [] - - matchers.each do |matcher| - results_for_matcher = matcher.finish - overall_results.append(*results_for_matcher) - end - - stdout.puts format_results(overall_results) - end - end - - def format_results(results) - # Each Hash in results should have _at least_ a :title key defined - - results. - sort_by { |r| r[:timestamp] || r[:started_at] || Time.zone.at(0) }. - map do |r| - timestamp = r[:timestamp] || r[:started_at] - - heading = r[:title] - - if timestamp - heading = "#{heading} (#{format_time(timestamp)})" - end - - prev_timestamp = timestamp - - list_items = r[:attributes] - &.sort_by { |attr| attr[:timestamp] || Time.zone.at(0) } - &.map do |attr| - text = attr[:description] - - formatted_timestamp = format_time(attr[:timestamp], prev_timestamp) - prev_timestamp = attr[:timestamp] - - text = "(#{formatted_timestamp}) #{text}" if formatted_timestamp - - "* #{text}" - end - - [ - "## #{heading}", - *list_items, - '', - ] - end.join("\n") - end - - def format_time(timestamp, prev_timestamp = nil) - return if timestamp.blank? - - timestamp = timestamp.in_time_zone(zone) - prev_timestamp = prev_timestamp&.in_time_zone(zone) - - same_date = timestamp.to_date == prev_timestamp&.to_date - - if same_date - timestamp.strftime(TIME_ONLY_FORMAT) - else - timestamp.strftime(NICE_DATE_AND_TIME_FORMAT) - end - end - - def query - format(<<~QUERY) - fields - name - , properties.event_properties.success as success - , @message - , @timestamp - | filter properties.user_id = '#{uuid}' - | sort @timestamp asc - | limit 10000 - QUERY - end - - def cloudwatch_client - @cloudwatch_client ||= Reporting::CloudwatchClient.new( - num_threads: 5, - ensure_complete_logs: true, - log_group_name: 'prod_/srv/idp/shared/log/events.log', - ) - end - - def find_cloudwatch_events(&block) - unless file_name.nil? - warn 'Reading Cloudwatch events as newline-delimited JSON (ndjson) file' - file_source(&block) - else - cloudwatch_source(&block) - end - end - - def file_source(&block) - events = [] - - File.read(file_name).each_line do |line| - next if line.blank? - events << JSON.parse(line) - end - - events.sort_by! { |e| e['@timestamp'] } - - events.each do |e| - block.call(e) - end - end - - def cloudwatch_source(&block) - cloudwatch_client.fetch( - query: query, - from: from_date, - to: to_date, - &block - ) - end - - def in_correct_time_zone - old_time_zone = Time.zone - Time.zone = zone - yield - ensure - Time.zone = old_time_zone - end - - def normalize_event!(event) - if event['@timestamp'].is_a?(String) - event['@timestamp'] = ActiveSupport::TimeZone['UTC'].parse(event['@timestamp']) - end - - if event['@message'].is_a?(String) - event['@message'] = JSON.parse(event['@message']) - end - - event['name'] ||= event['@message']['name'] - end -end - -def main - options = SummarizeUserEvents.parse_command_line_options(ARGV) - SummarizeUserEvents.new(**options).run -end - -if $PROGRAM_NAME == __FILE__ - main -end diff --git a/config/application.yml.default b/config/application.yml.default index 097594707d4..3aeaffe4dcc 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -129,7 +129,6 @@ enable_load_testing_mode: false enable_rate_limiting: true enable_test_routes: true enable_usps_verification: true -encrypted_document_storage_s3_bucket: 'test-bucket' event_disavowal_expiration_hours: 240 facial_match_general_availability_enabled: true feature_idv_force_gpo_verification_enabled: false @@ -168,7 +167,6 @@ idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 idv_socure_reason_code_download_enabled: false idv_socure_shadow_mode_enabled: false -idv_socure_shadow_mode_enabled_for_docv_users: true idv_sp_required: false in_person_completion_survey_url: 'https://login.gov' in_person_doc_auth_button_enabled: true @@ -248,13 +246,13 @@ logins_per_ip_track_only_mode: false logo_upload_enabled: false mailer_domain_name: http://localhost:3000 max_auth_apps_per_account: 2 +max_bad_passwords: 5 +max_bad_passwords_window_in_seconds: 60 max_emails_per_account: 12 max_mail_events: 4 max_mail_events_window_in_days: 30 max_phone_numbers_per_account: 5 max_piv_cac_per_account: 2 -max_sign_in_failures: 5 -max_sign_in_failures_window_in_seconds: 60 mfa_report_config: '[]' min_password_score: 3 minimum_wait_before_another_usps_letter_in_hours: 24 @@ -521,7 +519,6 @@ production: email_registrations_per_ip_track_only_mode: true enable_test_routes: false enable_usps_verification: false - encrypted_document_storage_s3_bucket: '' facial_match_general_availability_enabled: false feature_select_email_to_share_enabled: false idv_sp_required: true diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index bc053803f56..aff0162a210 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -97,11 +97,11 @@ def self.all }, ).freeze - SOCURE_IDV_SHADOW_MODE_FOR_NON_DOCV_USERS = AbTest.new( + SOCURE_IDV_SHADOW_MODE = AbTest.new( experiment_name: 'Socure shadow mode', should_log: ['IdV: doc auth verify proofing results'].to_set, buckets: { - socure_shadow_mode_for_non_docv_users: IdentityConfig.store.socure_idplus_shadow_mode_percent, + shadow_mode_enabled: IdentityConfig.store.socure_idplus_shadow_mode_percent, }, ).freeze diff --git a/config/locales/en.yml b/config/locales/en.yml index 332fd7014e1..3a2e615c72f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -799,7 +799,7 @@ errors.messages.wrong_length.one: is the wrong length (should be 1 character) errors.messages.wrong_length.other: is the wrong length (should be %{count} characters) errors.piv_cac_setup.unique_name: That name is already taken. Please choose a different name. errors.registration.terms: Before you can continue, you must give us permission. Please check the box below and then click continue. -errors.sign_in.sign_in_failure_limit: You have exceeded the maximum sign in attempts. You must wait %{time_left} before trying again. +errors.sign_in.bad_password_limit: You have exceeded the maximum sign in attempts. You must wait %{time_left} before trying again. errors.two_factor_auth_setup.must_select_additional_option: Select an additional authentication method. errors.two_factor_auth_setup.must_select_option: Select an authentication method. errors.verify_personal_key.rate_limited: You tried too many times, please try again in %{timeout}. @@ -1223,7 +1223,7 @@ image_description.warning: Yellow caution sign in_person_proofing.body.barcode.cancel_link_text: Cancel your barcode in_person_proofing.body.barcode.close_window: You may now close this window. in_person_proofing.body.barcode.deadline: You must visit any participating Post Office by %{deadline}. -in_person_proofing.body.barcode.deadline_restart: If you go after this deadline, your barcode will not work. You will need to restart the process. +in_person_proofing.body.barcode.deadline_restart: If you go after this deadline, your information will not be saved and you will need to restart the process. in_person_proofing.body.barcode.eipp_tag: GSA Enhanced Pilot Barcode in_person_proofing.body.barcode.eipp_what_to_bring: 'Depending on your ID, you may need to show supporting documents. Review the following options carefully:' in_person_proofing.body.barcode.email_sent: We have sent your barcode and the information below to the email you used to sign in @@ -1233,11 +1233,11 @@ in_person_proofing.body.barcode.questions: Questions? in_person_proofing.body.barcode.retail_hours: Retail hours in_person_proofing.body.barcode.retail_hours_closed: Closed in_person_proofing.body.barcode.return_to_partner_link: Return to %{sp_name} -in_person_proofing.body.barcode.what_to_expect: What to do at the Post Office +in_person_proofing.body.barcode.what_to_expect: What to expect at the Post Office in_person_proofing.body.cta.button: Try in person in_person_proofing.body.cta.prompt_detail: Most people who are unable to complete this step online are successful in verifying their identity at a participating Post Office. No appointment needed. Locations are available nationwide. in_person_proofing.body.expect.heading: What to expect after your visit -in_person_proofing.body.expect.info: You’ll get an email within 24 hours of visiting a Post Office. We’ll tell you if your identity verification was successful or unsuccessful. Check your email for a message from no-reply@login.gov. +in_person_proofing.body.expect.info: We’ll send you an email to let you know if your identity verification was successful or unsuccessful within 24 hours of your visit to the Post Office. in_person_proofing.body.location.change_location_find_other_locations: Find other participating Post Office locations. in_person_proofing.body.location.change_location_heading: Need to change your Post Office location? in_person_proofing.body.location.change_location_info_html: You don’t need to create a new barcode, you can bring this barcode to any participating Post Office location. %{find_other_locations_link_html} @@ -1316,7 +1316,7 @@ in_person_proofing.form.state_id.state_id_number_hint_spaces: spaces in_person_proofing.form.state_id.state_id_number_texas_hint: This is the 8-digit number on your ID. Enter only numbers in this field. in_person_proofing.form.state_id.zipcode: ZIP Code in_person_proofing.headings.address: Enter your current residential address -in_person_proofing.headings.barcode: Show this barcode and your state ID at a Post Office to finish verifying your identity +in_person_proofing.headings.barcode: Show this barcode and your state‑issued ID at a Post Office to finish verifying your identity in_person_proofing.headings.barcode_eipp: Bring this barcode and supporting documents to a Post Office to finish verifying your identity in_person_proofing.headings.barcode_what_to_bring: What to bring to the Post Office in_person_proofing.headings.cta: Try verifying your ID in person @@ -1366,7 +1366,7 @@ in_person_proofing.process.state_id.heading_eipp: Show your ID and supporting do in_person_proofing.process.state_id.info: This must not be expired. We do not currently accept any other forms of identification, such as passports and military IDs. in_person_proofing.process.state_id.info_eipp: The retail associate will scan your ID. This must not be expired. Depending on the type of ID that you present, you may need to show supporting documents. See the requirements in the “What to bring to the Post Office” section. in_person_proofing.process.what_to_do.heading: Stand in any line -in_person_proofing.process.what_to_do.info: Tell the Post Office retail associate you are here to verify your identity. If they don’t know how to proceed, ask for a supervisor to help you. +in_person_proofing.process.what_to_do.info: Tell the Post Office retail associate you are here to verify your identity with %{app_name}. instructions.account.reactivate.begin: Let’s get started. instructions.account.reactivate.explanation: 'When you created your account, we gave you a list of words and asked you to store them in a safe place. It looked similar to this:' instructions.account.reactivate.intro: We take extra steps to keep your personal information secure and private, so resetting your password takes a little extra effort. diff --git a/config/locales/es.yml b/config/locales/es.yml index 00686cb34f0..a8b5b98d7b1 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -810,7 +810,7 @@ errors.messages.wrong_length.one: tiene la longitud incorrecta (debe ser de 1 c errors.messages.wrong_length.other: tiene la longitud incorrecta (debe ser de %{count} caracteres) errors.piv_cac_setup.unique_name: Ese nombre ya fue seleccionado. Elija un nombre diferente. errors.registration.terms: Antes de continuar, debe darnos permiso. Marque la casilla a continuación y luego haga clic en continuar. -errors.sign_in.sign_in_failure_limit: Superó el número máximo de intentos de inicio de sesión. Debe esperar %{time_left} antes de volver a intentarlo. +errors.sign_in.bad_password_limit: Superó el número máximo de intentos de inicio de sesión. Debe esperar %{time_left} antes de volver a intentarlo. errors.two_factor_auth_setup.must_select_additional_option: Seleccione un método de autenticación adicional. errors.two_factor_auth_setup.must_select_option: Seleccione un método de autenticación. errors.verify_personal_key.rate_limited: Lo intentó demasiadas veces; vuelva a intentarlo en %{timeout}. @@ -1233,8 +1233,8 @@ image_description.us_flag: Bandera de los EE. UU. image_description.warning: Señal amarilla de precaución in_person_proofing.body.barcode.cancel_link_text: Cancele su código de barras in_person_proofing.body.barcode.close_window: Ya puede cerrar esta ventana. -in_person_proofing.body.barcode.deadline: Debe acudir a cualquier oficina de correos participante antes del %{deadline}. -in_person_proofing.body.barcode.deadline_restart: Si acude después de esta fecha límite, su código de barras ya no funcionará y tendrá que reiniciar el proceso. +in_person_proofing.body.barcode.deadline: Debe acudir a cualquier oficina de correos participante antes del %{deadline} +in_person_proofing.body.barcode.deadline_restart: Si acude después del plazo, su información no se guardará y tendrá que reiniciar el proceso. in_person_proofing.body.barcode.eipp_tag: Código de barras piloto mejorado GSA in_person_proofing.body.barcode.eipp_what_to_bring: 'Según el tipo de identificación que tenga, es posible que deba presentar documentos comprobatorios. Lea con atención las opciones siguientes:' in_person_proofing.body.barcode.email_sent: Enviamos el código de barras y la información más abajo al correo electrónico que usó para iniciar sesión @@ -1244,11 +1244,11 @@ in_person_proofing.body.barcode.questions: '¿Tiene alguna pregunta?' in_person_proofing.body.barcode.retail_hours: Horario de atención al público in_person_proofing.body.barcode.retail_hours_closed: Cerrado in_person_proofing.body.barcode.return_to_partner_link: Volver a %{sp_name} -in_person_proofing.body.barcode.what_to_expect: Qué hacer en la oficina de correos +in_person_proofing.body.barcode.what_to_expect: Qué esperar en la oficina de correos in_person_proofing.body.cta.button: Intentar en persona in_person_proofing.body.cta.prompt_detail: La mayoría de las personas que no pueden hacer la verificación de su identidad en línea logran verificarla en una oficina de correos participante. No es necesario hacer cita para ello, y hay oficinas en todo el país. in_person_proofing.body.expect.heading: Qué esperar después de la visita -in_person_proofing.body.expect.info: En las 24 horas siguientes a su visita a la oficina de correos, recibirá un correo electrónico para informarle si se logró o no su verificación de identidad. Busque un mensaje de no-reply@login.gov en su correo electrónico. +in_person_proofing.body.expect.info: En las 24 horas siguientes a su visita a la oficina de correos, recibirá un correo electrónico para informarle si se logró o no su verificación de identidad. in_person_proofing.body.location.change_location_find_other_locations: Busque otras oficinas de correos participantes. in_person_proofing.body.location.change_location_heading: ¿Necesita cambiar su oficina de correos? in_person_proofing.body.location.change_location_info_html: No necesita crear un nuevo código de barras, puede llevar este código de barras a cualquier oficina de correos participante. %{find_other_locations_link_html} @@ -1327,7 +1327,7 @@ in_person_proofing.form.state_id.state_id_number_hint_spaces: espacios in_person_proofing.form.state_id.state_id_number_texas_hint: Este es el número de 8 dígitos de su identificación. Ingrese únicamente números en este campo. in_person_proofing.form.state_id.zipcode: Código postal in_person_proofing.headings.address: Ingrese su domicilio actual -in_person_proofing.headings.barcode: Muestre este código de barras y su identificación el estado en una oficina de correos para terminar de verificar su identidad. +in_person_proofing.headings.barcode: Muestre este código de barras y su identificación emitida por el estado en una oficina de correos para terminar de verificar su identidad. in_person_proofing.headings.barcode_eipp: Lleve este código de barras y los documentos comprobatorios a una oficina de correos para terminar de verificar su identidad in_person_proofing.headings.barcode_what_to_bring: Lo que debe llevar a la oficina de correos in_person_proofing.headings.cta: Intente verificar su identidad en persona @@ -1377,7 +1377,7 @@ in_person_proofing.process.state_id.heading_eipp: Muestre su identificación y l in_person_proofing.process.state_id.info: No debe estar vencida. Actualmente no aceptamos otras formas de identificación, como pasaportes o identificaciones militares. in_person_proofing.process.state_id.info_eipp: El empleado escaneará su identificación. No debe estar vencida. Según el tipo de identificación que presente, es posible que deba mostrar documentos comprobatorios. Consulte los requisitos en la sección “Lo que debe llevar a la oficina de correos”. in_person_proofing.process.what_to_do.heading: Colóquese en cualquier fila -in_person_proofing.process.what_to_do.info: Diga al empleado de la oficina de correos que desea verificar su identidad. Si el empleado no sabe qué hacer, pida la ayuda de un supervisor. +in_person_proofing.process.what_to_do.info: Diga al empleado de la oficina de correos que desea verificar su identidad con %{app_name}. instructions.account.reactivate.begin: Empecemos. instructions.account.reactivate.explanation: 'Cuando creó su cuenta, le dimos una lista de palabras y le pedimos que las guardara en un lugar seguro. Era similar a esto:' instructions.account.reactivate.intro: Adoptamos medidas adicionales para mantener su información personal segura y privada, por lo que restablecer su contraseña requiere un poco más de trabajo. diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 827f65367ad..cdbd12ab40c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -799,7 +799,7 @@ errors.messages.wrong_length.one: n’est pas de la bonne longueur (devrait êtr errors.messages.wrong_length.other: n’est pas de la bonne longueur (devrait être de %{count} caractères) errors.piv_cac_setup.unique_name: Ce nom est déjà pris. Veuillez choisir un autre nom. errors.registration.terms: Avant de pouvoir continuer, vous devez nous donner la permission. Veuillez cocher la case ci-dessous, puis cliquez sur Suite. -errors.sign_in.sign_in_failure_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. Vous devez attendre %{time_left} avant de réessayer. +errors.sign_in.bad_password_limit: Vous avez dépassé le nombre maximal de tentatives de connexion. Vous devez attendre %{time_left} avant de réessayer. errors.two_factor_auth_setup.must_select_additional_option: Sélectionnez une méthode d’authentification supplémentaire. errors.two_factor_auth_setup.must_select_option: Sélectionnez une méthode d’authentification. errors.verify_personal_key.rate_limited: Vous avez essayé trop de fois, veuillez réessayer dans %{timeout}. @@ -997,7 +997,7 @@ help_text.requested_attributes.full_name: Nom complet help_text.requested_attributes.ial2_reverified_consent_info_html: 'Étant donné que vous avez revérifié votre identité, nous avons besoin de votre autorisation pour partager ces informations avec %{sp_html} :' help_text.requested_attributes.intro_html: 'Nous partagerons ces informations avec %{sp_html}:' help_text.requested_attributes.phone: Numéro de téléphone -help_text.requested_attributes.select_email_link: Sélectionner l’adresse e-mail +help_text.requested_attributes.select_email_link: Sélectionner l’e-mail help_text.requested_attributes.social_security_number: Numéro de sécurité sociale help_text.requested_attributes.verified_at: Mis à jour le help_text.requested_attributes.x509_issuer: Émetteur PIV/CAC @@ -1222,8 +1222,8 @@ image_description.us_flag: Drapeau des États-Unis image_description.warning: Panneau d’avertissement jaune in_person_proofing.body.barcode.cancel_link_text: Annuler votre code-barres in_person_proofing.body.barcode.close_window: Vous pouvez maintenant fermer cette fenêtre -in_person_proofing.body.barcode.deadline: Vous devez vous rendre dans un bureau de poste participant avant le %{deadline}. -in_person_proofing.body.barcode.deadline_restart: Si vous laissez passer la date limite, votre code-barres ne fonctionnera pas. Vous devrez recommencer toute la procédure. +in_person_proofing.body.barcode.deadline: Vous devez vous rendre dans un bureau de poste participant avant le %{deadline} +in_person_proofing.body.barcode.deadline_restart: Si vous y allez après cette date limite, vos informations ne seront pas sauvegardées et vous devrez recommencer le processus. in_person_proofing.body.barcode.eipp_tag: Code-barres pilote amélioré de la GSA in_person_proofing.body.barcode.eipp_what_to_bring: 'Selon la pièce d’identité dont vous disposez, il pourra vous être demandé de présenter des documents complémentaires. Étudiez attentivement les options suivantes:' in_person_proofing.body.barcode.email_sent: Nous avons envoyé votre code-barre et les informations ci-dessous à l’adresse e-mail que vous avez utilisée pour vous connecter @@ -1233,11 +1233,11 @@ in_person_proofing.body.barcode.questions: Des questions ? in_person_proofing.body.barcode.retail_hours: Heures d’ouverture in_person_proofing.body.barcode.retail_hours_closed: Fermé in_person_proofing.body.barcode.return_to_partner_link: Retourner à %{sp_name} -in_person_proofing.body.barcode.what_to_expect: Que faire au bureau de poste +in_person_proofing.body.barcode.what_to_expect: À quoi s’attendre au bureau de poste in_person_proofing.body.cta.button: Essayer en personne in_person_proofing.body.cta.prompt_detail: La plupart des personnes qui ne parviennent pas à effectuer cette étape en ligne réussissent à confirmer leur identité dans un bureau de poste participant. Sans rendez-vous. Il existe des sites dans l’ensemble du pays. in_person_proofing.body.expect.heading: Que faire après votre visite -in_person_proofing.body.expect.info: Vous recevrez un e-mail dans les 24 heures suivant votre visite dans un bureau de poste. Nous vous informerons si la vérification de votre identité a réussi ou échoué. Vérifiez si vous avez reçu un message de no-reply@login.gov dans votre messagerie. +in_person_proofing.body.expect.info: Nous vous enverrons un e-mail pour vous informer de la réussite ou de l’échec de la vérification de votre identité dans les 24 heures suivant votre visite au bureau de poste. in_person_proofing.body.location.change_location_find_other_locations: Trouvez d’autres bureaux de poste participants. in_person_proofing.body.location.change_location_heading: Vous devez changer de bureau de poste ? in_person_proofing.body.location.change_location_info_html: Pas besoin de créer un nouveau code-barres. Vous pouvez apporter le vôtre à n’importe quel bureau de poste participant. %{find_other_locations_link_html} @@ -1316,7 +1316,7 @@ in_person_proofing.form.state_id.state_id_number_hint_spaces: des espaces in_person_proofing.form.state_id.state_id_number_texas_hint: Il s’agit du numéro à huit chiffres figurant sur votre pièce d’identité. Dans ce champ, ne saisissez que des chiffres. in_person_proofing.form.state_id.zipcode: Code postal in_person_proofing.headings.address: Saisissez votre adresse résidentielle actuelle -in_person_proofing.headings.barcode: Présentez ce code-barres et votre pièce d’identité l’État à un bureau de poste pour terminer la vérification de votre identité +in_person_proofing.headings.barcode: Présentez ce code-barres et votre pièce d’identité délivrée par l’État à un bureau de poste pour terminer la vérification de votre identité in_person_proofing.headings.barcode_eipp: Présenter ce code-barres et les documents complémentaires à un bureau de poste pour terminer la vérification de votre identité in_person_proofing.headings.barcode_what_to_bring: À apporter au bureau de poste in_person_proofing.headings.cta: Essayez de confirmer votre identité en personne @@ -1366,7 +1366,7 @@ in_person_proofing.process.state_id.heading_eipp: Présenter votre pièce d’id in_person_proofing.process.state_id.info: Ce document ne doit pas être périmé. Nous n’acceptons actuellement aucune autre pièce d’identité, comme les passeports et les cartes d’identité militaires. in_person_proofing.process.state_id.info_eipp: Le préposé numérisera votre pièce d’identité. Ce document ne doit pas être périmé. Selon le type de pièce d’identité que vous présentez, il pourra vous être demandé de produire des documents complémentaires. Vous trouverez les exigences à respecter dans la section « Qu’apporter au bureau de poste ». in_person_proofing.process.what_to_do.heading: Faites la queue -in_person_proofing.process.what_to_do.info: Dites à l’employé que vous venez pour une vérification d’identité. Si la personne ne sait pas comment procéder, demandez qu’un responsable vienne vous aider. +in_person_proofing.process.what_to_do.info: Dites au préposé du bureau de poste que vous êtes là pour confirmer votre identité auprès de %{app_name}. instructions.account.reactivate.begin: Commençons. instructions.account.reactivate.explanation: 'Lorsque vous avez créé votre compte, nous vous avons donné une liste de mots et vous avons demandé de les placer en lieu sûr. La liste ressemblait à ceci :' instructions.account.reactivate.intro: Comme nous prenons des mesures supplémentaires pour maintenir vos informations sécurisées et confidentielles, la réinitialisation de votre mot de passe est un peu plus fastidieuse. diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 00a41aa73e8..ea0185944b9 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -64,7 +64,7 @@ account.index.continue_to_service_provider: 继续到 %{service_provider} account.index.default: 默认 account.index.device: 在 %{os}的 %{browser} account.index.email: 电邮地址 -account.index.email_add: 添加新电邮 +account.index.email_add: 添加新电邮地址 account.index.email_addresses: 电邮地址 account.index.email_preferences: 电邮选择 account.index.password: 密码 @@ -810,7 +810,7 @@ errors.messages.wrong_length.one: 长度不对(应当是 1 个字符) errors.messages.wrong_length.other: 长度不对(应当是 %{count} 个字符) errors.piv_cac_setup.unique_name: 这个名字已被使用。请选择一个不同的名字。 errors.registration.terms: 在你能继续之前,你必须授予我们你的同意。请在下面的框打勾然后点击继续。 -errors.sign_in.sign_in_failure_limit: 你已超出登录尝试允许最多次数。你必须等待 %{time_left} 才能重试。 +errors.sign_in.bad_password_limit: 你已超出登录尝试允许最多次数。你必须等待 %{time_left} 才能重试。 errors.two_factor_auth_setup.must_select_additional_option: 请选择一个额外的身份证实方法。 errors.two_factor_auth_setup.must_select_option: 选择一个身份证实方法。 errors.verify_personal_key.rate_limited: 你尝试了太多次。请在 %{timeout}后再试。 @@ -1236,7 +1236,7 @@ image_description.warning: 黄色警告标志 in_person_proofing.body.barcode.cancel_link_text: 取消你的条形码 in_person_proofing.body.barcode.close_window: 你现在可以关闭这一窗口。 in_person_proofing.body.barcode.deadline: 你必须在 %{deadline}之前去任何参与邮局。 -in_person_proofing.body.barcode.deadline_restart: 如果你过了截止日期才去邮局,那你的条形码将无法使用。你将需要重新开始这一流程。 +in_person_proofing.body.barcode.deadline_restart: 如果你在截止日期后才去邮局,你的信息不会被存储,你又得从头开始这一过程。 in_person_proofing.body.barcode.eipp_tag: GSA 增强型试行条形码 in_person_proofing.body.barcode.eipp_what_to_bring: 取决于您身份证件类型,您也许需要显示支持文件。请仔细阅读以下选项: in_person_proofing.body.barcode.email_sent: 我们已将条形码和以下信息发到了您用来登录的电邮地址 @@ -1246,11 +1246,11 @@ in_person_proofing.body.barcode.questions: 有问题吗? in_person_proofing.body.barcode.retail_hours: 营业时间 in_person_proofing.body.barcode.retail_hours_closed: 关闭 in_person_proofing.body.barcode.return_to_partner_link: 返回 %{sp_name} -in_person_proofing.body.barcode.what_to_expect: 到了邮局做什么 +in_person_proofing.body.barcode.what_to_expect: 在邮局会发生什么 in_person_proofing.body.cta.button: 尝试亲身去 in_person_proofing.body.cta.prompt_detail: 无法在网上完成这一步骤的大多数人都能在一个参与本项目的邮局成功地证实身份。去邮局无需预约。全国各地都有参与本项目的邮局。 in_person_proofing.body.expect.heading: 去邮局后会发生什么 -in_person_proofing.body.expect.info: 你去邮局后24小时内会收到一封电邮。我们会告诉你你身份验证是否成功。检查你的电邮中是否有来自no-reply@login.gov的一则信息。 +in_person_proofing.body.expect.info: 你去邮局后 24 小时内,我们会给你发电邮,告诉你是否成功验证了身份。 in_person_proofing.body.location.change_location_find_other_locations: 查找其他参与本项目的邮局地点。 in_person_proofing.body.location.change_location_heading: 需要更改你的邮局地点吗? in_person_proofing.body.location.change_location_info_html: 你不需要创建新的条形码,你可以将这个条形码带到任何参与本项目的邮局。 %{find_other_locations_link_html} @@ -1329,7 +1329,7 @@ in_person_proofing.form.state_id.state_id_number_hint_spaces: 空格 in_person_proofing.form.state_id.state_id_number_texas_hint: 这是你身份证件上的8位数在这一字段中只输入数字 in_person_proofing.form.state_id.zipcode: 邮编 in_person_proofing.headings.address: 输入你当前住宅地址 -in_person_proofing.headings.barcode: 到邮局出示这一条形码和你政府颁发的身份证件来完成验证身份。 +in_person_proofing.headings.barcode: 到邮局出示这一条形码和你州政府颁发的身份证件来完成验证身份。 in_person_proofing.headings.barcode_eipp: 携带该条形码和支持性文件到邮局去完成验证身份。 in_person_proofing.headings.barcode_what_to_bring: 到邮局要带什么 in_person_proofing.headings.cta: 尝试亲身去验证身份证件 @@ -1379,7 +1379,7 @@ in_person_proofing.process.state_id.heading_eipp: 出示你的身份证件以及 in_person_proofing.process.state_id.info: 该证件必须在有效期内。我们目前不接受任何其他形式的身份证件,比如护照和军队身份证件。 in_person_proofing.process.state_id.info_eipp: 邮局工作人员会扫描你的身份证件。该证件必须在有效期内。取决于您出示的身份证件类型,您也许需要显示支持文件。参见“到邮局要带什么”部分中的规定。 in_person_proofing.process.what_to_do.heading: 请排在任何一队里 -in_person_proofing.process.what_to_do.info: 告诉邮局工作人员你是来验证身份的。如果他们不知道如何处理,要求让一位主管来帮你。 +in_person_proofing.process.what_to_do.info: 告诉邮局工作人员你是为了%{app_name}验证你的身份。 instructions.account.reactivate.begin: 我们开始吧。 instructions.account.reactivate.explanation: 你设立账户时,我们给了你一个单词清单并请你将其存放在一个安全的地方。清单像这样: instructions.account.reactivate.intro: 我们采取额外步骤来保护你个人信息的安全和私密,所以重设密码需要多花点精力。 @@ -1666,7 +1666,7 @@ titles.reactivate_account: 重新激活你账户 titles.registrations.new: 设立账户 titles.revoke_consent: 撤销同意 titles.rules_of_use: 使用规则 -titles.select_email: 选择你想用的电邮 +titles.select_email: 选择你比较愿意分享的电邮 titles.sign_up.completion_consent_expired_ial1: 从你上次授权我们分享你的信息已经一年了。 titles.sign_up.completion_consent_expired_ial2: 从你上次授权我们分享你验证过的身份已经一年了。 titles.sign_up.completion_first_sign_in: 继续到 %{sp} diff --git a/db/primary_migrate/20250106232958_drop_proofing_components_table.rb b/db/primary_migrate/20250106232958_drop_proofing_components_table.rb deleted file mode 100644 index 417d4f1d4bc..00000000000 --- a/db/primary_migrate/20250106232958_drop_proofing_components_table.rb +++ /dev/null @@ -1,23 +0,0 @@ -class DropProofingComponentsTable < ActiveRecord::Migration[7.2] - def change - drop_table :proofing_components do |t| - t.integer "user_id", null: false, comment: "sensitive=false" - t.string "document_check", comment: "sensitive=false" - t.string "document_type", comment: "sensitive=false" - t.string "source_check", comment: "sensitive=false" - t.string "resolution_check", comment: "sensitive=false" - t.string "address_check", comment: "sensitive=false" - t.datetime "verified_at", precision: nil, comment: "sensitive=false" - t.datetime "created_at", precision: nil, null: false, comment: "sensitive=false" - t.datetime "updated_at", precision: nil, null: false, comment: "sensitive=false" - t.string "liveness_check", comment: "sensitive=false" - t.string "device_fingerprinting_vendor", comment: "sensitive=false" - t.boolean "threatmetrix", comment: "sensitive=false" - t.string "threatmetrix_review_status", comment: "sensitive=false" - t.string "threatmetrix_risk_rating", comment: "sensitive=false" - t.string "threatmetrix_policy_score", comment: "sensitive=false" - t.index ["user_id"], name: "index_proofing_components_on_user_id", unique: true - t.index ["verified_at"], name: "index_proofing_components_on_verified_at" - end - end -end diff --git a/db/schema.rb b/db/schema.rb index 376cc842b52..135f2b0231c 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[7.2].define(version: 2025_01_06_232958) do +ActiveRecord::Schema[7.2].define(version: 2024_12_03_163014) do # These are extensions that must be enabled in order to support this database enable_extension "citext" enable_extension "pg_stat_statements" @@ -471,6 +471,26 @@ t.index ["user_id"], name: "index_profiles_on_user_id" end + create_table "proofing_components", force: :cascade do |t| + t.integer "user_id", null: false, comment: "sensitive=false" + t.string "document_check", comment: "sensitive=false" + t.string "document_type", comment: "sensitive=false" + t.string "source_check", comment: "sensitive=false" + t.string "resolution_check", comment: "sensitive=false" + t.string "address_check", comment: "sensitive=false" + t.datetime "verified_at", precision: nil, comment: "sensitive=false" + t.datetime "created_at", precision: nil, null: false, comment: "sensitive=false" + t.datetime "updated_at", precision: nil, null: false, comment: "sensitive=false" + t.string "liveness_check", comment: "sensitive=false" + t.string "device_fingerprinting_vendor", comment: "sensitive=false" + t.boolean "threatmetrix", comment: "sensitive=false" + t.string "threatmetrix_review_status", comment: "sensitive=false" + t.string "threatmetrix_risk_rating", comment: "sensitive=false" + t.string "threatmetrix_policy_score", comment: "sensitive=false" + t.index ["user_id"], name: "index_proofing_components_on_user_id", unique: true + t.index ["verified_at"], name: "index_proofing_components_on_verified_at" + end + create_table "registration_logs", force: :cascade do |t| t.integer "user_id", null: false, comment: "sensitive=false" t.datetime "registered_at", precision: nil, comment: "sensitive=false" diff --git a/dockerfiles/nginx.Dockerfile b/dockerfiles/nginx.Dockerfile index ad189fa8db1..c96304c1ffa 100644 --- a/dockerfiles/nginx.Dockerfile +++ b/dockerfiles/nginx.Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/alpine:3.20 +FROM public.ecr.aws/docker/library/alpine:3 RUN apk upgrade --no-cache RUN apk add --no-cache jq curl nginx nginx-mod-http-headers-more openssl diff --git a/lib/analytics_events_documenter.rb b/lib/analytics_events_documenter.rb index f14f1b794c1..6200eab1216 100644 --- a/lib/analytics_events_documenter.rb +++ b/lib/analytics_events_documenter.rb @@ -129,16 +129,6 @@ def missing_documentation errors << "#{error_prefix} #{tag.name} missing types" if !tag.types end - description = method_description(method_object) - if description.present? && !method_object.docstring.match?(/\A[A-Z]/) - indented_description = description.lines.map { |line| " #{line.chomp}" }.join("\n") - - errors << <<~MSG - #{error_prefix} method description starts with lowercase, check indentation: - #{indented_description} - MSG - end - errors end end @@ -166,7 +156,7 @@ def as_json { event_name: extract_event_name(method_object), previous_event_names: method_object.tags(PREVIOUS_EVENT_NAME_TAG).map(&:text), - description: method_description(method_object), + description: method_object.docstring.presence, attributes: attributes, method_name: method_object.name, source_line: method_object.line, @@ -178,12 +168,6 @@ def as_json { events: events_json_summary } end - # Strips Rubocop directives from description text - # @return [String, nil] - def method_description(method_object) - method_object.docstring.to_s.gsub(/^rubocop.+$/, '').presence&.chomp - end - private # Naive attempt to pull tracked event string or symbol from source code diff --git a/lib/event_summarizer/account_deletion_matcher.rb b/lib/event_summarizer/account_deletion_matcher.rb deleted file mode 100644 index ec21f689cce..00000000000 --- a/lib/event_summarizer/account_deletion_matcher.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -module EventSummarizer - class AccountDeletionMatcher - ACCOUNT_DELETION_STARTED_EVENT = 'Account Reset: request' - ACCOUNT_DELETION_SUBMITTED_EVENT = 'Account Reset: delete' - ACCOUNT_DELETION_CANCELED_EVENT = 'Account Reset: cancel' - - EVENT_PROPERTIES = ['@message', 'properties', 'event_properties'].freeze - - attr_accessor :account_deletion_events, :event_summaries - - def initialize - @account_deletion_events = Array.new - @event_summaries = Array.new - account_deletion_events - end - - def handle_cloudwatch_event(event) - case event['name'] - when ACCOUNT_DELETION_STARTED_EVENT - process_account_reset_request(event) - when ACCOUNT_DELETION_SUBMITTED_EVENT - process_account_reset_delete(event) - when ACCOUNT_DELETION_CANCELED_EVENT - process_account_reset_cancel(event) - end - end - - def finish - event_summaries - end - - private - - def process_account_reset_request(event) - event_message = { - title: 'Account deletion Request', - attributes: [ - { type: :account_deletion_request, - description: "On #{event["@timestamp"]} user initiated account deletion" }, - ], - } - event_summaries.push(event_message) - end - - def process_account_reset_cancel(event) - event_message = { - title: 'Account deletion cancelled', - attributes: [ - { type: :account_deletion_cancelled, - description: "On #{event["@timestamp"]} user canceled account deletion" }, - ], - } - event_summaries.push(event_message) - end - - def process_account_reset_delete(event) - message = event['@message'] - age = message['properties']['event_properties']['account_age_in_days'] - date = event['@timestamp'] - event_message = { - title: 'Account deleted', - attributes: [ - { - type: :account_deleted, - description: "On #{date} user deleted their account which was #{age} days old", - }, - ], - } - event_summaries.push(event_message) - end - end -end diff --git a/lib/event_summarizer/example_matcher.rb b/lib/event_summarizer/example_matcher.rb deleted file mode 100644 index b7807ec47f0..00000000000 --- a/lib/event_summarizer/example_matcher.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module EventSummarizer - class ExampleMatcher - attr_reader :event_count - - def initialize - @event_count = 0 - end - - def handle_cloudwatch_event(_event) - @event_count += 1 - end - - def finish - [ - { - title: 'Processed some events', - attributes: [ - { type: :event_count, description: "Processed #{event_count} event(s)" }, - ], - }.tap do - @event_count = 0 - end, - ] - end - end -end diff --git a/lib/event_summarizer/idv_matcher.rb b/lib/event_summarizer/idv_matcher.rb deleted file mode 100644 index fdade426166..00000000000 --- a/lib/event_summarizer/idv_matcher.rb +++ /dev/null @@ -1,538 +0,0 @@ -# frozen_string_literal: true - -require 'active_support' -require 'active_support/time' - -require 'event_summarizer/vendor_result_evaluators/aamva' -require 'event_summarizer/vendor_result_evaluators/instant_verify' -require 'event_summarizer/vendor_result_evaluators/true_id' - -module EventSummarizer - class IdvMatcher - IDV_WELCOME_SUBMITTED_EVENT = 'IdV: doc auth welcome submitted' - IDV_GPO_CODE_SUBMITTED_EVENT = 'IdV: enter verify by mail code submitted' - IDV_FINAL_RESOLUTION_EVENT = 'IdV: final resolution' - IDV_IMAGE_UPLOAD_VENDOR_SUBMITTED_EVENT = 'IdV: doc auth image upload vendor submitted' - IDV_VERIFY_PROOFING_RESULTS_EVENT = 'IdV: doc auth verify proofing results' - IPP_ENROLLMENT_STATUS_UPDATED_EVENT = 'GetUspsProofingResultsJob: Enrollment status updated' - PROFILE_ENCRYPTION_INVALID_EVENT = 'Profile Encryption: Invalid' - RATE_LIMIT_REACHED_EVENT = 'Rate Limit Reached' - - EVENT_PROPERTIES = ['@message', 'properties', 'event_properties'].freeze - - VENDORS = { - 'TrueID' => { - id: :trueid, - name: 'True ID', - evaluator_module: EventSummarizer::VendorResultEvaluators::TrueId, - }, - 'lexisnexis:instant_verify' => { - id: :instant_verify, - name: 'Instant Verify', - evaluator_module: EventSummarizer::VendorResultEvaluators::InstantVerify, - }, - 'aamva:state_id' => { - id: :aamva, - name: 'AAMVA', - evaluator_module: EventSummarizer::VendorResultEvaluators::Aamva, - }, - }.freeze - - UNKNOWN_VENDOR = { - id: :unknown, - name: 'Unknown vendor', - }.freeze - - IdvAttempt = Data.define( - :started_at, - :significant_events, - ) do - def initialize(started_at:, significant_events: []) - super(started_at:, significant_events:) - end - - def flagged_for_fraud? - self.significant_events.any? { |e| e.type == :flagged_for_fraud } - end - - def gpo? - self.significant_events.any? { |e| e.type == :start_gpo } - end - - def gpo_pending? - gpo? && !self.significant_events.any? { |e| e.type == :gpo_code_success } - end - - def ipp? - self.significant_events.any? { |e| e.type == :start_ipp } - end - - def ipp_pending? - ipp? && !self.significant_events.any? { |e| e.type == :ipp_enrollment_complete } - end - - def successful? - self.significant_events.any? do |e| - e.type == :verified - end - end - - def workflow_complete? - gpo? || ipp? || flagged_for_fraud? || successful? - end - end.freeze - - SignificantIdvEvent = Data.define( - :timestamp, - :type, - :description, - ).freeze - - attr_reader :current_idv_attempt - attr_reader :idv_attempts - attr_reader :idv_abandoned_event - - def initialize - @idv_attempts = [] - @current_idv_attempt = nil - end - - # @return {Hash,nil} - def handle_cloudwatch_event(event) - case event['name'] - when IDV_WELCOME_SUBMITTED_EVENT - start_new_idv_attempt(event:) - - when IDV_FINAL_RESOLUTION_EVENT - - for_current_idv_attempt(event:) do - handle_final_resolution_event(event:) - end - - when IDV_GPO_CODE_SUBMITTED_EVENT - for_current_idv_attempt(event:) do - handle_gpo_code_submission(event:) - end - - when IPP_ENROLLMENT_STATUS_UPDATED_EVENT - for_current_idv_attempt(event:) do - handle_ipp_enrollment_status_update(event:) - end - - when IDV_IMAGE_UPLOAD_VENDOR_SUBMITTED_EVENT - for_current_idv_attempt(event:) do - handle_image_upload_vendor_submitted(event:) - end - - when IDV_VERIFY_PROOFING_RESULTS_EVENT - for_current_idv_attempt(event:) do - handle_verify_proofing_results_event(event:) - end - - when PROFILE_ENCRYPTION_INVALID_EVENT - for_current_idv_attempt(event:) do - handle_profile_encryption_error(event:) - end - - when RATE_LIMIT_REACHED_EVENT - handle_rate_limit_reached(event:) - - else - if ENV['LOG_UNHANDLED_EVENTS'] - warn "#{event['@timestamp']} #{event['name']}" - end - end - - check_for_idv_abandonment(event) - end - - def finish - finish_current_idv_attempt - - self.idv_attempts.map { |a| summarize_idv_attempt(a) }.tap do - idv_attempts.clear - end - end - - private - - def add_significant_event( - timestamp:, - type:, - description: - ) - current_idv_attempt.significant_events << SignificantIdvEvent.new( - timestamp:, - type:, - description:, - ) - end - - def check_for_idv_abandonment(event) - return if current_idv_attempt.nil? - - looks_like_idv = /^idv/i.match(event['name']) - return if !looks_like_idv - - if idv_abandoned_event.nil? - @idv_abandoned_event = event - return - end - - is_a_little_bit_newer = event['@timestamp'] - idv_abandoned_event['@timestamp'] < 1.hour - - if is_a_little_bit_newer - @idv_abandoned_event = event - end - end - - def for_current_idv_attempt(event:, &block) - if !current_idv_attempt - warn <<~WARNING - Encountered #{event['name']} without seeing a '#{IDV_WELCOME_SUBMITTED_EVENT}' event first. - This could indicate you need to include earlier events in your request. - WARNING - return - end - - block.call(event) - end - - def finish_current_idv_attempt - if !current_idv_attempt.nil? - looks_like_abandonment = - !current_idv_attempt.workflow_complete? && - !idv_abandoned_event.nil? && - idv_abandoned_event['@timestamp'] < 1.hour.ago - - if looks_like_abandonment - add_significant_event( - type: :idv_abandoned, - timestamp: idv_abandoned_event['@timestamp'], - description: 'User abandoned identity verification', - ) - end - end - - idv_attempts << current_idv_attempt if current_idv_attempt - @current_idv_attempt = nil - @idv_abandoned_event = nil - end - - # @return {Hash,nil} - def handle_final_resolution_event(event:) - timestamp = event['@timestamp'] - - gpo_pending = !!event.dig( - *EVENT_PROPERTIES, - 'gpo_verification_pending', - ) - - if gpo_pending - add_significant_event( - type: :start_gpo, - timestamp:, - description: 'User requested a letter to verify by mail', - ) - end - - ipp_pending = !!event.dig( - *EVENT_PROPERTIES, - 'in_person_verification_pending', - ) - - if ipp_pending - add_significant_event( - type: :start_ipp, - timestamp:, - descirption: 'User entered the in-person proofing flow', - ) - end - - fraud_review_pending = !!event.dig( - *EVENT_PROPERTIES, - 'fraud_review_pending', - ) - - if fraud_review_pending - add_significant_event( - type: :flagged_for_fraud, - timestamp:, - description: 'User was flagged for fraud', - ) - end - - pending = - gpo_pending || - ipp_pending || - fraud_review_pending - - if !pending - add_significant_event( - type: :verified, - timestamp:, - description: 'User completed identity verification (remote unsupervised flow)', - ) - - finish_current_idv_attempt - end - end - - def handle_gpo_code_submission(event:) - timestamp = event['@timestamp'] - success = event.dig(*EVENT_PROPERTIES, 'success') - - if !success - add_significant_event( - type: :gpo_code_failure, - timestamp:, - description: 'The user entered an invalid GPO code', - ) - return - end - - # User successfully entered GPO code. If fraud review is not pending, - # then they are fully verified - fraud_review_pending = !!event.dig( - *EVENT_PROPERTIES, - 'fraud_check_failed', - ) - - fully_verified = !fraud_review_pending - - add_significant_event( - type: :gpo_code_success, - timestamp:, - description:, - ) - - if fully_verified - add_significant_event( - type: :verified, - timestamp:, - description: 'User completed identity verification', - ) - - finish_current_idv_attempt - end - end - - def handle_ipp_enrollment_status_update(event:) - timestamp = event['@timestamp'] - passed = event.dig(*EVENT_PROPERTIES, 'passed') - tmx_status = event.dig(*EVENT_PROPERTIES, 'tmx_status') - - return if !passed - - add_significant_event( - type: :ipp_enrollment_complete, - timestamp:, - description: 'User visited the post office and completed IPP enrollment', - ) - - verified = tmx_status != 'review' && tmx_status != 'reject' - - if verified - current_idv_attempt.event << SignificantIdvEvent.new( - type: :verified, - timestamp:, - description: 'User is fully verified', - ) - end - end - - def handle_profile_encryption_error(event:) - caveats = [ - # TODO these need to check if GPO/IPP were still pending at time of the event - current_idv_attempt.gpo? ? 'The user will not be able to enter a GPO code' : nil, - current_idv_attempt.ipp? ? 'the user will not be able to verify in-person' : nil, - ].compact - - add_significant_event( - type: :password_reset, - timestamp: event['@timestamp'], - description: [ - 'The user reset their password and did not provide their personal key.', - caveats.length > 0 ? - "The user will not be able to #{caveats.join(' or ')}" : - nil, - ].compact.join(' '), - ) - end - - def handle_rate_limit_reached(event:) - limiters = { - 'idv_doc_auth' => 'Doc Auth', - } - - limiter_type = event.dig(*EVENT_PROPERTIES, 'limiter_type') - - limit_name = limiters[limiter_type] - - return if limit_name.blank? - - timestamp = event['@timestamp'] - - for_current_idv_attempt(event:) do - add_significant_event( - type: :rate_limited, - timestamp:, - description: "Rate limited for #{limit_name}", - ) - end - end - - def handle_image_upload_vendor_submitted(event:) - timestamp = event['@timestamp'] - success = event.dig(*EVENT_PROPERTIES, 'success') - doc_type = event.dig(*EVENT_PROPERTIES, 'DocClassName') - - if success - prior_failures = current_idv_attempt.significant_events.count do |e| - e.type == :failed_document_capture - end - attempts = prior_failures > 0 ? "after #{prior_failures} tries" : 'on the first attempt' - - add_significant_event( - timestamp:, - type: :passed_document_capture, - description: "User successfully verified their #{doc_type.downcase} #{attempts}", - ) - return - end - - prev_count = current_idv_attempt.significant_events.count - - alerts = event.dig(*EVENT_PROPERTIES, 'processed_alerts') - alerts['success'] = false - alerts['vendor_name'] = event.dig(*EVENT_PROPERTIES, 'vendor') - - add_events_for_failed_vendor_result( - alerts, - timestamp:, - ) - - any_events_added = current_idv_attempt.significant_events.count > prev_count - - if !any_events_added - add_significant_event( - timestamp:, - type: :failed_document_capture, - description: "User failed to verify their #{doc_type.downcase} (check logs for reason)", - ) - end - end - - def handle_verify_proofing_results_event(event:) - timestamp = event['@timestamp'] - success = event.dig(*EVENT_PROPERTIES, 'success') - - if success - # We only really care about passing identity resolution if the - # user previously failed in this attempt - - prior_failures = current_idv_attempt.significant_events.count do |e| - e.type == :failed_identity_resolution - end - - if prior_failures > 0 - # TODO: What changed that made them be able to pass? - - add_significant_event( - timestamp:, - type: :passed_identity_resolution, - description: "User passed identity resolution after #{prior_failures + 1} tries", - ) - end - - return - end - - # Failing identity resolution is where it gets interesting - - prev_count = current_idv_attempt.significant_events.count - - add_events_for_failed_vendor_result( - event.dig( - *EVENT_PROPERTIES, 'proofing_results', 'context', 'stages', 'resolution' - ), - timestamp:, - ) - - add_events_for_failed_vendor_result( - event.dig( - *EVENT_PROPERTIES, 'proofing_results', 'context', 'stages', 'residential_address' - ), - timestamp:, - ) - - add_events_for_failed_vendor_result( - event.dig( - *EVENT_PROPERTIES, 'proofing_results', 'context', 'stages', 'state_id' - ), - timestamp:, - ) - - any_events_added = current_idv_attempt.significant_events.count > prev_count - - if !any_events_added - add_significant_event( - timestamp:, - type: :failed_identity_resolution, - description: 'User failed identity resolution (check logs for reason)', - ) - end - end - - def add_events_for_failed_vendor_result(result, timestamp:) - return if result['success'] - - vendor = VENDORS[result['vendor_name']] || UNKNOWN_VENDOR - evaluator = vendor[:evaluator_module] - - if !evaluator.present? - add_significant_event( - type: :"#{vendor[:id]}_request_failed", - timestamp:, - description: "Request to #{vendor[:name]} failed.", - ) - return - end - - evaluation = evaluator.evaluate_result(result) - add_significant_event(**evaluation, timestamp:) if evaluation - end - - # @return {IdvAttempt,nil} The previous IdvAttempt (if any) - def start_new_idv_attempt(event:) - finish_current_idv_attempt if current_idv_attempt - - @current_idv_attempt = IdvAttempt.new( - started_at: event['@timestamp'], - ) - end - - def summarize_idv_attempt(attempt) - type = :idv - title = 'Identity verification started' - attributes = attempt.significant_events.map do |e| - { - type: e.type, - timestamp: e.timestamp, - description: e.description, - } - end - - if attempt.successful? - title = 'Identity verified' - end - - { - started_at: attempt.started_at, - title:, - type:, - attributes:, - } - end - end -end diff --git a/lib/event_summarizer/vendor_result_evaluators/aamva.rb b/lib/event_summarizer/vendor_result_evaluators/aamva.rb deleted file mode 100644 index 1929b8acd9f..00000000000 --- a/lib/event_summarizer/vendor_result_evaluators/aamva.rb +++ /dev/null @@ -1,129 +0,0 @@ -# frozen_string_literal: true - -module EventSummarizer - module VendorResultEvaluators - module Aamva - ID_TYPES = { - 'state_id_card' => 'non-driving ID card', - 'drivers_license' => "drivers' license", - }.freeze - - # TODO: Load these from the AAMVA proofer or put them somewhere common - - REQUIRED_VERIFICATION_ATTRIBUTES = %i[ - state_id_number - dob - last_name - first_name - ].freeze - - REQUIRED_IF_PRESENT_ATTRIBUTES = [:state_id_expiration].freeze - - # @param result {Hash} The result structure logged to Cloudwatch - # @return [Hash] A Hash with a type, timestamp, and description key. - def self.evaluate_result(result) - if result['success'] - return { - type: :aamva_success, - description: 'AAMVA call succeeded', - } - end - - if result['timed_out'] - return { - type: :aamva_timed_out, - description: 'AAMVA request timed out.', - } - end - - if result['mva_exception'] - state = result['state_id_jurisdiction'] - return { - type: :aamva_mva_exception, - # rubocop:disable Layout/LineLength - description: "AAMVA request failed because the MVA in #{state} failed to return a response.", - # rubocop:enable Layout/LineLength - } - end - - if result['exception'] - - description = 'AAMVA request resulted in an exception' - - m = /ExceptionText: (.+?),/.match(result['exception']) - if m.present? - description = "#{description} (#{m[1]})" - end - - return { - type: :aamva_exception, - description:, - } - end - - # The API call failed because of actual errors in the user's data. - # Try to come up with an explanation - - explanation = explain_errors(result) || 'Check logs for more info.' - - return { - type: :aamva_error, - description: "AAMVA request failed. #{explanation}", - } - end - - def self.explain_errors(result) - # The values in the errors object are arrays - attributes = {} - result['errors'].each do |key, values| - attributes[key] = values.first - end - - id_type = ID_TYPES[result['state_id_type']] || 'id card' - state = result['state_id_jurisdiction'] - - if mva_says_invalid_id_number?(attributes) - # rubocop:disable Layout/LineLength - return "The ID # from the user's #{id_type} was invalid according to the state of #{state}" - # rubocop:enable Layout/LineLength - end - - failed_attributes = relevant_failed_attributes(attributes) - - if !failed_attributes.empty? - plural = failed_attributes.length == 1 ? '' : 's' - - # rubocop:disable Layout/LineLength - "#{failed_attributes.length} attribute#{plural} failed to validate: #{failed_attributes.join(', ')}" - # rubocop:enable Layout/LineLength - end - end - - def self.mva_says_invalid_id_number?(attributes) - # When all attributes are marked "MISSING", except ID number, - # which is marked "UNVERIFIED", that indicates the MVA could not - # find the ID number to compare PII - - missing_count = attributes.count do |_attr, status| - status == 'MISSING' - end - - attributes['state_id_number'] == 'UNVERIFIED' && missing_count == attributes.count - 1 - end - - def self.relevant_failed_attributes(attributes) - failed_attributes = Set.new - - REQUIRED_VERIFICATION_ATTRIBUTES.each do |attr| - failed_attributes << attr if attributes[attr] != 'VERIFIED' - end - - REQUIRED_IF_PRESENT_ATTRIBUTES.each do |attr| - failed_attributes << attr if attributes[attr].present? && attributes[attr] != 'VERIFIED' - end - - failed_attributes - end - end - end -end diff --git a/lib/event_summarizer/vendor_result_evaluators/instant_verify.rb b/lib/event_summarizer/vendor_result_evaluators/instant_verify.rb deleted file mode 100644 index e8d7941ee0e..00000000000 --- a/lib/event_summarizer/vendor_result_evaluators/instant_verify.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'active_support/core_ext/string/inflections' - -module EventSummarizer - module VendorResultEvaluators - module InstantVerify - # @param result {Hash} The result structure logged to Cloudwatch - # @return [Hash] A Hash with a type and description keys. - def self.evaluate_result(result) - if result['success'] - { - type: :instant_verify_success, - description: 'Instant Verify call succeeded', - } - elsif result['timed_out'] - { - type: :instant_verify_timed_out, - description: 'Instant Verify request timed out.', - } - elsif result['exception'] - { - type: :instant_verify_exception, - description: 'Instant Verify request resulted in an exception', - } - else - # The API call failed because of actual errors in the user's data. - # Try to come up with an explanation - - explanation = explain_errors(result) || 'Check logs for more info.' - - { - type: :instant_verify_error, - description: "Instant Verify request failed. #{explanation}", - } - end - end - - # Attempts to render a legible explanation of what went wrong in a - # LexisNexis Instant Verify request. - # @param result {Hash} The result structure logged to Cloudwatch - # @return {String} - def self.explain_errors(result) - # (The structure of the 'errors' key for Instant Verify is kind of weird) - - failed_items = [] - - result.dig('errors', 'InstantVerify')&.each do |iv_instance| - next if iv_instance['ProductStatus'] != 'fail' - iv_instance['Items'].each do |item| - if item['ItemStatus'] == 'fail' - failed_items << item - end - end - end - - if failed_items.empty? - return 'Check the full logs for more info.' - end - - checks = failed_items.map do |item| - name = item['ItemName'] - reason = item['ItemReason'] - reason_code = reason ? reason['Code'] : nil - - if reason_code - # TODO: Translate these reason codes to plain language - # TODO: Add suggestions for how the user could remedy - "#{name} (#{reason_code})" - else - name - end - end - - "#{checks.length} #{'check'.pluralize(checks.length)} failed: #{checks.join(", ")}" - end - end - end -end diff --git a/lib/event_summarizer/vendor_result_evaluators/true_id.rb b/lib/event_summarizer/vendor_result_evaluators/true_id.rb deleted file mode 100644 index f7b59694974..00000000000 --- a/lib/event_summarizer/vendor_result_evaluators/true_id.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module EventSummarizer - module VendorResultEvaluators - module TrueId - # @param result {Hash} The array of processed_alerts.failed logged to Cloudwatch - # @return [Hash] A Hash with a type and description keys. - def self.evaluate_result(result) - alerts = [] - result['failed'].each do |alert| - if alert['result'] == 'Failed' - alerts << { - type: :"trueid_#{alert['name'].parameterize(separator: '_')}", - description: alert['disposition'], - } - end - end - - if alerts.present? - alerts.uniq! { |a| a[:description] } - return { - type: :trueid_failures, - description: "TrueID request failed. #{alerts.map { |a| a[:description] }.join(' ')}", - } - end - end - end - end -end diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 849493a87ff..b2dc14f6aab 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -147,7 +147,6 @@ def self.store config.add(:enable_rate_limiting, type: :boolean) config.add(:enable_test_routes, type: :boolean) config.add(:enable_usps_verification, type: :boolean) - config.add(:encrypted_document_storage_s3_bucket, type: :string) config.add(:event_disavowal_expiration_hours, type: :integer) config.add(:facial_match_general_availability_enabled, type: :boolean) config.add(:feature_idv_force_gpo_verification_enabled, type: :boolean) @@ -194,7 +193,6 @@ def self.store config.add(:idv_send_link_max_attempts, type: :integer) config.add(:idv_socure_reason_code_download_enabled, type: :boolean) config.add(:idv_socure_shadow_mode_enabled, type: :boolean) - config.add(:idv_socure_shadow_mode_enabled_for_docv_users, type: :boolean) config.add(:idv_sp_required, type: :boolean) config.add(:in_person_completion_survey_url, type: :string) config.add(:in_person_doc_auth_button_enabled, type: :boolean) @@ -266,8 +264,8 @@ def self.store config.add(:logo_upload_enabled, type: :boolean) config.add(:mailer_domain_name) config.add(:max_auth_apps_per_account, type: :integer) - config.add(:max_sign_in_failures, type: :integer) - config.add(:max_sign_in_failures_window_in_seconds, type: :integer) + config.add(:max_bad_passwords, type: :integer) + config.add(:max_bad_passwords_window_in_seconds, type: :integer) config.add(:max_emails_per_account, type: :integer) config.add(:max_mail_events, type: :integer) config.add(:max_mail_events_window_in_days, type: :integer) diff --git a/package.json b/package.json index ebb73b0c461..74a0f354452 100644 --- a/package.json +++ b/package.json @@ -92,5 +92,9 @@ "swr": "^2.0.0", "typescript": "^5.7.2", "yarn-deduplicate": "^6.0.2" + }, + "resolutions": { + "minimist": "1.2.6", + "postcss": "8.4.31" } } diff --git a/spec/bin/summarize-user-events_spec.rb b/spec/bin/summarize-user-events_spec.rb deleted file mode 100644 index 9923123c849..00000000000 --- a/spec/bin/summarize-user-events_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -require 'rails_helper' -load Rails.root.join('bin/summarize-user-events') - -RSpec.describe SummarizeUserEvents do - let(:user_uuid) { nil } - let(:start_time) { nil } - let(:end_time) { nil } - let(:zone) { 'America/New_York' } - let(:stdout) { StringIO.new } - let(:stderr) { StringIO.new } - - subject(:instance) do - described_class.new( - file_name: nil, - user_uuid:, - start_time:, - end_time:, - zone:, - stdout:, - stderr:, - ) - end - - describe '#normalize_event!' do - let(:event) do - { - 'name' => 'test event', - '@timestamp' => '2024-12-31 21:21:47.374', - '@message' => { - '@timestamp' => '2024-12-31 21:21:47.374', - 'name' => 'test event', - }, - } - end - - subject(:normalized_event) do - event.dup.tap do |event| - instance.normalize_event!(event) - end - end - - context 'when @message is a string' do - let(:event) do - super().tap do |event| - event['@message'] = JSON.generate(event['@message']) - end - end - it 'parses @message as JSON' do - expect(event['@message']).to be_a(String) - expect(normalized_event['@message']).to eql( - '@timestamp' => '2024-12-31 21:21:47.374', - 'name' => 'test event', - ) - end - end - - context 'when @timestamp is a string' do - it 'parses it in UTC' do - expected = Time.zone.parse('2024-12-31 21:21:47.374 UTC') - Time.use_zone('America/Los_Angeles') do - expect(normalized_event['@timestamp']).to eql(expected) - end - end - end - end - - describe '#parse_command_line_options' do - let(:argv) do - [] - end - - subject(:parsed) do - described_class.parse_command_line_options(argv) - end - - it 'parses default options' do - expect(parsed).to eql( - { zone: 'America/New_York' }, - ) - end - - context '-z' do - let(:argv) { ['-z', 'America/Los_Angeles'] } - - it 'parses zone' do - expect(parsed).to eql( - { - zone: 'America/Los_Angeles', - }, - ) - end - end - end - - describe '#parse_time' do - let(:input) { nil } - - subject(:actual) do - instance.parse_time(input) - end - - context 'with valid UTC timestamp' do - let(:input) do - '2025-01-07T19:56:03Z' - end - it 'parses it as UTC, then converts to configured zone' do - expect(actual.to_s).to eql( - '2025-01-07 14:56:03 -0500', - ) - end - end - - context 'with a timestamp with no zone specified' do - let(:input) do - '2025-01-07T19:56:03' - end - it 'parses it as UTC, then converts to configured zone' do - expect(actual.to_s).to eql( - '2025-01-07 14:56:03 -0500', - ) - end - end - - context 'with a timestamp with a different zone specified' do - let(:input) do - '2025-01-07T19:56:03 -0600' - end - it 'parses it as UTC, then converts to configured zone' do - expect(actual.to_s).to eql( - '2025-01-07 20:56:03 -0500', - ) - end - end - - context 'with an invalid time value' do - let(:input) { 'not even a time' } - it 'returns nil' do - expect(actual).to eql(nil) - end - end - - context 'with blank string' do - let(:input) { '' } - it 'returns nil' do - expect(actual).to eql(nil) - end - end - end - - describe '#run' do - subject(:command_output) do - instance.run - stdout.string - end - - let(:cloudwatch_events) do - [ - { - '@timestamp' => '2024-12-30 15:42:51.336', - '@message' => JSON.generate( - { - name: 'IdV: doc auth welcome submitted', - }, - ), - }, - ] - end - - before do - allow(instance).to receive(:cloudwatch_source) do |&block| - cloudwatch_events.each do |raw_event| - block.call(raw_event) - end - end - end - - it 'matches expected output' do - expect(command_output).to eql(<<~END) - ## Processed some events - * Processed 1 event(s) - - ## Identity verification started (December 30, 2024 at 10:42 AM EST) - * (10:42 AM) User abandoned identity verification - END - end - end -end diff --git a/spec/config/initializers/ab_tests_spec.rb b/spec/config/initializers/ab_tests_spec.rb index b2fe27b55c5..b6b47801ade 100644 --- a/spec/config/initializers/ab_tests_spec.rb +++ b/spec/config/initializers/ab_tests_spec.rb @@ -53,21 +53,17 @@ }, } end - it 'returns a bucket' do expect(bucket).not_to be_nil end end - context 'and the user does not have an Idv::Session' do let(:user_session) do {} end - it 'does not return a bucket' do expect(bucket).to be_nil end - it 'does not write :idv key in user_session' do expect { bucket }.not_to change { user_session } end @@ -79,7 +75,6 @@ let(:session) do { document_capture_session_uuid: 'a-random-uuid' } end - it 'returns a bucket' do expect(bucket).not_to be_nil end @@ -95,7 +90,6 @@ context 'when A/B test is disabled and it would otherwise assign a bucket' do let(:user) { build(:user) } - let(:user_session) do { idv: { @@ -108,7 +102,6 @@ disable_ab_test.call reload_ab_tests end - it 'does not assign a bucket' do expect(bucket).to be_nil end @@ -267,11 +260,11 @@ end end - describe 'SOCURE_IDV_SHADOW_MODE_FOR_NON_DOCV_USERS' do + describe 'SOCURE_IDV_SHADOW_MODE' do let(:user) { create(:user) } subject(:bucket) do - AbTests::SOCURE_IDV_SHADOW_MODE_FOR_NON_DOCV_USERS.bucket( + AbTests::SOCURE_IDV_SHADOW_MODE.bucket( request: nil, service_provider: nil, session: nil, @@ -302,7 +295,7 @@ end it 'returns a bucket' do - expect(bucket).to eq :socure_shadow_mode_for_non_docv_users + expect(bucket).to eq :shadow_mode_enabled end end end diff --git a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb index 5598337bc29..e9f5ef0f835 100644 --- a/spec/controllers/idv/by_mail/enter_code_controller_spec.rb +++ b/spec/controllers/idv/by_mail/enter_code_controller_spec.rb @@ -181,11 +181,8 @@ let(:user) { create(:user, :with_pending_gpo_profile, created_at: 2.days.ago) } let!(:pending_profile) { user.gpo_verification_pending_profile } - let(:initiating_service_provider) { create(:service_provider) } let(:success) { true } - before { pending_profile.update!(initiating_service_provider:) } - it 'uses the PII from the pending profile' do # action will make the profile active, so grab the ID here. pending_profile_id = pending_profile.id @@ -205,7 +202,6 @@ fraud_check_failed: false, enqueued_at: pending_profile.gpo_confirmation_codes.last.code_sent_at, profile_age_in_seconds: instance_of(Integer), - initiating_service_provider: initiating_service_provider.issuer, which_letter: 1, letter_count: 1, submit_attempts: 1, @@ -251,7 +247,6 @@ fraud_check_failed: false, enqueued_at: pending_profile.gpo_confirmation_codes.last.code_sent_at, profile_age_in_seconds: instance_of(Integer), - initiating_service_provider: initiating_service_provider.issuer, which_letter: 1, letter_count: 1, submit_attempts: 1, @@ -281,7 +276,6 @@ fraud_check_failed: true, enqueued_at: pending_profile.gpo_confirmation_codes.last.code_sent_at, profile_age_in_seconds: instance_of(Integer), - initiating_service_provider: initiating_service_provider.issuer, which_letter: 1, letter_count: 1, submit_attempts: 1, @@ -319,7 +313,6 @@ fraud_check_failed: true, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, profile_age_in_seconds: instance_of(Integer), - initiating_service_provider: initiating_service_provider.issuer, which_letter: 1, letter_count: 1, submit_attempts: 1, @@ -362,7 +355,6 @@ fraud_check_failed: true, enqueued_at: user.pending_profile.gpo_confirmation_codes.last.code_sent_at, profile_age_in_seconds: instance_of(Integer), - initiating_service_provider: initiating_service_provider.issuer, which_letter: 1, letter_count: 1, submit_attempts: 1, diff --git a/spec/controllers/idv/in_person/verify_info_controller_spec.rb b/spec/controllers/idv/in_person/verify_info_controller_spec.rb index 7ba8e8e4ae5..0d190f44107 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -349,7 +349,6 @@ user_id: anything, request_ip: request.remote_ip, ipp_enrollment_in_progress: false, - proofing_components: Idv::ProofingComponents, ) put :update @@ -365,7 +364,6 @@ user_id: anything, request_ip: anything, ipp_enrollment_in_progress: true, - proofing_components: Idv::ProofingComponents, ) put :update @@ -394,7 +392,6 @@ user_id: anything, request_ip: request.remote_ip, ipp_enrollment_in_progress: true, - proofing_components: Idv::ProofingComponents, ) put :update diff --git a/spec/controllers/saml_idp_controller_spec.rb b/spec/controllers/saml_idp_controller_spec.rb index 7d5a2b5a49d..1e23ba2b050 100644 --- a/spec/controllers/saml_idp_controller_spec.rb +++ b/spec/controllers/saml_idp_controller_spec.rb @@ -1694,66 +1694,93 @@ def name_id_version(format_urn) ) end - context 'when request is using SHA1 as the digest method algorithm' do + context 'when request is using SHA1 as the signature method algorithm' do let(:auth_settings) do saml_settings( overrides: { security: { authn_requests_signed:, - digest_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', - signature_method:, + signature_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', }, }, ) end - context 'when request is using SHA256 as the signature method algorithm' do - let(:signature_method) do - 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1' - end - - it 'notes an error in the event' do + context 'when the certificate matches' do + it 'does not note that certs are different in the event' do user.identities.last.update!(verified_attributes: ['email']) generate_saml_response(user, auth_settings) expect(response.status).to eq(200) expect(@analytics).to have_logged_event( - 'SAML Auth', hash_including( - request_signed: authn_requests_signed, - cert_error_details: [ - { - cert: '16692258094164984098', - error_code: :wrong_sig_algorithm, - }, - { - cert: '14834808178619537243', - error_code: :request_cert_not_registered, - }, - ], + 'SAML Auth', hash_not_including( + certs_different: true, + sha256_matching_cert: matching_cert_serial, ) ) end end - context 'when request is using SHA1 as the signature method algorithm' do - let(:signature_method) do - 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + context 'when the certificate does not match' do + let(:wrong_cert) do + OpenSSL::X509::Certificate.new( + Rails.root.join('certs', 'sp', 'saml_test_sp2.crt').read, + ) + end + + before do + service_provider.update!(certs: [wrong_cert, saml_test_sp_cert]) end - it 'notes an error in the event' do + it 'notes that certs are different in the event' do user.identities.last.update!(verified_attributes: ['email']) generate_saml_response(user, auth_settings) expect(response.status).to eq(200) expect(@analytics).to have_logged_event( - 'SAML Auth', hash_not_including( - :cert_error_details, + 'SAML Auth', hash_including( + certs_different: true, + sha256_matching_cert: wrong_cert.serial.to_s, ) ) end end end + context 'when request is using SHA1 as the digest method algorithm' do + let(:auth_settings) do + saml_settings( + overrides: { + security: { + authn_requests_signed:, + digest_method: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha1', + }, + }, + ) + end + + it 'notes an error in the event' do + user.identities.last.update!(verified_attributes: ['email']) + generate_saml_response(user, auth_settings) + + expect(response.status).to eq(200) + expect(@analytics).to have_logged_event( + 'SAML Auth', hash_including( + request_signed: authn_requests_signed, + cert_error_details: [ + { + cert: '16692258094164984098', + error_code: :fingerprint_mismatch, + }, + { + cert: '14834808178619537243', error_code: :fingerprint_mismatch + }, + ], + ) + ) + end + end + context 'Certificate sig validation fails because of namespace bug' do let(:request_sp) { double } @@ -1809,7 +1836,7 @@ def name_id_version(format_urn) cert_error_details = [ { cert: saml_test_sp_cert_serial, - error_code: :request_cert_not_registered, + error_code: :fingerprint_mismatch, }, ] @@ -2357,8 +2384,7 @@ def name_id_version(format_urn) end it 'has valid signature' do - cert = OpenSSL::X509::Certificate.new(saml_test_idp_cert) - expect(xmldoc.saml_document.valid_signature?(cert)).to eq(true) + expect(xmldoc.saml_document.valid_signature?(idp_fingerprint)).to eq(true) end context 'Reference' do diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 39d72de7baa..a033b885eef 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -98,7 +98,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 0, + bad_password_count: 0, sp_request_url_present: false, remember_device: false, new_device: true, @@ -171,7 +171,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 0, + bad_password_count: 0, sp_request_url_present: false, remember_device: false, new_device: false, @@ -183,11 +183,11 @@ context 'locked out session' do let(:locked_at) { Time.zone.now } let(:user) { create(:user, :fully_registered) } - let(:sign_in_failure_window) { IdentityConfig.store.max_sign_in_failures_window_in_seconds } + let(:bad_password_window) { IdentityConfig.store.max_bad_passwords_window_in_seconds } before do - session[:sign_in_failure_count] = IdentityConfig.store.max_sign_in_failures + 1 - session[:max_sign_in_failures_at] = locked_at.to_i + session[:bad_password_count] = IdentityConfig.store.max_bad_passwords + 1 + session[:max_bad_passwords_at] = locked_at.to_i end it 'renders an error letting user know they are locked out for a period of time', @@ -196,14 +196,14 @@ current_time = Time.zone.now time_in_hours = distance_of_time_in_words( current_time, - (locked_at + sign_in_failure_window.seconds), + (locked_at + bad_password_window.seconds), true, ) expect(response).to redirect_to root_url expect(flash[:error]).to eq( t( - 'errors.sign_in.sign_in_failure_limit', + 'errors.sign_in.bad_password_limit', time_left: time_in_hours, ), ) @@ -211,7 +211,7 @@ end it 'prevents attempt and logs after exceeding maximum rate limit' do - allow(IdentityConfig.store).to receive(:max_sign_in_failures).and_return(10_000) + allow(IdentityConfig.store).to receive(:max_bad_passwords).and_return(10_000) allow(RateLimiter).to receive(:rate_limit_config).and_return( sign_in_user_id_per_ip: { max_attempts: 6, @@ -243,7 +243,7 @@ post :create, params: { user: { email: user.email, password: 'incorrect' } } expect(flash[:error]).to eq( t( - 'errors.sign_in.sign_in_failure_limit', + 'errors.sign_in.bad_password_limit', time_left: distance_of_time_in_words(12.hours), ), ) @@ -254,7 +254,7 @@ rate_limited: true, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 8, + bad_password_count: 8, sp_request_url_present: false, remember_device: false, ) @@ -276,7 +276,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 1, + bad_password_count: 1, sp_request_url_present: false, remember_device: false, ) @@ -296,7 +296,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 1, + bad_password_count: 1, sp_request_url_present: false, remember_device: false, ) @@ -320,7 +320,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 0, + bad_password_count: 0, sp_request_url_present: false, remember_device: false, ) @@ -371,7 +371,7 @@ rate_limited: false, valid_captcha_result: false, captcha_validation_performed: true, - sign_in_failure_count: 1, + bad_password_count: 0, remember_device: false, sp_request_url_present: false, ) @@ -385,37 +385,6 @@ expect(response).to redirect_to sign_in_security_check_failed_url end end - - context 'recaptcha lock out' do - let(:locked_at) { Time.zone.now } - let(:sign_in_failure_window) { IdentityConfig.store.max_sign_in_failures_window_in_seconds } - it 'prevents attempt after exceeding maximum rate limit' do - allow(IdentityConfig.store).to receive(:max_sign_in_failures).and_return(5) - - user = create(:user, :fully_registered) - freeze_time do - current_time = Time.zone.now - rate_limit_time_left = distance_of_time_in_words( - current_time, - (locked_at + sign_in_failure_window.seconds), - true, - ) - 6.times do - post :create, params: { - user: { email: user.email, password: user.password, score: 0.1 }, - } - end - - expect(response).to redirect_to root_url - expect(flash[:error]).to eq( - t( - 'errors.sign_in.sign_in_failure_limit', - time_left: rate_limit_time_left, - ), - ) - end - end - end end it 'tracks count of multiple unsuccessful authentication attempts' do @@ -435,7 +404,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 2, + bad_password_count: 2, sp_request_url_present: false, remember_device: false, ) @@ -454,7 +423,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 1, + bad_password_count: 1, sp_request_url_present: true, remember_device: false, ) @@ -625,7 +594,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 0, + bad_password_count: 0, sp_request_url_present: false, remember_device: false, new_device: true, @@ -750,7 +719,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 0, + bad_password_count: 0, sp_request_url_present: false, remember_device: true, new_device: true, @@ -777,7 +746,7 @@ rate_limited: false, valid_captcha_result: true, captcha_validation_performed: false, - sign_in_failure_count: 0, + bad_password_count: 0, sp_request_url_present: false, remember_device: true, new_device: true, diff --git a/spec/features/visitors/bad_password_spec.rb b/spec/features/visitors/bad_password_spec.rb index e2f5de02848..846afa05716 100644 --- a/spec/features/visitors/bad_password_spec.rb +++ b/spec/features/visitors/bad_password_spec.rb @@ -4,7 +4,7 @@ include ActionView::Helpers::DateHelper let(:user) { create(:user, :fully_registered) } let(:bad_password) { 'badpassword' } - let(:window) { IdentityConfig.store.max_sign_in_failures_window_in_seconds.seconds } + let(:window) { IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds } scenario 'visitor tries too many bad passwords gets locked out then waits window seconds' do visit new_user_session_path @@ -12,12 +12,12 @@ 'devise.failure.invalid_html', link_html: t('devise.failure.invalid_link_text'), ) - IdentityConfig.store.max_sign_in_failures.times do + IdentityConfig.store.max_bad_passwords.times do fill_in_credentials_and_submit(user.email, bad_password) expect(page).to have_content(error_message) expect(page).to have_current_path(new_user_session_path) end - locked_at = Time.zone.at(page.get_rack_session['max_sign_in_failures_at']) + locked_at = Time.zone.at(page.get_rack_session['max_bad_passwords_at']) # Need to do this because getting rack session changes the url. visit new_user_session_path 2.times do @@ -29,7 +29,7 @@ time_left = distance_of_time_in_words(Time.zone.now, new_time, true) expect(page).to have_content( t( - 'errors.sign_in.sign_in_failure_limit', + 'errors.sign_in.bad_password_limit', time_left: time_left, ), ) @@ -43,13 +43,13 @@ time_left = distance_of_time_in_words(Time.zone.now, new_time, true) expect(page).to have_content( t( - 'errors.sign_in.sign_in_failure_limit', + 'errors.sign_in.bad_password_limit', time_left: time_left, ), ) end - travel_to(IdentityConfig.store.max_sign_in_failures_window_in_seconds.seconds.from_now) do + travel_to(IdentityConfig.store.max_bad_passwords_window_in_seconds.seconds.from_now) do fill_in_credentials_and_submit(user.email, bad_password) expect(page).to have_content(error_message) fill_in_credentials_and_submit(user.email, user.password) diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx index 9a63f206b08..5c8e5b3caee 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-spec.jsx @@ -74,29 +74,6 @@ describe('document-capture/components/acuant-capture', () => { }); } - /** - * Mimics Drag Drop a file to the given input. Unlike `@testing-library/user-event`, - * this does not call any click handlers associated with the input. - * - * @param {HTMLInputElement} input - * @param {File} value - */ - function dragDropFile(input, value) { - fireEvent( - input, - createEvent('input', input, { - target: { files: [value] }, - bubbles: true, - cancelable: false, - composed: true, - }), - ); - - fireEvent.drop(input, { - target: { files: [value] }, - }); - } - describe('getNormalizedAcuantCaptureFailureMessage', () => { beforeEach(() => { window.AcuantJavascriptWebSdk = { @@ -633,23 +610,6 @@ describe('document-capture/components/acuant-capture', () => { ); }); - it('onChange not called if allowUpload is false and user drags drops file', () => { - const onChange = sinon.stub(); - const { getByLabelText } = render( - - - - - , - ); - - initialize({ isCameraSupported: false }); - - const input = getByLabelText('Image'); - dragDropFile(input, validUpload); - expect(onChange).not.to.have.been.called(); - }); - it('renders error message and logs metadata if capture succeeds but the document type identified is unsupported', async () => { const trackEvent = sinon.spy(); const { getByText, findByText } = render( diff --git a/spec/jobs/resolution_proofing_job_spec.rb b/spec/jobs/resolution_proofing_job_spec.rb index e1e3254a441..c28352a132b 100644 --- a/spec/jobs/resolution_proofing_job_spec.rb +++ b/spec/jobs/resolution_proofing_job_spec.rb @@ -18,7 +18,6 @@ let(:proofing_device_profiling) { :enabled } let(:lexisnexis_threatmetrix_mock_enabled) { false } let(:ipp_enrollment_in_progress) { false } - let(:proofing_components) { nil } before do allow(IdentityConfig.store).to receive(:proofing_device_profiling) @@ -44,7 +43,6 @@ threatmetrix_session_id: threatmetrix_session_id, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - proofing_components: proofing_components, ) end @@ -546,151 +544,69 @@ end end - context 'Socure shadow mode test' do - let(:idv_socure_shadow_mode_enabled_for_docv_users) { false } - let(:idv_socure_shadow_mode_enabled) { false } - let(:doc_auth_vendor) { nil } - let(:in_shadow_mode_ab_test_bucket) { false } - - before do - allow(IdentityConfig.store).to receive(:idv_socure_shadow_mode_enabled_for_docv_users) - .and_return(idv_socure_shadow_mode_enabled_for_docv_users) - allow(IdentityConfig.store).to receive(:idv_socure_shadow_mode_enabled) - .and_return(idv_socure_shadow_mode_enabled) - - allow(instance).to receive(:shadow_mode_ab_test_bucket) do |user:| - expect(user).not_to eql(nil) - if in_shadow_mode_ab_test_bucket - :socure_shadow_mode_for_non_docv_users - end + context 'socure shadow mode' do + context 'turned on' do + before do + allow(instance).to receive(:use_shadow_mode?).and_return(true) end - stub_vendor_requests - end - - context 'when enabled' do - let(:idv_socure_shadow_mode_enabled) { true } - - context 'and user is selected in A/B test' do - let(:in_shadow_mode_ab_test_bucket) { true } - - it 'schedules a SocureShadowModeProofingJob' do - expect(SocureShadowModeProofingJob).to receive(:perform_later).with( - user_email: user.email, - user_uuid: user.uuid, - document_capture_session_result_id: document_capture_session.result_id, - encrypted_arguments: satisfy do |ciphertext| - json = JSON.parse( - Encryption::Encryptors::BackgroundProofingArgEncryptor.new.decrypt(ciphertext), - symbolize_names: true, - ) - expect(json[:applicant_pii]).to eql( - { - first_name: 'FAKEY', - middle_name: nil, - last_name: 'MCFAKERSON', - name_suffix: 'JR', - address1: '1 FAKE RD', - identity_doc_address1: '1 FAKE RD', - identity_doc_address2: nil, - identity_doc_city: 'GREAT FALLS', - identity_doc_address_state: 'MT', - identity_doc_zipcode: '59010-1234', - issuing_country_code: 'US', - address2: nil, - same_address_as_id: 'true', - city: 'GREAT FALLS', - state: 'MT', - zipcode: '59010-1234', - dob: '1938-10-06', - sex: 'male', - height: 72, - weight: nil, - eye_color: nil, - ssn: '900-66-1234', - state_id_jurisdiction: 'ND', - state_id_expiration: '2099-12-31', - state_id_issued: '2019-12-31', - state_id_number: '1111111111111', - state_id_type: 'drivers_license', - }, - ) - end, - service_provider_issuer: service_provider.issuer, - ) - - perform - end - - context 'and shadow mode also enabled for docv users' do - let(:idv_socure_shadow_mode_enabled_for_docv_users) { true } - - context 'when the user is a docv user' do - let(:proofing_components) do + it 'schedules a SocureShadowModeProofingJob' do + stub_vendor_requests + expect(SocureShadowModeProofingJob).to receive(:perform_later).with( + user_email: user.email, + user_uuid: user.uuid, + document_capture_session_result_id: document_capture_session.result_id, + encrypted_arguments: satisfy do |ciphertext| + json = JSON.parse( + Encryption::Encryptors::BackgroundProofingArgEncryptor.new.decrypt(ciphertext), + symbolize_names: true, + ) + expect(json[:applicant_pii]).to eql( { - document_check: Idp::Constants::Vendors::SOCURE, - } - end - it 'only schedules 1 SocureShadowModeProofingJob' do - expect(SocureShadowModeProofingJob).to receive(:perform_later).once - perform - end - end - end - end - - context 'and user is NOT selected in A/B test' do - let(:in_shadow_mode_ab_test_bucket) { false } - - it 'does not schedule a shadow mode job' do - expect(SocureShadowModeProofingJob).not_to receive(:perform_later) - perform - end + first_name: 'FAKEY', + middle_name: nil, + last_name: 'MCFAKERSON', + name_suffix: 'JR', + address1: '1 FAKE RD', + identity_doc_address1: '1 FAKE RD', + identity_doc_address2: nil, + identity_doc_city: 'GREAT FALLS', + identity_doc_address_state: 'MT', + identity_doc_zipcode: '59010-1234', + issuing_country_code: 'US', + address2: nil, + same_address_as_id: 'true', + city: 'GREAT FALLS', + state: 'MT', + zipcode: '59010-1234', + dob: '1938-10-06', + sex: 'male', + height: 72, + weight: nil, + eye_color: nil, + ssn: '900-66-1234', + state_id_jurisdiction: 'ND', + state_id_expiration: '2099-12-31', + state_id_issued: '2019-12-31', + state_id_number: '1111111111111', + state_id_type: 'drivers_license', + }, + ) + end, + service_provider_issuer: service_provider.issuer, + ) - context 'but shadow mode is enabled for docv users' do - let(:idv_socure_shadow_mode_enabled_for_docv_users) { true } - - context 'and the user happens to be a docv user' do - let(:proofing_components) do - { - document_check: Idp::Constants::Vendors::SOCURE, - } - end - - it 'schedules a SocureShadowModeProofingJob' do - expect(SocureShadowModeProofingJob).to receive(:perform_later).once - perform - end - end - - context 'except the user did not use Socure docv' do - it 'does not schedule a SocureShadowModeProofingJob' do - expect(SocureShadowModeProofingJob).not_to receive(:perform_later) - perform - end - end - end + perform end end - context 'when disabled' do - let(:idv_socure_shadow_mode_enabled) { false } - + context 'turned off' do it 'does not schedule a SocureShadowModeProofingJob' do stub_vendor_requests + expect(SocureShadowModeProofingJob).not_to receive(:perform_later) - perform - end - context 'but the flag to enable shadow mode for docv users was left on' do - let(:idv_socure_shadow_mode_enabled_for_docv_users) { true } - context 'when user is a docv user' do - it 'does not schedule a SocureShadowModeProofingJob' do - stub_vendor_requests - expect(SocureShadowModeProofingJob).not_to receive(:perform_later) - perform - end - end + perform end end end diff --git a/spec/lib/analytics_events_documenter_spec.rb b/spec/lib/analytics_events_documenter_spec.rb index d6dc013fbf7..81c2c24ef78 100644 --- a/spec/lib/analytics_events_documenter_spec.rb +++ b/spec/lib/analytics_events_documenter_spec.rb @@ -204,43 +204,6 @@ def some_event(*) end end - context 'param description gets munged into method descripion' do - let(:source_code) { <<~RUBY } - class AnalyticsEvents - # @param val [String] some value that - # does things and this should be part of the param - # Some Event - def some_event(val:, **extra) - track_event('Some Event', val:, **extra) - end - end - RUBY - - it 'errors' do - expect(documenter.missing_documentation.first) - .to include('method description starts with lowercase, check indentation') - end - end - - context 'rubocop comment around params description' do - let(:source_code) { <<~RUBY } - class AnalyticsEvents - # @param val [String] some value that - # does things and this should be part of the param - # Some Event - # rubocop:disable Layout/LineLength - def some_event(val:, **extra) - track_event('Some Event', val:, **extra) - end - # rubocop:enable Layout/LineLength - end - RUBY - - it 'ignores rubocop lines' do - expect(documenter.missing_documentation).to be_empty - end - end - describe '#as_json' do let(:source_code) { <<~RUBY } class AnalyticsEvents @@ -248,11 +211,9 @@ class AnalyticsEvents # @param [Integer] count number of attempts # The event that does something with stuff # @option extra [String] 'DocumentName' the document name - # rubocop:disable Layout/LineLength def some_event(success:, count:, **extra) track_event('Some Event', **extra) end - # rubocop:enable Layout/LineLength # @identity.idp.previous_event_name The Old Other Event # @identity.idp.previous_event_name Even Older Other Event @@ -262,7 +223,7 @@ def other_event end RUBY - it 'is a JSON representation of params for each event, ignoring rubocop directives' do + it 'is a JSON representation of params for each event' do expect(documenter.as_json[:events]).to match_array( [ { @@ -316,7 +277,7 @@ def some_event(success:, **extra) { event_name: 'some_event', previous_event_names: [], - description: nil, + description: '', attributes: [ { name: 'success', types: ['Boolean'], description: nil }, ], diff --git a/spec/lib/event_summarizer/idv_matcher_spec.rb b/spec/lib/event_summarizer/idv_matcher_spec.rb deleted file mode 100644 index 8c90dd3e9c4..00000000000 --- a/spec/lib/event_summarizer/idv_matcher_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'active_support' -require 'active_support/time' - -require 'event_summarizer/idv_matcher' - -RSpec.describe EventSummarizer::IdvMatcher do - describe '#handle_cloudwatch_event' do - let(:event) do - { - '@timestamp': '2024-01-02T03:04:05Z', - } - end - - subject(:matcher) do - described_class.new - end - - around do |example| - Time.use_zone('UTC') do - example.run - end - end - - context 'On unknown event' do - let(:event) { super().merge('name' => 'Some random event') } - it 'does not throw' do - matcher.handle_cloudwatch_event(event) - end - end - - context "On 'IdV: doc auth welcome submitted' event" do - let(:event) { super().merge('name' => 'IdV: doc auth welcome submitted') } - - it 'starts a new IdV attempt' do - matcher.handle_cloudwatch_event(event) - expect(matcher.current_idv_attempt).not_to eql(nil) - end - - context 'with an IdV attempt already started' do - before do - allow(matcher).to receive(:current_idv_attempt).and_return( - EventSummarizer::IdvMatcher::IdvAttempt.new( - started_at: Time.zone.now, - ), - ) - end - - it 'finishes it' do - expect(matcher.idv_attempts.length).to eql(0) - matcher.handle_cloudwatch_event(event) - expect(matcher.idv_attempts.length).to eql(1) - end - end - end - end -end diff --git a/spec/lib/event_summarizer/vendor_result_evaluators/instant_verify_spec.rb b/spec/lib/event_summarizer/vendor_result_evaluators/instant_verify_spec.rb deleted file mode 100644 index c22291b53f5..00000000000 --- a/spec/lib/event_summarizer/vendor_result_evaluators/instant_verify_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'json' -require 'event_summarizer/vendor_result_evaluators/instant_verify' - -RSpec.describe EventSummarizer::VendorResultEvaluators::InstantVerify do - let(:instant_verify_result) do - { - success: true, - errors: {}, - exception: nil, - timed_out: false, - } - end - - subject(:evaluation) do - described_class.evaluate_result( - JSON.parse(JSON.generate(instant_verify_result)), - ) - end - - context 'successful result' do - it 'looks correct' do - expect(evaluation).to eql( - { - type: :instant_verify_success, - description: 'Instant Verify call succeeded', - }, - ) - end - end - - context 'request timed out' do - let(:instant_verify_result) do - super().merge( - success: false, - errors: {}, - timed_out: true, - ) - end - - it 'reports the error appropriately' do - expect(evaluation).to eql( - { - type: :instant_verify_timed_out, - description: 'Instant Verify request timed out.', - }, - ) - end - end - - context 'failed result' do - let(:instant_verify_result) do - { - success: false, - errors: { - base: ["Verification failed with code: 'priority.scoring.model.verification.fail'"], - InstantVerify: [ - { - ProductType: 'InstantVerify', - ExecutedStepName: 'InstantVerify', - ProductConfigurationName: 'blah.config', - ProductStatus: 'fail', - ProductReason: { - Code: 'priority.scoring.model.verification.fail', - }, - Items: [ - { ItemName: 'Check1', ItemStatus: 'pass' }, - { ItemName: 'Check2', ItemStatus: 'fail' }, - { - ItemName: 'CheckWithCode', - ItemStatus: 'fail', - ItemReason: { Code: 'some_obscure_code ' }, - }, - ], - }, - ], - }, - exception: nil, - timed_out: false, - } - end - - it 'returns the correct result' do - expect(evaluation).to eql( - { - description: 'Instant Verify request failed. 2 checks failed: Check2, CheckWithCode (some_obscure_code )', # rubocop:disable Layout/LineLength - type: :instant_verify_error, - }, - ) - end - end -end diff --git a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb index 51d3608784f..16cf8f0af3a 100644 --- a/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb +++ b/spec/services/doc_auth/lexis_nexis/responses/true_id_response_spec.rb @@ -251,14 +251,12 @@ end context 'when doc_auth_read_additional_pii_attributes_enabled is enabled' do - before do - allow(IdentityConfig.store).to receive(:doc_auth_read_additional_pii_attributes_enabled) - .and_return(true) - end - let(:success_response_body) { LexisNexisFixtures.true_id_response_success } it 'reads the additional PII attributes' do + allow(IdentityConfig.store).to receive(:doc_auth_read_additional_pii_attributes_enabled) + .and_return(true) + pii_from_doc = response.pii_from_doc expect(pii_from_doc.first_name).to eq('LICENSE') @@ -266,17 +264,6 @@ expect(pii_from_doc.sex).to eq('male') expect(pii_from_doc.height).to eq(68) end - - context 'when the height has a space in it' do - # This fixture has the height returns as "5' 9\"" - let(:success_response_body) { LexisNexisFixtures.true_id_response_success_3 } - - it 'reads parses the height correctly' do - pii_from_doc = response.pii_from_doc - - expect(pii_from_doc.height).to eq(69) - end - end end end diff --git a/spec/services/encrypted_doc_storage/doc_writer_spec.rb b/spec/services/encrypted_doc_storage/doc_writer_spec.rb deleted file mode 100644 index d413f8f4704..00000000000 --- a/spec/services/encrypted_doc_storage/doc_writer_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -require 'rails_helper' - -RSpec.describe EncryptedDocStorage::DocWriter do - describe '#write' do - let(:img_path) { Rails.root.join('app', 'assets', 'images', 'logo.svg') } - let(:image) { File.read(img_path) } - - subject do - EncryptedDocStorage::DocWriter.new - end - - it 'encrypts the document and writes it to storage' do - result = subject.write(image:) - - key = Base64.strict_decode64(result.encryption_key) - aes_cipher = Encryption::AesCipherV2.new - - written_image = aes_cipher.decrypt( - File.read(file_path(result.name)), - key, - ) - - # cleanup - File.delete(file_path(result.name)) - - expect(written_image).to eq(image) - end - - it 'uses LocalStorage by default' do - expect_any_instance_of(EncryptedDocStorage::LocalStorage).to receive(:write_image).once - expect_any_instance_of(EncryptedDocStorage::S3Storage).to_not receive(:write_image) - - subject.write(image:) - end - - context 'when S3Storage is passed in' do - it 'uses S3' do - expect_any_instance_of(EncryptedDocStorage::S3Storage).to receive(:write_image).once - expect_any_instance_of(EncryptedDocStorage::LocalStorage).not_to receive(:write_image) - - subject.write( - image:, - data_store: EncryptedDocStorage::S3Storage, - ) - end - end - - def file_path(uuid) - Rails.root.join('tmp', 'encrypted_doc_storage', uuid) - end - end -end diff --git a/spec/services/encrypted_doc_storage/local_storage_spec.rb b/spec/services/encrypted_doc_storage/local_storage_spec.rb deleted file mode 100644 index fc476df3378..00000000000 --- a/spec/services/encrypted_doc_storage/local_storage_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' - -RSpec.describe EncryptedDocStorage::LocalStorage do - let(:img_path) { Rails.root.join('app', 'assets', 'images', 'logo.svg') } - let(:image) { File.read(img_path) } - let(:encrypted_image) do - Encryption::AesCipherV2.new.encrypt(image, SecureRandom.bytes(32)) - end - - describe '#write_image' do - it 'writes the document to the disk' do - name = SecureRandom.uuid - - EncryptedDocStorage::LocalStorage.new.write_image( - encrypted_image:, - name:, - ) - path = Rails.root.join('tmp', 'encrypted_doc_storage', name) - - f = File.new(path, 'rb') - result = f.read - f.close - - # cleanup - File.delete(path) - - expect(result).to eq(encrypted_image) - end - end -end diff --git a/spec/services/encrypted_doc_storage/s3_storage_spec.rb b/spec/services/encrypted_doc_storage/s3_storage_spec.rb deleted file mode 100644 index 9ef5db410fa..00000000000 --- a/spec/services/encrypted_doc_storage/s3_storage_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require 'rails_helper' - -RSpec.describe EncryptedDocStorage::S3Storage do - subject { EncryptedDocStorage::S3Storage.new } - let(:img_path) { Rails.root.join('app', 'assets', 'images', 'logo.svg') } - let(:image) { File.read(img_path) } - let(:encrypted_image) do - Encryption::AesCipherV2.new.encrypt(image, SecureRandom.bytes(32)) - end - - describe '#write_image' do - let(:stubbed_s3_client) { Aws::S3::Client.new(stub_responses: true) } - - before do - allow(subject).to receive(:s3_client).and_return(stubbed_s3_client) - allow(stubbed_s3_client).to receive(:put_object) - end - - it 'writes the document to S3' do - name = '123abc' - - subject.write_image(encrypted_image:, name:) - - expect(stubbed_s3_client).to have_received(:put_object).with( - bucket: IdentityConfig.store.encrypted_document_storage_s3_bucket, - key: name, - body: encrypted_image, - ) - end - end -end diff --git a/spec/services/idv/agent_spec.rb b/spec/services/idv/agent_spec.rb index bead0c38774..f3ebb0f9619 100644 --- a/spec/services/idv/agent_spec.rb +++ b/spec/services/idv/agent_spec.rb @@ -15,25 +15,6 @@ Idp::Constants::MOCK_IDV_APPLICANT_WITH_SSN end let(:document_capture_session) { DocumentCaptureSession.new(result_id: SecureRandom.hex) } - let(:session) { {} } - let(:user_session) { {} } - let(:idv_session) do - Idv::Session.new( - user_session:, - current_user: user, - service_provider: issuer, - ).tap do |idv_session| - idv_session.pii_from_doc = applicant - end - end - let(:proofing_components) do - Idv::ProofingComponents.new( - idv_session:, - session:, - user:, - user_session:, - ) - end subject(:agent) { Idv::Agent.new(applicant) } @@ -54,7 +35,6 @@ threatmetrix_session_id: nil, request_ip: request_ip, ipp_enrollment_in_progress: ipp_enrollment_in_progress, - proofing_components:, ) end @@ -143,18 +123,6 @@ proof_resolution end - it 'passes proofing components to ResolutionProofingJob' do - expect(ResolutionProofingJob).to receive(:perform_later).with( - hash_including( - proofing_components: { - document_check: 'mock', - document_type: 'state_id', - }, - ), - ) - proof_resolution - end - context 'when a proofing timeout occurs' do let(:applicant) do super().merge(first_name: 'Time Exception') diff --git a/spec/services/idv/proofing_components_spec.rb b/spec/services/idv/proofing_components_spec.rb index bdea18c079e..745eb9df054 100644 --- a/spec/services/idv/proofing_components_spec.rb +++ b/spec/services/idv/proofing_components_spec.rb @@ -39,7 +39,6 @@ .and_return(true) idv_session.threatmetrix_review_status = 'pass' idv_session.source_check_vendor = 'aamva' - idv_session.resolution_vendor = 'lexis_nexis' end it 'returns expected result' do @@ -169,6 +168,16 @@ expect(subject.residential_resolution_check).to eql('AReallyGoodVendor') end end + + context 'when resolution done but residential_resolution_vendor nil because of 50/50 state' do + before do + idv_session.mark_verify_info_step_complete! + end + + it 'returns nil to match previous behavior' do + expect(subject.residential_resolution_check).to be(nil) + end + end end describe '#resolution_check' do @@ -186,6 +195,16 @@ expect(subject.resolution_check).to eql('AReallyGoodVendor') end end + + context 'when resolution done but resolution_vendor nil because of 50/50 state' do + before do + idv_session.mark_verify_info_step_complete! + end + + it 'returns LexisNexis to match previous behavior' do + expect(subject.resolution_check).to eql('lexis_nexis') + end + end end describe '#address_check' do diff --git a/spec/services/proofing/aamva/applicant_spec.rb b/spec/services/proofing/aamva/applicant_spec.rb index 011d89e3174..cb53ffeed65 100644 --- a/spec/services/proofing/aamva/applicant_spec.rb +++ b/spec/services/proofing/aamva/applicant_spec.rb @@ -65,22 +65,11 @@ expect(aamva_applicant[:dob]).to eq('') end - context 'when height includes inches >= 10' do - it 'formats as expected' do - proofer_applicant[:height] = 95 - aamva_applicant = Proofing::Aamva::Applicant.from_proofer_applicant(proofer_applicant) - expect(aamva_applicant[:height]).to eq('711') - end - end - - context 'when height includes inches < 10' do - it 'formats as expected' do - proofer_applicant[:height] = 67 - aamva_applicant = Proofing::Aamva::Applicant.from_proofer_applicant(proofer_applicant) + it 'should format the height' do + proofer_applicant[:height] = 73 + aamva_applicant = Proofing::Aamva::Applicant.from_proofer_applicant(proofer_applicant) - # From the DLDV user guide: - # > Height data should be 3 characters (i.e. 5 foot 7 inches is submitted as 507) - expect(aamva_applicant[:height]).to eq('507') - end + # This is intended to describe 6'1" + expect(aamva_applicant[:height]).to eq('61') end end diff --git a/spec/support/shared_examples/sign_in.rb b/spec/support/shared_examples/sign_in.rb index 79bde23f642..013e9ad5172 100644 --- a/spec/support/shared_examples/sign_in.rb +++ b/spec/support/shared_examples/sign_in.rb @@ -118,7 +118,7 @@ end it 'gets bad password error' do - ial2_sign_in_with_piv_cac_gets_sign_in_failure_error(sp) + ial2_sign_in_with_piv_cac_gets_bad_password_error(sp) end end @@ -487,7 +487,7 @@ def no_authn_context_sign_in_with_piv_cac_goes_to_sp(sp) ) end -def ial2_sign_in_with_piv_cac_gets_sign_in_failure_error(sp) +def ial2_sign_in_with_piv_cac_gets_bad_password_error(sp) user = create(:user, :proofed, :with_piv_or_cac) visit_idp_from_sp_with_ial2(sp) diff --git a/yarn.lock b/yarn.lock index fb667e03011..ca61d2a5cc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4081,10 +4081,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -libphonenumber-js@^1.11.17: - version "1.11.17" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.17.tgz#37ddbf16dc4dd45c723a150996c253c58dad034b" - integrity sha512-Jr6v8thd5qRlOlc6CslSTzGzzQW03uiscab7KHQZX1Dfo4R6n6FDhZ0Hri6/X7edLIDv9gl4VMZXhxTjLnl0VQ== +libphonenumber-js@^1.11.4: + version "1.11.4" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.11.4.tgz#e63fe553f45661b30bb10bb8c82c9cf2b22ec32a" + integrity sha512-F/R50HQuWWYcmU/esP5jrH5LiWYaN7DpN0a/99U8+mnGGtnx8kmRE+649dQh3v+CowXXZc8vpkf5AmYkO0AQ7Q== lightningcss-darwin-arm64@1.23.0: version "1.23.0" @@ -4364,7 +4364,7 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.6: +minimist@1.2.6, minimist@^1.2.0, minimist@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== @@ -4444,10 +4444,10 @@ mute-stream@^2.0.0: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== -nanoid@^3.3.7: - version "3.3.8" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" - integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== +nanoid@^3.3.6: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" @@ -4748,7 +4748,7 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== -picocolors@^1.0.0, picocolors@^1.1.0, picocolors@^1.1.1: +picocolors@^1.0.0, picocolors@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -4817,14 +4817,14 @@ postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.33: - version "8.4.49" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.49.tgz#4ea479048ab059ab3ae61d082190fabfd994fe19" - integrity sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA== +postcss@8.4.31, postcss@^8.4.33: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: - nanoid "^3.3.7" - picocolors "^1.1.1" - source-map-js "^1.2.1" + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" prelude-ls@^1.2.1: version "1.2.1" @@ -5436,10 +5436,10 @@ source-list-map@^2.0.1: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -source-map-js@^1.0.1, source-map-js@^1.0.2, source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +source-map-js@^1.0.1, source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== source-map-loader@^4.0.0: version "4.0.0"