diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ed90ce7bfa..31f338017b9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -462,7 +462,7 @@ review-app: stage: review allow_failure: true needs: - - job: build-review-image + - job: build-idp-image resource_group: $CI_ENVIRONMENT_SLUG.reviewapps.identitysandbox.gov extends: .deploy environment: @@ -503,6 +503,7 @@ include: secret_detection: allow_failure: false + needs: [] variables: SECRET_DETECTION_EXCLUDED_PATHS: 'keys.example,config/artifacts.example,public/acuant/*/opencv.min.js,tmp/0.0.0.0-3000.key' SECRET_DETECTION_REPORT_FILE: 'gl-secret-detection-report.json' @@ -527,6 +528,7 @@ secret_detection: # check if '{ "vulnerabilities": [], ..' is empty in the report file if it exists if [ "$(jq ".vulnerabilities | length" $SECRET_DETECTION_REPORT_FILE)" -gt 0 ]; then echo "Vulnerabilities detected. Please analyze the artifact $SECRET_DETECTION_REPORT_FILE produced by the 'secret-detection' job." + echo "Check the \"Security\" tab on the overall pipeline run to download the report for more information." exit 80 fi else diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 90d78f33bf3..e6068396f1c 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -19,6 +19,12 @@ def show user: current_user, locked_for_session: pii_locked_for_session?(current_user), ) + if session.delete(:from_select_email_flow) + flash.now[:success] = t( + 'account.emails.confirmed_html', + url: account_connected_accounts_url, + ) + end end def reauthentication diff --git a/app/controllers/concerns/idv/verify_info_concern.rb b/app/controllers/concerns/idv/verify_info_concern.rb index efceb72bc06..e2608ab854e 100644 --- a/app/controllers/concerns/idv/verify_info_concern.rb +++ b/app/controllers/concerns/idv/verify_info_concern.rb @@ -19,8 +19,6 @@ def shared_update Funnel::DocAuth::RegisterStep.new(current_user.id, sp_session[:issuer]). call('verify', :update, true) - set_state_id_type - ssn_rate_limiter.increment! document_capture_session = DocumentCaptureSession.create( diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb index 5284d5e6881..1eb612bb664 100644 --- a/app/controllers/idv/in_person/verify_info_controller.rb +++ b/app/controllers/idv/in_person/verify_info_controller.rb @@ -57,14 +57,6 @@ def flow_param 'in_person' end - # state_id_type is hard-coded here because it's required for proofing against - # AAMVA. We're sticking with driver's license because most states don't discern - # between various ID types and driver's license is the most common one that will - # be supported. See also LG-3852 and related findings document. - def set_state_id_type - pii_from_user[:state_id_type] = 'drivers_license' unless invalid_state? - end - def invalid_state? pii_from_user.blank? end diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index 9496890cf04..bc11819486b 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -64,9 +64,6 @@ def self.step_info def flow_param; end - # state ID type isn't manually set for Idv::VerifyInfoController - def set_state_id_type; end - def prev_url idv_ssn_url end diff --git a/app/controllers/users/email_confirmations_controller.rb b/app/controllers/users/email_confirmations_controller.rb index fdd01844ad0..7e7e87e9f8b 100644 --- a/app/controllers/users/email_confirmations_controller.rb +++ b/app/controllers/users/email_confirmations_controller.rb @@ -42,6 +42,7 @@ def email_address_already_confirmed? def process_successful_confirmation(email_address) confirm_and_notify(email_address) + store_from_select_email_flow_in_session if current_user flash[:success] = t('devise.confirmations.confirmed') redirect_to account_url @@ -98,5 +99,9 @@ def email_address_already_confirmed_by_current_user? def confirmation_params params.permit(:confirmation_token) end + + def store_from_select_email_flow_in_session + session[:from_select_email_flow] = params[:from_select_email_flow].to_s == 'true' + end end end diff --git a/app/controllers/users/emails_controller.rb b/app/controllers/users/emails_controller.rb index e213740f38c..b52c7625d8c 100644 --- a/app/controllers/users/emails_controller.rb +++ b/app/controllers/users/emails_controller.rb @@ -12,14 +12,19 @@ class EmailsController < ApplicationController def show analytics.add_email_visit + session[:in_select_email_flow] = params[:in_select_email_flow] @add_user_email_form = AddUserEmailForm.new @pending_completions_consent = pending_completions_consent? end def add - @add_user_email_form = AddUserEmailForm.new + @add_user_email_form = AddUserEmailForm.new( + session[:in_select_email_flow], + ) - result = @add_user_email_form.submit(current_user, permitted_params) + result = @add_user_email_form.submit( + current_user, permitted_params + ) analytics.add_email_request(**result.to_h) if result.success? @@ -71,7 +76,8 @@ def verify if session_email.blank? redirect_to add_email_url else - render :verify, locals: { email: session_email } + render :verify, + locals: { email: session_email, in_select_email_flow: params[:in_select_email_flow] } end end @@ -97,7 +103,10 @@ def process_successful_creation resend_confirmation = params[:user][:resend] session[:email] = @add_user_email_form.email - redirect_to add_email_verify_email_url(resend: resend_confirmation) + redirect_to add_email_verify_email_url( + resend: resend_confirmation, + in_select_email_flow: session.delete(:in_select_email_flow), + ) end def session_email diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 95c4149e587..b77e7351c39 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -39,7 +39,7 @@ def create 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? - return process_failed_captcha unless valid_captcha_result? || log_captcha_failures_only? + return process_failed_captcha unless recaptcha_response.success? || log_captcha_failures_only? rate_limit_password_failure = true self.resource = warden.authenticate!(auth_options) @@ -100,11 +100,10 @@ def locked_out_time_remaining distance_of_time_in_words(Time.zone.now, time_lockout_expires, true) end - def valid_captcha_result? - return @valid_captcha_result if defined?(@valid_captcha_result) - @valid_captcha_result = recaptcha_form.submit( + def recaptcha_response + @recaptcha_response ||= recaptcha_form.submit( recaptcha_token: params.require(:user)[:recaptcha_token], - ).success? + ) end def recaptcha_form @@ -206,15 +205,16 @@ def track_authentication_attempt success = current_user.present? && !user_locked_out?(user) && - (valid_captcha_result? || log_captcha_failures_only?) + (recaptcha_response.success? || log_captcha_failures_only?) analytics.email_and_password_auth( + **recaptcha_response.to_h, success: success, user_id: user.uuid, user_locked_out: user_locked_out?(user), rate_limited: rate_limited?, captcha_validation_performed: captcha_validation_performed?, - valid_captcha_result: valid_captcha_result?, + valid_captcha_result: recaptcha_response.success?, bad_password_count: session[:bad_password_count].to_i, sp_request_url_present: sp_session[:request_url].present?, remember_device: remember_device_cookie.present?, diff --git a/app/forms/add_user_email_form.rb b/app/forms/add_user_email_form.rb index 99530bb5bb2..cc91ff62fa2 100644 --- a/app/forms/add_user_email_form.rb +++ b/app/forms/add_user_email_form.rb @@ -5,12 +5,16 @@ class AddUserEmailForm include FormAddEmailValidator include ActionView::Helpers::TranslationHelper - attr_reader :email + attr_reader :email, :in_select_email_flow def self.model_name ActiveModel::Name.new(self, nil, 'User') end + def initialize(in_select_email_flow = nil) + @in_select_email_flow = in_select_email_flow + end + def user @user ||= User.new end @@ -47,7 +51,7 @@ def email_address_record(email) def process_successful_submission @success = true email_address.save! - SendAddEmailConfirmation.new(user).call(email_address) + SendAddEmailConfirmation.new(user).call(email_address, in_select_email_flow) end def extra_analytics_attributes diff --git a/app/javascript/packages/address-search/CHANGELOG.md b/app/javascript/packages/address-search/CHANGELOG.md index 5c682803c51..ee448b8734e 100644 --- a/app/javascript/packages/address-search/CHANGELOG.md +++ b/app/javascript/packages/address-search/CHANGELOG.md @@ -1,5 +1,9 @@ # `Change Log` +## v3.3.0 (2024-11-05) + +- Remove obsolete `is_pilot` field from PostOffice and FormattedLocation types + ## v3.2.0 (2024-10-01) ### New feature diff --git a/app/javascript/packages/address-search/components/in-person-locations.spec.tsx b/app/javascript/packages/address-search/components/in-person-locations.spec.tsx index 89971bc062b..195206b9f81 100644 --- a/app/javascript/packages/address-search/components/in-person-locations.spec.tsx +++ b/app/javascript/packages/address-search/components/in-person-locations.spec.tsx @@ -25,7 +25,6 @@ describe('InPersonLocations', () => { saturdayHours: '9 AM - 6 PM', sundayHours: 'Closed', id: 1, - isPilot: false, }, { name: 'test name', @@ -36,7 +35,6 @@ describe('InPersonLocations', () => { saturdayHours: '10 AM - 5 PM', sundayHours: 'Closed', id: 1, - isPilot: false, }, ]; diff --git a/app/javascript/packages/address-search/components/in-person-locations.tsx b/app/javascript/packages/address-search/components/in-person-locations.tsx index 7b446bb2def..af2f807cbe7 100644 --- a/app/javascript/packages/address-search/components/in-person-locations.tsx +++ b/app/javascript/packages/address-search/components/in-person-locations.tsx @@ -12,7 +12,6 @@ export interface FormattedLocation { streetAddress: string; sundayHours: string; weekdayHours: string; - isPilot: boolean; } function InPersonLocations({ @@ -22,8 +21,6 @@ function InPersonLocations({ noInPersonLocationsDisplay: NoInPersonLocationsDisplay, resultsHeaderComponent: HeaderComponent, }: InPersonLocationsProps) { - const isPilot = locations?.some((l) => l.isPilot); - if (locations?.length === 0) { return ; } @@ -31,11 +28,10 @@ function InPersonLocations({ return ( <>

