diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 84abbaf1ba0..b8ed7a69236 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -532,7 +532,7 @@ stop-review-app: deploy_production: stage: deploy_production - allow_failure: false + allow_failure: true needs: - job: build-review-image resource_group: $CI_ENVIRONMENT_SLUG.review-app.identitysandbox.gov diff --git a/app/assets/images/sp-logos/gsa.png b/app/assets/images/sp-logos/gsa.png new file mode 100644 index 00000000000..55ce511d9ee Binary files /dev/null and b/app/assets/images/sp-logos/gsa.png differ diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 67679a40869..23e80e0b8b6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -451,7 +451,7 @@ def render_timeout(exception) end def render_full_width(template, **opts) - render template, **opts, layout: 'base' + render template, **opts, layout: 'application' end def analytics_exception_info(exception) diff --git a/app/controllers/concerns/fraud_review_concern.rb b/app/controllers/concerns/fraud_review_concern.rb index cdc93641f26..1f98fd1eda5 100644 --- a/app/controllers/concerns/fraud_review_concern.rb +++ b/app/controllers/concerns/fraud_review_concern.rb @@ -4,14 +4,19 @@ module FraudReviewConcern delegate :fraud_check_failed?, :fraud_review_pending?, :fraud_rejection?, + :ipp_fraud_review_pending?, to: :fraud_review_checker def handle_fraud + in_person_handle_pending_fraud_review handle_pending_fraud_review handle_fraud_rejection end def handle_pending_fraud_review + # If the user has not passed IPP at a post office, allow them to + # complete another enrollment by not redirecting to please call + return if in_person_can_perform_fraud_review? redirect_to_fraud_review if fraud_review_pending? end @@ -19,6 +24,19 @@ def handle_fraud_rejection redirect_to_fraud_rejection if fraud_rejection? end + def in_person_handle_pending_fraud_review + return unless in_person_can_perform_fraud_review? + if fraud_review_pending? && current_user.in_person_enrollment_status == 'passed' + redirect_to_fraud_review + end + end + + def in_person_can_perform_fraud_review? + IdentityConfig.store.in_person_proofing_enforce_tmx && + current_user.in_person_enrollment_status != 'canceled' && + !current_user.in_person_enrollment_status.nil? + end + def redirect_to_fraud_review redirect_to idv_please_call_url end diff --git a/app/controllers/idv/how_to_verify_controller.rb b/app/controllers/idv/how_to_verify_controller.rb index f9c14d7a9f0..2ab4cef383e 100644 --- a/app/controllers/idv/how_to_verify_controller.rb +++ b/app/controllers/idv/how_to_verify_controller.rb @@ -65,7 +65,10 @@ def self.step_info idv_session.idv_consent_given && idv_session.service_provider&.in_person_proofing_enabled end, - undo_step: ->(idv_session:, user:) { idv_session.skip_doc_auth = nil }, + undo_step: ->(idv_session:, user:) { + idv_session.skip_doc_auth = nil + idv_session.opted_in_to_in_person_proofing = nil + }, ) end diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index b1a350e869a..61a54ec39ff 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -33,12 +33,25 @@ def update end end + def self.selected_remote(idv_session:) + if IdentityConfig.store.in_person_proofing_opt_in_enabled && + IdentityConfig.store.in_person_proofing_enabled && + idv_session.service_provider&.in_person_proofing_enabled + idv_session.skip_doc_auth == false + else + idv_session.skip_doc_auth.nil? || idv_session.skip_doc_auth == false + end + end + def self.step_info Idv::StepInfo.new( key: :hybrid_handoff, controller: self, next_steps: [:link_sent, :document_capture], - preconditions: ->(idv_session:, user:) { idv_session.idv_consent_given }, + preconditions: ->(idv_session:, user:) { + idv_session.idv_consent_given && + self.selected_remote(idv_session: idv_session) + }, undo_step: ->(idv_session:, user:) do idv_session.flow_path = nil idv_session.phone_for_mobile_flow = nil diff --git a/app/controllers/idv/in_person/ready_to_verify_controller.rb b/app/controllers/idv/in_person/ready_to_verify_controller.rb index e2b83b121f1..26f87f5f2de 100644 --- a/app/controllers/idv/in_person/ready_to_verify_controller.rb +++ b/app/controllers/idv/in_person/ready_to_verify_controller.rb @@ -6,9 +6,11 @@ class ReadyToVerifyController < ApplicationController include RenderConditionConcern include StepIndicatorConcern include OptInHelper + include FraudReviewConcern check_or_render_not_found -> { IdentityConfig.store.in_person_proofing_enabled } + before_action :handle_fraud before_action :confirm_two_factor_authenticated before_action :confirm_in_person_session diff --git a/app/controllers/idv/please_call_controller.rb b/app/controllers/idv/please_call_controller.rb index 677a384e0fe..cb39173da69 100644 --- a/app/controllers/idv/please_call_controller.rb +++ b/app/controllers/idv/please_call_controller.rb @@ -13,6 +13,12 @@ def show analytics.idv_please_call_visited pending_at = current_user.fraud_review_pending_profile.fraud_review_pending_at @call_by_date = pending_at + FRAUD_REVIEW_CONTACT_WITHIN_DAYS + @in_person = ipp_enabled_and_enrollment_passed? + end + + def ipp_enabled_and_enrollment_passed? + return unless in_person_tmx_enabled? + in_person_proofing_enabled? && ipp_enrollment_passed? end private @@ -22,5 +28,18 @@ def confirm_fraud_pending redirect_to account_url end end + + def in_person_proofing_enabled? + IdentityConfig.store.in_person_proofing_enabled + end + + def in_person_tmx_enabled? + IdentityConfig.store.in_person_proofing_enforce_tmx + end + + # we only want to handle enrollments that have passed + def ipp_enrollment_passed? + current_user&.in_person_enrollment_status == 'passed' + end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 68eecf7f08d..dea8c2f9244 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -20,7 +20,6 @@ class SessionsController < Devise::SessionsController def new override_csp_for_google_analytics - @ial = sp_session_ial @issuer_forced_reauthentication = issuer_forced_reauthentication?( issuer: decorated_sp_session.sp_issuer, ) @@ -76,10 +75,9 @@ def increment_session_bad_password_count end def process_locked_out_session - irs_attempts_api_tracker.login_rate_limited( - email: auth_params[:email], - ) - + irs_attempts_api_tracker.login_rate_limited(email: auth_params[:email]) + warden.logout(:user) + warden.lock! flash[:error] = t('errors.sign_in.bad_password_limit') redirect_to root_url end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7b0eb6c382a..c6decbc77da 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,8 +22,12 @@ def title end end - def background_cls(cls) - content_for(:background_cls) { cls } + def extends_layout(layout, **locals, &block) + if block.present? + @view_flow.get(:layout).replace capture(&block) # rubocop:disable Rails/HelperInstanceVariable + end + + render template: "layouts/#{layout}", locals: end def sp_session diff --git a/app/javascript/packages/document-capture/context/file-base64-cache.js b/app/javascript/packages/document-capture/context/file-base64-cache.ts similarity index 59% rename from app/javascript/packages/document-capture/context/file-base64-cache.js rename to app/javascript/packages/document-capture/context/file-base64-cache.ts index 8ad95a50d03..366b141a6bf 100644 --- a/app/javascript/packages/document-capture/context/file-base64-cache.js +++ b/app/javascript/packages/document-capture/context/file-base64-cache.ts @@ -1,6 +1,6 @@ import { createContext } from 'react'; -const FileBase64CacheContext = createContext(/** @type {WeakMap} */ (new WeakMap())); +const FileBase64CacheContext = createContext(new WeakMap()); FileBase64CacheContext.displayName = 'FileBase64CacheContext'; diff --git a/app/models/user.rb b/app/models/user.rb index a92c017fefb..77a7d18dedd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -206,6 +206,13 @@ def in_person_pending_profile pending_profile if pending_profile&.in_person_verification_pending? end + ## + # Return the status of the current In Person Proofing Enrollment + # @return [String] enrollment status + def in_person_enrollment_status + pending_profile&.in_person_enrollment&.status + end + def has_in_person_enrollment? pending_in_person_enrollment.present? || establishing_in_person_enrollment.present? end diff --git a/app/presenters/image_upload_response_presenter.rb b/app/presenters/image_upload_response_presenter.rb index 6395f2ea889..e6476312323 100644 --- a/app/presenters/image_upload_response_presenter.rb +++ b/app/presenters/image_upload_response_presenter.rb @@ -51,8 +51,9 @@ def as_json(*) json[:ocr_pii] = ocr_pii json[:result_failed] = doc_auth_result_failed? json[:doc_type_supported] = doc_type_supported? - json[:selfie_live] = selfie_live? if show_selfie_failures - json[:selfie_quality_good] = selfie_quality_good? if show_selfie_failures + json[:selfie_status] = selfie_status if show_selfie_failures? + json[:selfie_live] = selfie_live? if show_selfie_failures? + json[:selfie_quality_good] = selfie_quality_good? if show_selfie_failures? json[:failed_image_fingerprints] = failed_fingerprints json end @@ -90,10 +91,14 @@ def failed_fingerprints @form_response.extra[:failed_image_fingerprints] || { front: [], back: [], selfie: [] } end - def show_selfie_failures + def show_selfie_failures? @form_response.extra[:liveness_checking_required] == true end + def selfie_status + @form_response.respond_to?(:selfie_status) ? @form_response.selfie_status : :not_processed + end + def selfie_live? @form_response.respond_to?(:selfie_live?) ? @form_response.selfie_live? : true end diff --git a/app/services/doc_auth/lexis_nexis/request.rb b/app/services/doc_auth/lexis_nexis/request.rb index 422385329ed..68ca892ec57 100644 --- a/app/services/doc_auth/lexis_nexis/request.rb +++ b/app/services/doc_auth/lexis_nexis/request.rb @@ -70,6 +70,7 @@ def handle_connection_error(exception:, status_code: nil, status_message: nil) selfie_quality_good: false, vendor_status_code: status_code, vendor_status_message: status_message, + reference: @reference, }.compact, ) end @@ -146,13 +147,14 @@ def hmac_authorization end def settings + @reference = uuid { Type: 'Initiate', Settings: { Mode: request_mode, Locale: config.locale, Venue: 'online', - Reference: uuid, + Reference: @reference, }, } end diff --git a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb index bdfbc98da61..bfa3acb7a82 100644 --- a/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb +++ b/app/services/doc_auth/lexis_nexis/responses/true_id_response.rb @@ -37,7 +37,8 @@ def initialize(http_response, config, liveness_checking_enabled = false) # document type # bar code attention def successful_result? - (all_passed? || attention_with_barcode?) && id_type_supported? + doc_auth_success? && + (@liveness_checking_enabled ? selfie_passed? : true) end # all checks from document perspectives, without considering selfie: @@ -193,15 +194,6 @@ def basic_logging_info } end - # Status of all checks from Vendor perspective - def all_passed? - transaction_status_passed? && - true_id_product.present? && - product_status_passed? && - doc_auth_result_passed? && - (@liveness_checking_enabled ? selfie_passed? : true) - end - def selfie_result portrait_match_results&.dig(:FaceMatchResult) end diff --git a/app/services/doc_auth/mock/doc_auth_mock_client.rb b/app/services/doc_auth/mock/doc_auth_mock_client.rb index 82427f1bd49..739a6617fe0 100644 --- a/app/services/doc_auth/mock/doc_auth_mock_client.rb +++ b/app/services/doc_auth/mock/doc_auth_mock_client.rb @@ -75,10 +75,13 @@ def post_images( back_image_response = post_back_image(image: back_image, instance_id: instance_id) return back_image_response unless back_image_response.success? - get_results(instance_id: instance_id) + get_results( + instance_id: instance_id, + selfie_required: liveness_checking_required, + ) end - def get_results(instance_id:) + def get_results(instance_id:, selfie_required: false) return mocked_response_for_method(__method__) if method_mocked?(__method__) error_response = http_error_response(self.class.last_uploaded_back_image, 'result') return error_response if error_response @@ -91,6 +94,7 @@ def get_results(instance_id:) ResultResponse.new( self.class.last_uploaded_back_image, overriden_config, + selfie_required, ) end # rubocop:enable Lint/UnusedMethodArgument diff --git a/app/services/doc_auth/mock/result_response.rb b/app/services/doc_auth/mock/result_response.rb index 4cb188025cc..52bd4699761 100644 --- a/app/services/doc_auth/mock/result_response.rb +++ b/app/services/doc_auth/mock/result_response.rb @@ -7,9 +7,10 @@ class ResultResponse < DocAuth::Response attr_reader :uploaded_file, :config - def initialize(uploaded_file, config) + def initialize(uploaded_file, config, selfie_required = false) @uploaded_file = uploaded_file.to_s @config = config + @selfie_required = selfie_required super( success: success?, errors: errors, @@ -22,6 +23,7 @@ def initialize(uploaded_file, config) portrait_match_results: portrait_match_results, billed: true, classification_info: classification_info, + liveness_checking_required: @selfie_required, }.compact, ) end @@ -138,8 +140,12 @@ def doc_auth_success? end def selfie_status - return :not_processed if portrait_match_results&.dig(:FaceMatchResult).nil? - portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail + if @selfie_required + return :success if portrait_match_results&.dig(:FaceMatchResult).nil? + portrait_match_results[:FaceMatchResult] == 'Pass' ? :success : :fail + else + :not_processed + end end private @@ -240,6 +246,7 @@ def create_response_info( liveness_enabled: liveness_enabled, classification_info: classification_info, portrait_match_results: selfie_check_performed? ? portrait_match_results : nil, + extra: { liveness_checking_required: liveness_enabled }, }.compact end end diff --git a/app/services/doc_auth/response.rb b/app/services/doc_auth/response.rb index 5c311934a82..994f0b3b229 100644 --- a/app/services/doc_auth/response.rb +++ b/app/services/doc_auth/response.rb @@ -18,6 +18,7 @@ def initialize( pii_from_doc: {}, attention_with_barcode: false, doc_type_supported: true, + selfie_status: :not_processed, selfie_live: true, selfie_quality_good: true ) @@ -28,6 +29,7 @@ def initialize( @pii_from_doc = pii_from_doc @attention_with_barcode = attention_with_barcode @doc_type_supported = doc_type_supported + @selfie_status = selfie_status @selfie_live = selfie_live @selfie_quality_good = selfie_quality_good end @@ -41,6 +43,7 @@ def merge(other) pii_from_doc: pii_from_doc.merge(other.pii_from_doc), attention_with_barcode: attention_with_barcode? || other.attention_with_barcode?, doc_type_supported: doc_type_supported? || other.doc_type_supported?, + selfie_status: selfie_status, selfie_live: selfie_live?, selfie_quality_good: selfie_quality_good?, ) diff --git a/app/services/document_capture_session_result.rb b/app/services/document_capture_session_result.rb index 374a338cce4..33ae3c94e94 100644 --- a/app/services/document_capture_session_result.rb +++ b/app/services/document_capture_session_result.rb @@ -28,7 +28,7 @@ def selfie_status def success_status # doc_auth_success : including document, attention_with_barcode and id type verification - !!doc_auth_success && selfie_status != :fail + !!doc_auth_success && selfie_status != :fail && !!pii end alias_method :success?, :success_status diff --git a/app/services/rate_limiter.rb b/app/services/rate_limiter.rb index f278b178fe3..67f7f06f7f1 100644 --- a/app/services/rate_limiter.rb +++ b/app/services/rate_limiter.rb @@ -71,18 +71,19 @@ def increment! return if limited? value = nil + now = Time.zone.now REDIS_THROTTLE_POOL.with do |client| value, _success = client.multi do |multi| multi.incr(key) - multi.expire( + multi.expireat( key, - RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes.seconds.to_i, + now + RateLimiter.attempt_window_in_minutes(rate_limit_type).minutes.seconds.to_i, ) end end @redis_attempts = value.to_i - @redis_attempted_at = Time.zone.now + @redis_attempted_at = now attempts end diff --git a/app/services/service_provider_request_proxy.rb b/app/services/service_provider_request_proxy.rb index f99c21b5947..a7a8f983366 100644 --- a/app/services/service_provider_request_proxy.rb +++ b/app/services/service_provider_request_proxy.rb @@ -36,6 +36,7 @@ def self.find_or_create_by(uuid:) aal: nil, requested_attributes: nil, biometric_comparison_required: false, acr_values: nil, vtr: nil ) + yield(spr) create( uuid: uuid, @@ -59,8 +60,8 @@ def self.create(hash) :aal, :requested_attributes, :biometric_comparison_required, - :acr_values, :vtr, + :acr_values, ) write(obj, uuid) hash_to_spr(obj, uuid) diff --git a/app/services/store_sp_metadata_in_session.rb b/app/services/store_sp_metadata_in_session.rb index c3c0577504e..e42746c95b5 100644 --- a/app/services/store_sp_metadata_in_session.rb +++ b/app/services/store_sp_metadata_in_session.rb @@ -16,6 +16,16 @@ def call(service_provider_request: nil, requested_service_provider: nil) attr_reader :session, :request_id + def parsed_vot + return nil if !sp_request.vtr && !sp_request.acr_values + + @parsed_vot ||= AuthnContextResolver.new( + service_provider: service_provider, + vtr: sp_request.vtr, + acr_values: sp_request.acr_values, + ).resolve + end + def ial_context @ial_context ||= IalContext.new(ial: sp_request.ial, service_provider: service_provider) end @@ -24,38 +34,66 @@ def sp_request @sp_request ||= ServiceProviderRequestProxy.from_uuid(request_id) end + def ial_value + return nil unless parsed_vot + + if parsed_vot&.ialmax? + 0 + elsif parsed_vot&.identity_proofing? + 2 + elsif parsed_vot + 1 + end + end + + def ial2_value + parsed_vot&.identity_proofing? + end + + def ialmax_value + parsed_vot&.ialmax? + end + + def aal_level_requested_value + return nil unless parsed_vot + + if parsed_vot.aal2? + 2 + else + 1 + end + end + + def piv_cac_requested_value + parsed_vot&.hspd12? + end + + def phishing_resistant_value + parsed_vot&.phishing_resistant? + end + + def biometric_comparison_required_value + parsed_vot&.biometric_comparison? || sp_request&.biometric_comparison_required + end + def update_session session[:sp] = { issuer: sp_request.issuer, - ial: ial_context.ial, - ial2: ial_context.ial2_requested?, - ialmax: ial_context.ialmax_requested?, - aal_level_requested: aal_requested, - piv_cac_requested: hspd12_requested, - phishing_resistant_requested: phishing_resistant_requested, request_url: sp_request.url, request_id: sp_request.uuid, requested_attributes: sp_request.requested_attributes, - biometric_comparison_required: sp_request.biometric_comparison_required, + ial: ial_value, + ial2: ial2_value, + ialmax: ialmax_value, + aal_level_requested: aal_level_requested_value, + piv_cac_requested: piv_cac_requested_value, + phishing_resistant_requested: phishing_resistant_value, + biometric_comparison_required: biometric_comparison_required_value, acr_values: sp_request.acr_values, vtr: sp_request.vtr, } end - def aal_requested - Saml::Idp::Constants::AUTHN_CONTEXT_CLASSREF_TO_AAL[sp_request.aal] - end - - def phishing_resistant_requested - sp_request.aal == Saml::Idp::Constants::AAL2_PHISHING_RESISTANT_AUTHN_CONTEXT_CLASSREF || - sp_request.aal == Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF - end - - def hspd12_requested - sp_request.aal == Saml::Idp::Constants::AAL3_HSPD12_AUTHN_CONTEXT_CLASSREF || - sp_request.aal == Saml::Idp::Constants::AAL2_HSPD12_AUTHN_CONTEXT_CLASSREF - end - def service_provider return @service_provider if defined?(@service_provider) @service_provider = ServiceProvider.find_by(issuer: sp_request.issuer) diff --git a/app/services/vot/legacy_component_values.rb b/app/services/vot/legacy_component_values.rb index 034edb6d40b..785c70a7bf3 100644 --- a/app/services/vot/legacy_component_values.rb +++ b/app/services/vot/legacy_component_values.rb @@ -67,7 +67,7 @@ module LegacyComponentValues name: Saml::Idp::Constants::AAL3_AUTHN_CONTEXT_CLASSREF, description: 'Legacy AAL3', implied_component_values: [], - requirements: [:aal2, :hspd12], + requirements: [:aal2, :phishing_resistant], ) AAL3_HSPD12 = ComponentValue.new( name: Saml::Idp::Constants::AAL3_HSPD12_AUTHN_CONTEXT_CLASSREF, diff --git a/app/services/vot/parser.rb b/app/services/vot/parser.rb index 3fa71a67a1a..1ac2ed9e8b0 100644 --- a/app/services/vot/parser.rb +++ b/app/services/vot/parser.rb @@ -25,6 +25,11 @@ def parse elsif acr_values.present? map_initial_acr_values_to_component_values end + + if !initial_components + raise ParseException.new('VoT parser called without VoT or ACR values') + end + expand_components_with_initial_components(initial_components) end diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index 48c66b23065..24047efaa5f 100644 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -50,7 +50,7 @@ <%= hidden_field_tag :platform_authenticator_available, id: 'platform_authenticator_available' %> <%= f.submit t('links.sign_in'), full_width: true, wide: false %> <% end %> -<% if @ial && desktop_device? %> +<% if desktop_device? %>
<%= link_to( t('account.login.piv_cac'), @@ -88,4 +88,3 @@ <%= javascript_packs_tag_once('platform-authenticator-available') %> - diff --git a/app/views/idv/please_call/show.html.erb b/app/views/idv/please_call/show.html.erb index 1172c70fe15..71b455203e4 100644 --- a/app/views/idv/please_call/show.html.erb +++ b/app/views/idv/please_call/show.html.erb @@ -1,11 +1,14 @@ -<% content_for(:pre_flash_content) do %> - <%= render StepIndicatorComponent.new( - steps: Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS, - current_step: :secure_account, - locale_scope: 'idv', - class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', - ) %> -<% end %> +<% if @in_person == false or @in_person.nil? %> + <% content_for(:pre_flash_content) do %> + <%= render StepIndicatorComponent.new( + steps: Idv::StepIndicatorConcern::STEP_INDICATOR_STEPS, + current_step: :secure_account, + locale_scope: 'idv', + class: 'margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4', + ) %> + <% end %> +<% end %> + <%= render( 'idv/shared/error', title: t('titles.failure.information_not_verified'), @@ -15,6 +18,6 @@ <%= t('idv.failure.setup.fail_date_html', date_html: I18n.l(@call_by_date, format: I18n.t('time.formats.event_date'))) %>

- <%= t('idv.failure.setup.fail', support_code: IdentityConfig.store.lexisnexis_threatmetrix_support_code, contact_number: IdentityConfig.store.idv_contact_phone_number) %> + <%= t('idv.failure.setup.fail_html', support_code: IdentityConfig.store.lexisnexis_threatmetrix_support_code, contact_number: IdentityConfig.store.idv_contact_phone_number) %>

<% end %> diff --git a/app/views/layouts/account_side_nav.html.erb b/app/views/layouts/account_side_nav.html.erb index 1f7046969df..9fafd48cb8a 100644 --- a/app/views/layouts/account_side_nav.html.erb +++ b/app/views/layouts/account_side_nav.html.erb @@ -6,7 +6,9 @@ <%= render 'accounts/mobile_nav' %> <% end %> -<% content_for :content do %> +<%= javascript_packs_tag_once('navigation') %> + +<%= extends_layout :application, body_class: 'site', disable_card: true, user_main_tag: false do %>
<% end %> - -<%= javascript_packs_tag_once('navigation') %> -<%= render template: 'layouts/base', locals: { disable_card: true, user_main_tag: false } %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 42996cf65e1..9f4331c6143 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -1,3 +1,37 @@ -<% background_cls 'tablet:bg-primary-lighter' %> +<%= extends_layout :base, body_class: local_assigns.fetch(:body_class, 'site tablet:bg-primary-lighter') do %> + <%= link_to t('shared.skip_link'), '#main-content', class: 'usa-skipnav' %> +
+ <% if content_for?(:mobile_nav) %> +
+ <%= yield(:mobile_nav) %> +
+ <% end %> + <%= render 'shared/banner' %> + <%= content_tag( + local_assigns[:user_main_tag] == false ? 'div' : 'main', + class: 'site-wrap bg-primary-lighter', + id: local_assigns[:user_main_tag] == false ? nil : 'main-content', + ) do %> +
+ <%= yield(:pre_flash_content) if content_for?(:pre_flash_content) %> + <%= render FlashComponent.new(flash: flash) %> + <%= yield %> +
+ <% end %> + <%= render 'shared/footer_lite' %> -<%= render template: 'layouts/base' %> + <% if current_user %> + <%= render partial: 'session_timeout/ping', + locals: { + warning: session_timeout_warning, + start: session_timeout_start, + frequency: session_timeout_frequency, + modal: session_modal, + } %> + <% elsif !@skip_session_expiration %> + <%= render partial: 'session_timeout/expire_session', + locals: { + session_timeout_in: Devise.timeout_in, + } %> + <% end %> +<% end %> diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 60422ebd37b..dfb2599f44e 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -60,42 +60,8 @@ <%= yield(:head) if content_for?(:head) %> - - <%= link_to t('shared.skip_link'), '#main-content', class: 'usa-skipnav' %> -
- <% if content_for?(:mobile_nav) %> -
- <%= yield(:mobile_nav) %> -
- <% end %> - <%= render 'shared/banner' %> - <%= content_tag( - local_assigns[:user_main_tag] == false ? 'div' : 'main', - class: 'site-wrap bg-primary-lighter', - id: local_assigns[:user_main_tag] == false ? nil : 'main-content', - ) do %> -
- <%= yield(:pre_flash_content) if content_for?(:pre_flash_content) %> - <%= render FlashComponent.new(flash: flash) %> - <%= content_for?(:content) ? yield(:content) : yield %> -
- <% end %> - <%= render 'shared/footer_lite' %> - - <% if current_user %> - <%= render partial: 'session_timeout/ping', - locals: { - warning: session_timeout_warning, - start: session_timeout_start, - frequency: session_timeout_frequency, - modal: session_modal, - } %> - <% elsif !@skip_session_expiration %> - <%= render partial: 'session_timeout/expire_session', - locals: { - session_timeout_in: Devise.timeout_in, - } %> - <% end %> +<%= content_tag(:body, class: local_assigns[:body_class]) do %> + <%= yield %> <%= content_tag( :script, @@ -111,6 +77,6 @@ <%= render_javascript_pack_once_tags %> <%= render 'shared/dap_analytics' if IdentityConfig.store.participate_in_dap && !session_with_trust? %> - +<% end %> diff --git a/app/views/layouts/flow_step.html.erb b/app/views/layouts/flow_step.html.erb index d700ea6457a..c612769c92f 100644 --- a/app/views/layouts/flow_step.html.erb +++ b/app/views/layouts/flow_step.html.erb @@ -8,5 +8,4 @@ ) %> <% end %> <% end %> -<% content_for(:content) { render(template: step_template, locals: local_assigns) } %> -<%= render template: 'layouts/base' %> +<%= render(template: step_template, locals: local_assigns) %> diff --git a/app/views/layouts/no_card.html.erb b/app/views/layouts/no_card.html.erb index 55592264cdc..c11b20c15d9 100644 --- a/app/views/layouts/no_card.html.erb +++ b/app/views/layouts/no_card.html.erb @@ -1 +1 @@ -<%= render template: 'layouts/base', locals: { disable_card: true } %> +<%= extends_layout(:application, body_class: 'site', disable_card: true) { yield } %> diff --git a/app/views/openid_connect/shared/redirect.html.erb b/app/views/openid_connect/shared/redirect.html.erb index 75531cb75e1..d4db1437327 100644 --- a/app/views/openid_connect/shared/redirect.html.erb +++ b/app/views/openid_connect/shared/redirect.html.erb @@ -1,12 +1,5 @@ - - - - - <%= t('headings.redirecting') %> | <%= APP_NAME %> - <%= stylesheet_link_tag 'application', media: 'all' %> - <%= render_stylesheet_once_tags %> - - - - - +<% self.title = t('headings.redirecting') %> + +<%= content_for(:meta_refresh, "0;url=#{@oidc_redirect_uri}") %> + +<%= extends_layout :base %> diff --git a/app/views/openid_connect/shared/redirect_js.html.erb b/app/views/openid_connect/shared/redirect_js.html.erb index 79613be8c0b..171bd3c2d81 100644 --- a/app/views/openid_connect/shared/redirect_js.html.erb +++ b/app/views/openid_connect/shared/redirect_js.html.erb @@ -1,28 +1,18 @@ - - - - - <%= t('headings.redirecting') %> | <%= APP_NAME %> - <%= javascript_tag(nonce: true) do %> - document.documentElement.classList.replace('no-js', 'js'); - <% end %> - <%= stylesheet_link_tag 'application', media: 'all' %> - <%= render_stylesheet_once_tags %> - - -
-
-
- <%= render PageHeadingComponent.new.with_content(t('saml_idp.shared.saml_post_binding.heading')) %> +<% self.title = t('headings.redirecting') %> -

- <%= t('saml_idp.shared.saml_post_binding.no_js') %> -

+<%= extends_layout :base do %> +
+
+
+ <%= render PageHeadingComponent.new.with_content(t('saml_idp.shared.saml_post_binding.heading')) %> - <%= link_to(t('forms.buttons.continue'), @oidc_redirect_uri, class: 'usa-button usa-button--wide usa-button--big', data: { click_immediate: '' }) %> -
+

+ <%= t('saml_idp.shared.saml_post_binding.no_js') %> +

+ + <%= link_to(t('forms.buttons.continue'), @oidc_redirect_uri, class: 'usa-button usa-button--wide usa-button--big', data: { click_immediate: '' }) %>
- <%= render_javascript_pack_once_tags 'click-immediate' %> - - +
+ <%= render_javascript_pack_once_tags 'click-immediate' %> +<% end %> diff --git a/app/views/shared/saml_post_form.html.erb b/app/views/shared/saml_post_form.html.erb index b305dff895d..f68172dac8f 100644 --- a/app/views/shared/saml_post_form.html.erb +++ b/app/views/shared/saml_post_form.html.erb @@ -1,33 +1,23 @@ - - - - - <%= javascript_tag(nonce: true) do %> - document.documentElement.classList.replace('no-js', 'js'); - <% end %> - <%= csrf_meta_tags %> - <%= stylesheet_link_tag 'application', media: 'all' %> - <%= render_stylesheet_once_tags %> - - -
-
-
- <%= render PageHeadingComponent.new.with_content(t('saml_idp.shared.saml_post_binding.heading')) %> +<% self.title = t('headings.redirecting') %> -

- <%= t('saml_idp.shared.saml_post_binding.no_js') %> -

+<%= extends_layout :base do %> +
+
+
+ <%= render PageHeadingComponent.new.with_content(t('saml_idp.shared.saml_post_binding.heading')) %> - <%= simple_form_for('', url: action_url) do |f| %> - <% form_params.each do |key, val| %> - <%= hidden_field_tag(key, val) %> - <% end %> - <%= f.submit t('forms.buttons.submit.default'), data: { click_immediate: '' } %> +

+ <%= t('saml_idp.shared.saml_post_binding.no_js') %> +

+ + <%= simple_form_for('', url: action_url) do |f| %> + <% form_params.each do |key, val| %> + <%= hidden_field_tag(key, val) %> <% end %> -
+ <%= f.submit t('forms.buttons.submit.default'), data: { click_immediate: '' } %> + <% end %>
- <%= render_javascript_pack_once_tags 'click-immediate' %> - - +
+ <%= render_javascript_pack_once_tags 'click-immediate' %> +<% end %> diff --git a/config/application.yml.default b/config/application.yml.default index fa0507797c2..9b82d3281e7 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -342,6 +342,7 @@ verify_personal_key_max_attempts: 5 version_headers_enabled: false use_dashboard_service_providers: false use_kms: false +use_vot_in_sp_requests: false usps_auth_token_refresh_job_enabled: false usps_confirmation_max_days: 30 usps_ipp_password: '' diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index 2b70b1cba1c..ff153d1258a 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -137,10 +137,10 @@ en: warning: Please check the information you entered and try again. Common mistakes are an incorrect Social Security number or ZIP Code. setup: - fail: Call %{contact_number} and provide them with the error code - %{support_code}. - fail_date_html: Call our contact center by %{date_html} to continue verifying - your identity. + fail_date_html: Call our contact center by %{date_html} to + continue verifying your identity. + fail_html: Call %{contact_number} and provide them with the + error
code %{support_code}. heading: Please give us a call timeout: We are experiencing higher than usual wait time processing your request. Please try again. diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index 9144315a446..d7f32050a59 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -147,10 +147,10 @@ es: Los errores más comunes suelen producirse al ingresar un Número de Seguridad Social o un Código Postal incorrecto. setup: - fail: Llame al %{contact_number} y facilíteles el código de error - %{support_code}. - fail_date_html: Llame a nuestro centro de atención antes del %{date_html} para - seguir verificando su identidad. + fail_date_html: Llame a nuestro centro de atención antes del + %{date_html} para seguir verificando su identidad. + fail_html: Llame al %{contact_number} y facilíteles el código + de
error %{support_code}. heading: Llámenos timeout: Estamos experimentando un tiempo de espera superior al habitual al procesar su solicitud. Inténtalo de nuevo. diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 8966a537d34..283c6958e1b 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -152,9 +152,11 @@ fr: Les erreurs les plus courantes sont un numéro de sécurité sociale ou un code postal incorrect. setup: - fail: Appelez le %{contact_number} et indiquez le code d’erreur %{support_code}. - fail_date_html: Appelez notre centre de contact avant le %{date_html} pour - continuer à vérifier votre identité. + fail_date_html: Appelez notre centre de contact avant le + %{date_html} pour continuer à vérifier votre + identité. + fail_html: Appelez le %{contact_number} et indiquez le +
code d’erreur %{support_code}. heading: S’il vous plaît, appelez-nous timeout: Le temps d’attente pour le traitement de votre demande est plus long que d’habitude Veuillez réessayer. diff --git a/lib/data_pull.rb b/lib/data_pull.rb index dd5b211f565..671efa0c8bb 100644 --- a/lib/data_pull.rb +++ b/lib/data_pull.rb @@ -152,13 +152,36 @@ def run(args:, config:) ActiveRecord::Base.connection.execute('SET statement_timeout = 0') uuids = args - users = uuids.map { |uuid| DataRequests::Deployed::LookupUserByUuid.new(uuid).call }.compact + users, missing_uuids = uuids.map do |uuid| + DataRequests::Deployed::LookupUserByUuid.new(uuid).call || uuid + end.partition { |u| u.is_a?(User) } + shared_device_users = DataRequests::Deployed::LookupSharedDeviceUsers.new(users).call output = shared_device_users.map do |user| DataRequests::Deployed::CreateUserReport.new(user, config.requesting_issuers).call end + if config.include_missing? + output += missing_uuids.map do |uuid| + { + user_id: nil, + login_uuid: nil, + requesting_issuer_uuid: uuid, + email_addresses: [], + mfa_configurations: { + phone_configurations: [], + auth_app_configurations: [], + webauthn_configurations: [], + piv_cac_configurations: [], + backup_code_configurations: [], + }, + user_events: [], + not_found: true, + } + end + end + ScriptBase::Result.new( subtask: 'ig-request', uuids: users.map(&:uuid), diff --git a/lib/data_requests/deployed/create_mfa_configurations_report.rb b/lib/data_requests/deployed/create_mfa_configurations_report.rb index 58b050f02ab..6a2f97e0b69 100644 --- a/lib/data_requests/deployed/create_mfa_configurations_report.rb +++ b/lib/data_requests/deployed/create_mfa_configurations_report.rb @@ -40,6 +40,7 @@ def backup_code_configurations_report def phone_configurations_report user.phone_configurations.map do |phone_configuration| { + id: phone_configuration.id, phone: phone_configuration.phone, created_at: phone_configuration.created_at, confirmed_at: phone_configuration.confirmed_at, diff --git a/lib/data_requests/local/write_cloudwatch_logs.rb b/lib/data_requests/local/write_cloudwatch_logs.rb index c9f5a1939ab..9805edd5877 100644 --- a/lib/data_requests/local/write_cloudwatch_logs.rb +++ b/lib/data_requests/local/write_cloudwatch_logs.rb @@ -4,6 +4,7 @@ module DataRequests module Local class WriteCloudwatchLogs HEADERS = %w[ + uuid timestamp event_name success @@ -14,19 +15,24 @@ class WriteCloudwatchLogs user_agent ].freeze - attr_reader :cloudwatch_results, :output_dir + attr_reader :cloudwatch_results, :requesting_issuer_uuid, :csv - def initialize(cloudwatch_results, output_dir) + def initialize(cloudwatch_results:, requesting_issuer_uuid:, csv:, include_header: false) @cloudwatch_results = cloudwatch_results - @output_dir = output_dir + @requesting_issuer_uuid = requesting_issuer_uuid + @csv = csv + @include_header = include_header + end + + def include_header? + !!@include_header end def call - CSV.open(File.join(output_dir, 'logs.csv'), 'w') do |csv| - csv << HEADERS - cloudwatch_results.each do |row| - csv << build_row(row) - end + csv << HEADERS if include_header? + + cloudwatch_results.each do |row| + csv << build_row(row) end end @@ -60,6 +66,7 @@ def build_row(row) user_agent = data.dig('properties', 'user_agent') [ + requesting_issuer_uuid, timestamp, event_name, success, diff --git a/lib/data_requests/local/write_user_events.rb b/lib/data_requests/local/write_user_events.rb index 034deaf7e1d..f60fafce969 100644 --- a/lib/data_requests/local/write_user_events.rb +++ b/lib/data_requests/local/write_user_events.rb @@ -3,16 +3,21 @@ module DataRequests module Local class WriteUserEvents - attr_reader :user_report, :output_dir, :requesting_issuer_uuid + attr_reader :user_report, :requesting_issuer_uuid, :csv - def initialize(user_report, output_dir, requesting_issuer_uuid) + def initialize(user_report:, requesting_issuer_uuid:, csv:, include_header: false) @user_report = user_report - @output_dir = output_dir + @csv = csv @requesting_issuer_uuid = requesting_issuer_uuid + @include_header = include_header + end + + def include_header? + !!@include_header end def call - CSV.open(File.join(output_dir, 'events.csv'), 'w') do |csv| + if include_header? csv << %w[ uuid event_name @@ -22,17 +27,17 @@ def call user_agent device_cookie ] + end - user_report[:user_events].each do |row| - csv << [requesting_issuer_uuid] + row.values_at( - :event_name, - :date_time, - :ip, - :disavowed_at, - :user_agent, - :device_cookie, - ) - end + user_report[:user_events].each do |row| + csv << [requesting_issuer_uuid] + row.values_at( + :event_name, + :date_time, + :ip, + :disavowed_at, + :user_agent, + :device_cookie, + ) end end end diff --git a/lib/data_requests/local/write_user_info.rb b/lib/data_requests/local/write_user_info.rb index e1f67b23209..389d0a2d230 100644 --- a/lib/data_requests/local/write_user_info.rb +++ b/lib/data_requests/local/write_user_info.rb @@ -3,86 +3,122 @@ module DataRequests module Local class WriteUserInfo - attr_reader :user_report, :output_dir + attr_reader :user_report, :csv - def initialize(user_report, output_dir) + def initialize(user_report:, csv:, include_header: false) @user_report = user_report - @output_dir = output_dir + @csv = csv + @include_header = include_header + end + + def include_header? + !!@include_header end def call + if include_header? + csv << %w[ + uuid + type + value + created_at + confirmed_at + internal_id + ] + end + + write_not_found write_emails write_phone_configurations write_auth_app_configurations write_webauthn_configurations write_piv_cac_configurations write_backup_code_configurations - output_file.close end private - def output_file - @output_file ||= begin - output_path = File.join(output_dir, 'user.csv') - File.open(output_path, 'w') - end + def uuid + user_report[:requesting_issuer_uuid] end - def write_rows_to_csv(rows, *columns) - output_file.puts(columns.join(',')) - - return output_file.puts("No data\n\n") if rows.empty? - - rows.each do |row| - output_file.puts CSV.generate_line(row.values_at(*columns)) + def write_not_found + if user_report[:not_found] + csv << [ + uuid, + 'not found', + ] end - output_file.puts("\n") end def write_auth_app_configurations - output_file.puts('Auth app configurations:') - write_rows_to_csv( - user_report[:mfa_configurations][:auth_app_configurations], - :name, :created_at - ) + user_report[:mfa_configurations][:auth_app_configurations].each do |auth_app_config| + csv << [ + uuid, + 'Auth app configuration', + auth_app_config[:name], + auth_app_config[:created_at], + ] + end end def write_backup_code_configurations - output_file.puts('Backup code configurations:') - write_rows_to_csv( - user_report[:mfa_configurations][:backup_code_configurations], - :created_at, :used_at - ) + user_report[:mfa_configurations][:backup_code_configurations].each do |backup_code_config| + csv << [ + uuid, + 'Backup code configuration', + nil, + backup_code_config[:created_at], + backup_code_config[:used_at], + ] + end end def write_emails - output_file.puts('Emails:') - write_rows_to_csv(user_report[:email_addresses], :email, :created_at, :confirmed_at) + user_report[:email_addresses].each do |email| + csv << [ + uuid, + 'Email', + email[:email], + email[:created_at], + email[:confirmed_at], + ] + end end def write_phone_configurations - output_file.puts('Phone configurations:') - write_rows_to_csv( - user_report[:mfa_configurations][:phone_configurations], - :phone, :created_at, :confirmed_at - ) + user_report[:mfa_configurations][:phone_configurations].each do |phone_config| + csv << [ + uuid, + 'Phone configuration', + phone_config[:phone], + phone_config[:created_at], + phone_config[:confirmed_at], + phone_config[:id], + ] + end end def write_piv_cac_configurations - output_file.puts('PIV/CAC configurations:') - write_rows_to_csv( - user_report[:mfa_configurations][:piv_cac_configurations], - :name, :created_at - ) + user_report[:mfa_configurations][:piv_cac_configurations].each do |piv_cac_config| + csv << [ + uuid, + 'PIV/CAC configuration', + piv_cac_config[:name], + piv_cac_config[:created_at], + ] + end end def write_webauthn_configurations - output_file.puts('WebAuthn configurations:') - write_rows_to_csv( - user_report[:mfa_configurations][:webauthn_configurations], - :name, :created_at - ) + user_report[:mfa_configurations][:webauthn_configurations].each do |webauthn_config| + csv << [ + uuid, + 'WebAuthn configuration', + webauthn_config[:name], + webauthn_config[:created_at], + ] + end end end end diff --git a/lib/reporting/mfa_report.rb b/lib/reporting/mfa_report.rb new file mode 100644 index 00000000000..15523728c9d --- /dev/null +++ b/lib/reporting/mfa_report.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'csv' +begin + require 'reporting/cloudwatch_client' + require 'reporting/cloudwatch_query_quoting' + require 'reporting/command_line_options' +rescue LoadError => e + warn 'could not load paths, try running with "bundle exec rails runner"' + raise e +end + +module Reporting + class MfaReport + include Reporting::CloudwatchQueryQuoting + + attr_reader :issuers, :time_range + EVENT = 'Multi-Factor Authentication' + + # @param [Array] issuers + # @param [Range