- {!isPilot && - t('in_person_proofing.body.location.po_search.results_description', { - address, - count: locations?.length, - })} + {t('in_person_proofing.body.location.po_search.results_description', { + address, + count: locations?.length, + })}

{HeaderComponent && } {onSelect &&

{t('in_person_proofing.body.location.po_search.results_instructions')}

} diff --git a/app/javascript/packages/address-search/package.json b/app/javascript/packages/address-search/package.json index 8afaaa3524d..a02eebaee2d 100644 --- a/app/javascript/packages/address-search/package.json +++ b/app/javascript/packages/address-search/package.json @@ -1,6 +1,6 @@ { "name": "@18f/identity-address-search", - "version": "3.2.0", + "version": "3.3.0", "type": "module", "private": false, "files": [ diff --git a/app/javascript/packages/address-search/types.d.ts b/app/javascript/packages/address-search/types.d.ts index 86455ca4f6f..d75c0b971be 100644 --- a/app/javascript/packages/address-search/types.d.ts +++ b/app/javascript/packages/address-search/types.d.ts @@ -10,7 +10,6 @@ interface FormattedLocation { streetAddress: string; sundayHours: string; weekdayHours: string; - isPilot: boolean; } interface PostOffice { @@ -24,7 +23,6 @@ interface PostOffice { weekday_hours: string; zip_code_4: string; zip_code_5: string; - is_pilot: boolean; } interface LocationQuery { diff --git a/app/javascript/packages/address-search/utils.ts b/app/javascript/packages/address-search/utils.ts index 79d895ee6fb..d1f635868c7 100644 --- a/app/javascript/packages/address-search/utils.ts +++ b/app/javascript/packages/address-search/utils.ts @@ -10,7 +10,6 @@ export const formatLocations = (postOffices: PostOffice[]): FormattedLocation[] streetAddress: po.address, sundayHours: po.sunday_hours, weekdayHours: po.weekday_hours, - isPilot: !!po.is_pilot, })); export const snakeCase = (value: string) => diff --git a/app/javascript/packages/analytics/index.ts b/app/javascript/packages/analytics/index.ts index 1e9261ddae5..22fa2d91308 100644 --- a/app/javascript/packages/analytics/index.ts +++ b/app/javascript/packages/analytics/index.ts @@ -24,6 +24,8 @@ export function trackEvent(event: string, payload?: object) { * Logs an error. * * @param error Error object. + * @param event Error event, if error is caught using an `error` event handler. Including this can + * add additional resolution to the logged error, notably the filename where the error occurred. */ export const trackError = ({ name, message, stack }: Error, event?: ErrorEvent) => trackEvent('Frontend Error', { name, message, stack, filename: event?.filename }); diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts index ca980626684..f0bdece5bb7 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.spec.ts @@ -1,12 +1,26 @@ +import quibble from 'quibble'; import type { SinonStub } from 'sinon'; import userEvent from '@testing-library/user-event'; import { screen, waitFor, fireEvent } from '@testing-library/dom'; import { useSandbox, useDefineProperty } from '@18f/identity-test-helpers'; import '@18f/identity-spinner-button/spinner-button-element'; -import './captcha-submit-button-element'; describe('CaptchaSubmitButtonElement', () => { const sandbox = useSandbox(); + const trackError = sandbox.stub(); + + before(async () => { + quibble('@18f/identity-analytics', { trackError }); + await import('./captcha-submit-button-element'); + }); + + afterEach(() => { + trackError.reset(); + }); + + after(() => { + quibble.reset(); + }); context('without ancestor form element', () => { beforeEach(() => { @@ -157,6 +171,35 @@ describe('CaptchaSubmitButtonElement', () => { await waitFor(() => expect(didSubmit).to.be.true()); }); }); + + context('when recaptcha fails to execute', () => { + let error: Error; + + beforeEach(() => { + error = new Error('Invalid site key or not loaded in api.js: badkey'); + ((global as any).grecaptcha.execute as SinonStub).throws(error); + }); + + it('does not prevent default form submission', async () => { + const button = screen.getByRole('button', { name: 'Submit' }); + const form = document.querySelector('form')!; + sandbox.stub(form, 'submit'); + + await userEvent.click(button); + + await expect(form.submit).to.eventually.be.called(); + }); + + it('tracks error', async () => { + const button = screen.getByRole('button', { name: 'Submit' }); + const form = document.querySelector('form')!; + sandbox.stub(form, 'submit'); + + await userEvent.click(button); + + await expect(trackError).to.eventually.be.calledWith(error); + }); + }); }); }); }); diff --git a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts index 3ec45cbb5b0..45f6e33afeb 100644 --- a/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts +++ b/app/javascript/packages/captcha-submit-button/captcha-submit-button-element.ts @@ -1,3 +1,5 @@ +import { trackError } from '@18f/identity-analytics'; + class CaptchaSubmitButtonElement extends HTMLElement { form: HTMLFormElement | null; @@ -46,7 +48,14 @@ class CaptchaSubmitButtonElement extends HTMLElement { invokeChallenge() { this.recaptchaClient!.ready(async () => { const { recaptchaSiteKey: siteKey, recaptchaAction: action } = this; - const token = await this.recaptchaClient!.execute(siteKey!, { action }); + + let token; + try { + token = await this.recaptchaClient!.execute(siteKey!, { action }); + } catch (error) { + trackError(error); + } + this.tokenInput.value = token; this.submit(); }); diff --git a/app/jobs/data_warehouse/daily_sensitive_column_job.rb b/app/jobs/data_warehouse/daily_sensitive_column_job.rb index bdb0bec755e..d8c6d5d1317 100644 --- a/app/jobs/data_warehouse/daily_sensitive_column_job.rb +++ b/app/jobs/data_warehouse/daily_sensitive_column_job.rb @@ -42,7 +42,7 @@ def generate_column_data(columns, table) end def bucket_name - bucket_name = IdentityConfig.store.s3_idp_internal_dw_tasks + bucket_name = IdentityConfig.store.s3_idp_dw_tasks env = Identity::Hostdata.env aws_account_id = Identity::Hostdata.aws_account_id aws_region = Identity::Hostdata.aws_region diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index e92294e73d0..2f46ed4195e 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -199,13 +199,14 @@ def verify_by_mail_letter_requested end end - def add_email(token) + def add_email(token, from_select_email_flow = nil) with_user_locale(user) do presenter = ConfirmationEmailPresenter.new(user, view_context) @first_sentence = presenter.first_sentence @confirmation_period = presenter.confirmation_period @add_email_url = add_email_confirmation_url( confirmation_token: token, + from_select_email_flow:, locale: locale_url_param, ) mail(to: email_address.email, subject: t('user_mailer.add_email.subject')) diff --git a/app/services/analytics_events.rb b/app/services/analytics_events.rb index 21041899d5a..5d6c0b352cd 100644 --- a/app/services/analytics_events.rb +++ b/app/services/analytics_events.rb @@ -436,8 +436,9 @@ def edit_password_visit(required_password_change: false, **extra) ) end - # @param [Boolean] success - # @param [String] user_id + # @param [Boolean] success Whether form validation was successful + # @param [Hash] error_details Details for errors that occurred in unsuccessful submission + # @param [String] user_id UUID for user associated with attempted email address # @param [Boolean] user_locked_out if the user is currently locked out of their second factor # @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 @@ -446,7 +447,7 @@ def edit_password_visit(required_password_change: false, **extra) # @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 - # there is 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:, @@ -459,11 +460,13 @@ def email_and_password_auth( sp_request_url_present:, remember_device:, new_device:, + error_details: nil, **extra ) track_event( 'Email and Password Authentication', success:, + error_details:, user_id:, user_locked_out:, rate_limited:, diff --git a/app/services/proofing/aamva/request/verification_request.rb b/app/services/proofing/aamva/request/verification_request.rb index ecff76af4a4..84e25e75b34 100644 --- a/app/services/proofing/aamva/request/verification_request.rb +++ b/app/services/proofing/aamva/request/verification_request.rb @@ -84,9 +84,36 @@ def add_user_provided_data_to_body inside: '//dldv:verifyDriverLicenseDataRequest', ) + if IdentityConfig.store.aamva_send_id_type + add_state_id_type( + applicant.state_id_data.state_id_type, + document, + ) + end + @body = document.to_s end + def add_state_id_type(id_type, document) + category_code = case id_type + when 'drivers_license' + 1 + when 'drivers_permit' + 2 + when 'state_id_card' + 3 + end + + if category_code + add_optional_element( + 'aa:DocumentCategoryCode', + value: category_code, + document:, + inside: '//dldv:verifyDriverLicenseDataRequest', + ) + end + end + def add_optional_element(name, value:, document:, inside: nil, after: nil) return if value.blank? diff --git a/app/services/proofing/mock/state_id_mock_client.rb b/app/services/proofing/mock/state_id_mock_client.rb index 7f01945df57..3b86f0bff68 100644 --- a/app/services/proofing/mock/state_id_mock_client.rb +++ b/app/services/proofing/mock/state_id_mock_client.rb @@ -72,8 +72,8 @@ def invalid_state_id_number?(state_id_number) end def invalid_state_id_type?(state_id_type) - !SUPPORTED_STATE_ID_TYPES.include?(state_id_type) || - state_id_type.nil? + !SUPPORTED_STATE_ID_TYPES.include?(state_id_type) && + !state_id_type.nil? end end end diff --git a/app/services/send_add_email_confirmation.rb b/app/services/send_add_email_confirmation.rb index cc9eb7dc7d8..fbeb8d900a8 100644 --- a/app/services/send_add_email_confirmation.rb +++ b/app/services/send_add_email_confirmation.rb @@ -7,8 +7,9 @@ def initialize(user) @user = user end - def call(email_address) + def call(email_address, in_select_email_flow = nil) @email_address = email_address + @in_select_email_flow = in_select_email_flow update_email_address_record send_email end @@ -23,7 +24,7 @@ def confirmation_sent_at email_address.confirmation_sent_at end - attr_reader :email_address + attr_reader :email_address, :in_select_email_flow def update_email_address_record email_address.update!( @@ -59,6 +60,7 @@ def send_email_associated_with_another_account_email def send_confirmation_email UserMailer.with(user: user, email_address: email_address).add_email( confirmation_token, + in_select_email_flow, ).deliver_now_or_later end end diff --git a/app/services/usps_in_person_proofing/enrollment_helper.rb b/app/services/usps_in_person_proofing/enrollment_helper.rb index 564f9a7e96b..c6599c3f2e0 100644 --- a/app/services/usps_in_person_proofing/enrollment_helper.rb +++ b/app/services/usps_in_person_proofing/enrollment_helper.rb @@ -111,7 +111,6 @@ def localized_location(location) weekday_hours: EnrollmentHelper.localized_hours(location.weekday_hours), zip_code_4: location.zip_code_4, zip_code_5: location.zip_code_5, - is_pilot: location.is_pilot, } end diff --git a/app/services/usps_in_person_proofing/post_office.rb b/app/services/usps_in_person_proofing/post_office.rb index f2350621978..54def242031 100644 --- a/app/services/usps_in_person_proofing/post_office.rb +++ b/app/services/usps_in_person_proofing/post_office.rb @@ -12,7 +12,6 @@ module UspsInPersonProofing :weekday_hours, :zip_code_4, :zip_code_5, - :is_pilot, keyword_init: true, ) end diff --git a/app/services/usps_in_person_proofing/proofer.rb b/app/services/usps_in_person_proofing/proofer.rb index f94ad4a1040..9d24bc856c5 100644 --- a/app/services/usps_in_person_proofing/proofer.rb +++ b/app/services/usps_in_person_proofing/proofer.rb @@ -204,7 +204,6 @@ def parse_facilities(facilities) weekday_hours: hours['weekdayHours'], zip_code_4: post_office['zip4'], zip_code_5: post_office['zip5'], - is_pilot: post_office['isPilot'], ) end end diff --git a/app/views/accounts/connected_accounts/selected_email/edit.html.erb b/app/views/accounts/connected_accounts/selected_email/edit.html.erb index f513ad8ddd7..f2a57f126e5 100644 --- a/app/views/accounts/connected_accounts/selected_email/edit.html.erb +++ b/app/views/accounts/connected_accounts/selected_email/edit.html.erb @@ -34,7 +34,7 @@ <% end %> <%= render ButtonComponent.new( - url: add_email_path, + url: add_email_path(in_select_email_flow: true), outline: true, big: true, wide: true, diff --git a/app/views/report_mailer/tables_report.html.erb b/app/views/report_mailer/tables_report.html.erb index f102bfea68c..403db2ce5fa 100644 --- a/app/views/report_mailer/tables_report.html.erb +++ b/app/views/report_mailer/tables_report.html.erb @@ -10,7 +10,7 @@ height: 21, ) %> -
+
<%= @message %>
<% @reports.each do |report| %> <% header, *rows = report.table %> @@ -47,4 +47,4 @@ <% end %> -
+
diff --git a/app/views/sign_up/completions/show.html.erb b/app/views/sign_up/completions/show.html.erb index e019e17a33d..b40f9ce2a3c 100644 --- a/app/views/sign_up/completions/show.html.erb +++ b/app/views/sign_up/completions/show.html.erb @@ -49,7 +49,7 @@ <% if @presenter.multiple_emails? %> <%= link_to t('help_text.requested_attributes.change_email_link'), sign_up_select_email_path %> <% else %> - <%= link_to t('account.index.email_add'), add_email_path %> + <%= link_to t('account.index.email_add'), add_email_path(in_select_email_flow: true) %> <% end %>

diff --git a/app/views/sign_up/select_email/show.html.erb b/app/views/sign_up/select_email/show.html.erb index 0403c5d2653..53b0c04e5de 100644 --- a/app/views/sign_up/select_email/show.html.erb +++ b/app/views/sign_up/select_email/show.html.erb @@ -30,7 +30,7 @@ <% end %> <%= render ButtonComponent.new( - url: add_email_path, + url: add_email_path(in_select_email_flow: true), outline: true, big: true, wide: true, diff --git a/app/views/users/emails/verify.html.erb b/app/views/users/emails/verify.html.erb index 471f93d0a34..211b59fc6ee 100644 --- a/app/views/users/emails/verify.html.erb +++ b/app/views/users/emails/verify.html.erb @@ -22,7 +22,7 @@ <%= t('notices.signed_up_and_confirmed.no_email_sent_explanation_start') %> <%= button_to(add_email_resend_path, method: :post, class: 'usa-button usa-button--unstyled', form_class: 'display-inline-block padding-left-1') { t('links.resend') } %> -

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

+

<%= t('notices.use_diff_email.text_html', link_html: link_to(t('notices.use_diff_email.link'), add_email_path(in_select_email_flow: in_select_email_flow))) %>

<%= t('devise.registrations.close_window') %>

<% if FeatureManagement.enable_load_testing_mode? && EmailAddress.find_with_email(email) %> diff --git a/config/application.yml.default b/config/application.yml.default index 4bdf5c9b6dc..8a39f3775fd 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -19,6 +19,7 @@ aamva_auth_url: 'https://example.org:12345/auth/url' aamva_cert_enabled: true aamva_private_key: '' aamva_public_key: '' +aamva_send_id_type: true aamva_supported_jurisdictions: '["AL","AR","AZ","CO","CT","DC","DE","FL","GA","HI","IA","ID","IL","IN","KS","KY","MA","MD","ME","MI","MO","MS","MT","NC","ND","NE","NJ","NM","NV","OH","OR","PA","RI","SC","SD","TN","TX","VA","VT","WA","WI","WV","WY"]' aamva_verification_request_timeout: 5.0 aamva_verification_url: https://example.org:12345/verification/url @@ -343,7 +344,7 @@ ruby_workers_idv_enabled: true rules_of_use_horizon_years: 5 rules_of_use_updated_at: '2022-01-19T00:00:00Z' # Production has a newer timestamp than this, update directly in S3 s3_data_warehouse_bucket_prefix: 'login-gov-analytics-export' -s3_idp_internal_dw_tasks: 'login-gov-idp-internal-dw-tasks' +s3_idp_dw_tasks: 'login-gov-idp-dw-tasks' s3_public_reports_enabled: false s3_report_bucket_prefix: login-gov.reports s3_report_public_bucket_prefix: login-gov-pubdata @@ -501,6 +502,7 @@ development: # production: aamva_auth_url: 'https://authentication-cert.aamva.org/Authentication/Authenticate.svc' + aamva_send_id_type: false aamva_verification_url: 'https://verificationservices-cert.aamva.org:18449/dldv/2.1/online' available_locales: 'en,es,fr' biometric_ial_enabled: false diff --git a/config/locales/en.yml b/config/locales/en.yml index def5a8cfd80..c9377403782 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -51,6 +51,7 @@ account.email_language.name.es: Español account.email_language.name.fr: Français account.email_language.name.zh: 中文 (简体) account.email_language.updated: Your email language preference has been updated. +account.emails.confirmed_html: You have confirmed your email address. Go to your connected accounts to update the email you share with connected agencies. account.forget_all_browsers.longer_description: Once you choose to ‘forget all browsers,’ we’ll need additional information to know that it’s actually you signing in to your account. We’ll ask for a multi-factor authentication method (such as text/SMS code or a security key) each time you want to access your account. account.index.auth_app_add: Add app account.index.auth_app_disabled: not enabled diff --git a/config/locales/es.yml b/config/locales/es.yml index a3d073ec453..f27cbad7794 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -51,6 +51,7 @@ account.email_language.name.es: Español account.email_language.name.fr: Français account.email_language.name.zh: 中文 (简体) account.email_language.updated: Se actualizó su preferencia de idioma del correo electrónico. +account.emails.confirmed_html: Usted confirmó su dirección de correo electrónico. Vaya a Sus cuentas conectadas para actualizar el correo electrónico que proporcionó a las agencias conectadas. account.forget_all_browsers.longer_description: Una vez que elija “Olvidar todos los navegadores”, necesitaremos más información para saber que realmente es usted quien está iniciando sesión en su cuenta. Le pediremos un método de autenticación multifactor (como código de texto o de SMS, o una clave de seguridad) cada vez que desee acceder a su cuenta. account.index.auth_app_add: Agregar aplicación account.index.auth_app_disabled: no habilitada diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 15199f121a5..2a41643f85c 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -51,6 +51,7 @@ account.email_language.name.es: Español account.email_language.name.fr: Français account.email_language.name.zh: 中文 (简体) account.email_language.updated: Votre langue de préférence pour les e-mails a été mise à jour. +account.emails.confirmed_html: Vous avez confirmé votre adresse e-mail. Rendez-vous sur vos comptes connectés pour actualiser l’adresse e-mail que vous communiquez aux organismes connectés. account.forget_all_browsers.longer_description: Une fois que vous aurez choisi d’« oublier tous les navigateurs », nous aurons besoin d’informations supplémentaires pour savoir que c’est bien vous qui vous connectez à votre compte. Nous vous demanderons une méthode d’authentification multi-facteurs (comme un code SMS/texto ou une clé de sécurité) chaque fois que vous souhaiterez accéder à votre compte. account.index.auth_app_add: Ajouter une appli account.index.auth_app_disabled: non activé diff --git a/config/locales/zh.yml b/config/locales/zh.yml index 7d6260e8ef1..e40a3f9067c 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -51,6 +51,7 @@ account.email_language.name.es: Español account.email_language.name.fr: Français account.email_language.name.zh: 中文 (简体) account.email_language.updated: 你的电邮语言选择已更新。 +account.emails.confirmed_html: 你已确认了你的电邮地址。请到你已连接的账户来更新你与已连接机构所分享的电邮。 account.forget_all_browsers.longer_description: 你选择“忘掉所有浏览器”后,我们将需要额外信息来知道的确是你在登录你自己的账户。每次你要访问自己的账户时,我们都会向你要一个多因素身份证实方法(比如短信/SMS 代码或安全密钥) account.index.auth_app_add: 添加应用程序 account.index.auth_app_disabled: 未启用 diff --git a/dockerfiles/application.yaml b/dockerfiles/application.yaml index 58d4707d8fa..9803a90beb8 100644 --- a/dockerfiles/application.yaml +++ b/dockerfiles/application.yaml @@ -396,7 +396,7 @@ spec: patch: |- - op: replace path: /spec/template/spec/containers/0/image - value: {{ECR_REGISTRY}}/identity-idp/review:{{IDP_CONTAINER_TAG}} + value: {{ECR_REGISTRY}}/identity-idp/idp:{{IDP_CONTAINER_TAG}} - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always @@ -406,7 +406,7 @@ spec: patch: |- - op: replace path: /spec/template/spec/containers/0/image - value: {{ECR_REGISTRY}}/identity-idp/review:{{IDP_CONTAINER_TAG}} + value: {{ECR_REGISTRY}}/identity-idp/idp:{{IDP_CONTAINER_TAG}} - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always @@ -416,7 +416,7 @@ spec: patch: |- - op: replace path: /spec/template/spec/containers/0/image - value: {{ECR_REGISTRY}}/identity-idp/review:{{IDP_CONTAINER_TAG}} + value: {{ECR_REGISTRY}}/identity-idp/idp:{{IDP_CONTAINER_TAG}} - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always @@ -489,7 +489,7 @@ spec: patch: |- - op: replace path: /spec/template/spec/containers/0/image - value: {{ECR_REGISTRY}}/identity-idp/review:{{IDP_CONTAINER_TAG}} + value: {{ECR_REGISTRY}}/identity-idp/idp:{{IDP_CONTAINER_TAG}} - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always @@ -500,7 +500,7 @@ spec: patch: |- - op: replace path: /spec/template/spec/containers/0/image - value: {{ECR_REGISTRY}}/identity-idp/review:{{IDP_CONTAINER_TAG}} + value: {{ECR_REGISTRY}}/identity-idp/idp:{{IDP_CONTAINER_TAG}} - op: replace path: /spec/template/spec/containers/0/imagePullPolicy value: Always diff --git a/lib/identity_config.rb b/lib/identity_config.rb index 88003036bb8..dbdc4874fa7 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -34,6 +34,7 @@ def self.store config.add(:aamva_cert_enabled, type: :boolean) config.add(:aamva_private_key, type: :string) config.add(:aamva_public_key, type: :string) + config.add(:aamva_send_id_type, type: :boolean) config.add(:aamva_supported_jurisdictions, type: :json) config.add(:aamva_verification_request_timeout, type: :float) config.add(:aamva_verification_url) @@ -372,7 +373,7 @@ def self.store config.add(:s3_report_bucket_prefix, type: :string) config.add(:s3_report_public_bucket_prefix, type: :string) config.add(:s3_data_warehouse_bucket_prefix, type: :string) - config.add(:s3_idp_internal_dw_tasks, type: :string) + config.add(:s3_idp_dw_tasks, type: :string) config.add(:s3_reports_enabled, type: :boolean) config.add(:saml_endpoint_configs, type: :json, options: { symbolize_names: true }) config.add(:saml_secret_rotation_enabled, type: :boolean) diff --git a/spec/controllers/accounts_controller_spec.rb b/spec/controllers/accounts_controller_spec.rb index 52818437548..0bae12eabd0 100644 --- a/spec/controllers/accounts_controller_spec.rb +++ b/spec/controllers/accounts_controller_spec.rb @@ -80,6 +80,42 @@ end end + context 'when user just added new email through select email flow' do + context 'when user is in select email form flow' do + before do + session[:from_select_email_flow] = true + end + it 'renders the proper flash message' do + flash_message = t( + 'account.emails.confirmed_html', + url: account_connected_accounts_url, + ) + user = create(:user, :fully_registered) + sign_in user + + get :show + + expect(response).to_not be_redirect + expect(flash[:success]).to eq(flash_message) + expect(session[:from_select_email_flow]).to be_nil + end + end + + context 'when user is not in email form flow' do + before do + session[:from_select_email_flow] = false + end + it 'renders proper flash message' do + t('devise.confirmations.confirmed') + user = create(:user, :fully_registered) + sign_in user + + get :show + expect(flash[:success]).to be_nil + end + end + end + context 'when a profile has been deactivated by password reset' do it 'renders the profile and shows a deactivation banner' do user = create( 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 4a6dbda956b..a05953e8b96 100644 --- a/spec/controllers/idv/in_person/verify_info_controller_spec.rb +++ b/spec/controllers/idv/in_person/verify_info_controller_spec.rb @@ -234,22 +234,15 @@ allow(user).to receive(:establishing_in_person_enrollment).and_return(enrollment) end - it 'sets ssn and state_id_type on pii_from_user' do + it 'sets ssn on pii_from_user' do expect(Idv::Agent).to receive(:new).with( hash_including( - state_id_type: 'drivers_license', ssn: Idp::Constants::MOCK_IDV_APPLICANT_SAME_ADDRESS_AS_ID[:ssn], consent_given_at: subject.idv_session.idv_consent_given_at, ), ).and_call_original - # our test data already has the expected value by default - subject.user_session['idv/in_person'][:pii_from_user].delete(:state_id_type) - put :update - - expect(subject.user_session['idv/in_person'][:pii_from_user][:state_id_type]). - to eq 'drivers_license' end context 'a user does not have an establishing in person enrollment associated with them' do diff --git a/spec/controllers/users/email_confirmations_controller_spec.rb b/spec/controllers/users/email_confirmations_controller_spec.rb index 8c984d65584..c6a7613e381 100644 --- a/spec/controllers/users/email_confirmations_controller_spec.rb +++ b/spec/controllers/users/email_confirmations_controller_spec.rb @@ -25,6 +25,26 @@ get :create, params: { confirmation_token: email_record.reload.confirmation_token } end + context 'when select email feature is disabled' do + before do + allow(IdentityConfig.store).to receive(:feature_select_email_to_share_enabled). + and_return(false) + end + it 'should render proper flash member' do + flash_message = t('devise.confirmations.confirmed') + user = create(:user) + sign_in user + new_email = Faker::Internet.email + + add_email_form = AddUserEmailForm.new + add_email_form.submit(user, email: new_email) + email_record = add_email_form.email_address_record(new_email) + + get :create, params: { confirmation_token: email_record.reload.confirmation_token } + expect(flash[:success]).to eq(flash_message) + end + end + it 'rejects an otherwise valid token for unconfirmed users' do user = create(:user, :unconfirmed, email_addresses: []) new_email = Faker::Internet.email diff --git a/spec/controllers/users/sessions_controller_spec.rb b/spec/controllers/users/sessions_controller_spec.rb index 6a535885546..c0a3f57cf76 100644 --- a/spec/controllers/users/sessions_controller_spec.rb +++ b/spec/controllers/users/sessions_controller_spec.rb @@ -372,6 +372,7 @@ expect(@analytics).to have_logged_event( 'Email and Password Authentication', success: false, + error_details: { recaptcha_token: { blank: true } }, user_id: user.uuid, user_locked_out: false, rate_limited: false, diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index 7505f8b7717..606eb9373ec 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -153,7 +153,7 @@ vendor_name: 'ResolutionMock', vendor_workflow: nil, verified_attributes: nil }, - state_id: state_id_resolution_with_id_type, + state_id: state_id_resolution, threatmetrix: threatmetrix_response, }, }, diff --git a/spec/fixtures/proofing/aamva/requests/verification_request.xml b/spec/fixtures/proofing/aamva/requests/verification_request.xml index 32d5a25010f..c42882043d4 100644 --- a/spec/fixtures/proofing/aamva/requests/verification_request.xml +++ b/spec/fixtures/proofing/aamva/requests/verification_request.xml @@ -31,7 +31,7 @@ VA 20176 - + 1 - \ No newline at end of file + diff --git a/spec/jobs/data_warehouse/daily_sensitive_column_report_spec.rb b/spec/jobs/data_warehouse/daily_sensitive_column_report_spec.rb index b671355600a..5c81de89a0b 100644 --- a/spec/jobs/data_warehouse/daily_sensitive_column_report_spec.rb +++ b/spec/jobs/data_warehouse/daily_sensitive_column_report_spec.rb @@ -5,9 +5,9 @@ let(:timestamp) { Date.new(2024, 10, 15).in_time_zone('UTC') } let(:job) { described_class.new } - let(:expected_bucket) { 'login-gov-ipd-internal-dw-tasks-test-1234-us-west-2' } + let(:expected_bucket) { 'login-gov-ipd-dw-tasks-test-1234-us-west-2' } let(:tables) { ['auth_app_configurations'] } - let(:s3_idp_internal_dw_tasks) { 'login-gov-idp-internal-dw-tasks' } + let(:s3_idp_dw_tasks) { 'login-gov-idp-dw-tasks' } let(:expected_json) do { @@ -58,7 +58,7 @@ { body: anything, content_type: 'application/json', - bucket: 'login-gov-idp-internal-dw-tasks-int-1234-us-west-1', + bucket: 'login-gov-idp-dw-tasks-int-1234-us-west-1', } end @@ -66,8 +66,8 @@ allow(Identity::Hostdata).to receive(:env).and_return('int') allow(Identity::Hostdata).to receive(:aws_account_id).and_return('1234') allow(Identity::Hostdata).to receive(:aws_region).and_return('us-west-1') - allow(IdentityConfig.store).to receive(:s3_idp_internal_dw_tasks). - and_return(s3_idp_internal_dw_tasks) + allow(IdentityConfig.store).to receive(:s3_idp_dw_tasks). + and_return(s3_idp_dw_tasks) Aws.config[:s3] = { stub_responses: { diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 7f4b80dd228..71f615af5fe 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -43,6 +43,22 @@ expect(mail.html_part.body).to have_content(add_email_url) expect(mail.html_part.body).to_not have_content(sign_up_create_email_confirmation_url) end + + context 'when user adds email from select email flow' do + let(:token) { SecureRandom.hex } + let(:mail) do + UserMailer.with(user: user, email_address: email_address).add_email(token, true) + end + + it 'renders the add_email_confirmation_url' do + add_email_url = add_email_confirmation_url( + confirmation_token: token, + from_select_email_flow: true, + ) + + expect(mail.html_part.body).to have_content(add_email_url) + end + end end describe '#email_deleted' do diff --git a/spec/services/proofing/aamva/applicant_spec.rb b/spec/services/proofing/aamva/applicant_spec.rb index bb22b20d376..b2172ce9bd5 100644 --- a/spec/services/proofing/aamva/applicant_spec.rb +++ b/spec/services/proofing/aamva/applicant_spec.rb @@ -14,7 +14,7 @@ end describe '.from_proofer_applicant(applicant)' do - it 'should create an AAMVA applicant with necessary proofer applcant data' do + it 'should create an AAMVA applicant with necessary proofer applicant data' do aamva_applicant = described_class.from_proofer_applicant(proofer_applicant) expect(aamva_applicant.uuid).to eq(proofer_applicant[:uuid]) diff --git a/spec/services/proofing/aamva/request/verification_request_spec.rb b/spec/services/proofing/aamva/request/verification_request_spec.rb index 190d9edf374..68c2a874d83 100644 --- a/spec/services/proofing/aamva/request/verification_request_spec.rb +++ b/spec/services/proofing/aamva/request/verification_request_spec.rb @@ -33,7 +33,7 @@ describe '#body' do it 'should be a request body' do - expect(subject.body).to eq(AamvaFixtures.verification_request) + expect(subject.body + "\n").to eq(AamvaFixtures.verification_request) end it 'should escape XML in applicant data' do @@ -88,6 +88,64 @@ '2030-01-02', ) end + + context '#state_id_type' do + context 'when the feature flag is off' do + before do + expect(IdentityConfig.store).to receive(:aamva_send_id_type).and_return(false) + end + + it 'does not add a DocumentCategoryCode' do + applicant.state_id_data.state_id_type = 'drivers_license' + expect(subject.body).to_not include('') + end + end + + context 'when the feature flag is on' do + before do + expect(IdentityConfig.store).to receive(:aamva_send_id_type).and_return(true) + end + + context 'when the type is a Drivers License' do + it 'includes DocumentCategoryCode=1' do + applicant.state_id_data.state_id_type = 'drivers_license' + expect(subject.body).to include( + '1', + ) + end + end + + context 'when the type is a learners permit' do + it 'includes DocumentCategoryCode=2' do + applicant.state_id_data.state_id_type = 'drivers_permit' + expect(subject.body).to include( + '2', + ) + end + end + + context 'when the type is an ID Card' do + it 'includes DocumentCategoryCode=3' do + applicant.state_id_data.state_id_type = 'state_id_card' + expect(subject.body).to include( + '3', + ) + end + end + + context 'when the type is something invalid' do + it 'does not add a DocumentCategoryCode for nil ID type' do + applicant.state_id_data.state_id_type = nil + expect(subject.body).to_not include('') + end + + it 'does not add a DocumentCategoryCode for invalid ID types' do + applicant.state_id_data.state_id_type = 'License to Keep an Alpaca' + expect(subject.body).to_not include('') + end + end + end + end end describe '#headers' do diff --git a/spec/views/users/emails/verify.html.erb_spec.rb b/spec/views/users/emails/verify.html.erb_spec.rb index 5c74dd8b7ed..f90ae443004 100644 --- a/spec/views/users/emails/verify.html.erb_spec.rb +++ b/spec/views/users/emails/verify.html.erb_spec.rb @@ -4,6 +4,7 @@ let(:email) { 'foo@bar.com' } before do allow(view).to receive(:email).and_return(email) + allow(view).to receive(:in_select_email_flow).and_return(nil) @resend_email_confirmation_form = ResendEmailConfirmationForm.new end