Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions app/jobs/vendor_proof_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class VendorProofJob
def self.perform(document_capture_session_id, stages)
dcs = DocumentCaptureSession.find_by(uuid: document_capture_session_id)
result = dcs.load_proofing_result
stages = stages.map(&:to_sym)
idv_result = Idv::Agent.new(result.pii).proof(*stages)
dcs.store_proofing_result(result.pii, idv_result)
end
end
37 changes: 32 additions & 5 deletions app/models/document_capture_session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,45 @@ class DocumentCaptureSession < ApplicationRecord
belongs_to :user

def load_result
DocumentCaptureSessionResult.load(result_id)
EncryptedRedisStructStorage.load(result_id, type: DocumentCaptureSessionResult)
end

def load_proofing_result
EncryptedRedisStructStorage.load(result_id, type: ProofingDocumentCaptureSessionResult)
end

def store_result_from_response(doc_auth_response)
DocumentCaptureSessionResult.store(
id: generate_result_id,
success: doc_auth_response.success?,
pii: doc_auth_response.pii_from_doc,
EncryptedRedisStructStorage.store(
DocumentCaptureSessionResult.new(
id: generate_result_id,
success: doc_auth_response.success?,
pii: doc_auth_response.pii_from_doc,
),
)
save!
end

def store_proofing_pii_from_doc(pii_from_doc)
EncryptedRedisStructStorage.store(
ProofingDocumentCaptureSessionResult.new(
id: generate_result_id,
pii: pii_from_doc,
result: nil,
),
)
save!
end

def store_proofing_result(pii_from_doc, result)
EncryptedRedisStructStorage.store(
ProofingDocumentCaptureSessionResult.new(
id: result_id,
pii: pii_from_doc,
result: result,
),
)
end

def expired?
return true unless requested_at
requested_at + Figaro.env.doc_capture_request_valid_for_minutes.to_i.minutes < Time.zone.now
Expand Down
2 changes: 0 additions & 2 deletions app/services/doc_auth/mock/doc_auth_mock_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def post_selfie(image:, instance_id:)
DocAuth::Response.new(success: true)
end

# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def post_images(front_image:, back_image:, selfie_image:, liveness_checking_enabled: nil)
return mocked_response_for_method(__method__) if method_mocked?(__method__)

Expand All @@ -73,7 +72,6 @@ def post_images(front_image:, back_image:, selfie_image:, liveness_checking_enab
results_response
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

def get_results(instance_id:)
return mocked_response_for_method(__method__) if method_mocked?(__method__)
Expand Down
64 changes: 6 additions & 58 deletions app/services/document_capture_session_result.rb
Original file line number Diff line number Diff line change
@@ -1,62 +1,10 @@
class DocumentCaptureSessionResult
REDIS_KEY_PREFIX = 'dcs:result'.freeze
# frozen_string_literal: true

attr_reader :id, :success, :pii

alias success? success
alias pii_from_doc pii

class << self
def load(id)
ciphertext = REDIS_POOL.with { |client| client.read(key(id)) }
return nil if ciphertext.blank?
decrypt_and_deserialize(id, ciphertext)
end

def store(id:, success:, pii:)
result = new(id: id, success: success, pii: pii)
REDIS_POOL.with do |client|
client.write(key(id), result.serialize_and_encrypt, expires_in: 60)
end
end

def key(id)
[REDIS_KEY_PREFIX, id].join(':')
end

private

def decrypt_and_deserialize(id, ciphertext)
deserialize(
id,
Encryption::Encryptors::SessionEncryptor.new.decrypt(ciphertext),
)
end

def deserialize(id, json)
data = JSON.parse(json)
new(
id: id,
success: data['success'],
pii: data['pii'],
)
end
end

def initialize(id:, success:, pii:)
@id = id
@success = success
@pii = pii
DocumentCaptureSessionResult = Struct.new(:id, :success, :pii, keyword_init: true) do
def self.redis_key_prefix
'dcs:result'
end

def serialize
{
success: success,
pii: pii,
}.to_json
end

def serialize_and_encrypt
Encryption::Encryptors::SessionEncryptor.new.encrypt(serialize)
end
alias_method :success?, :success
alias_method :pii_from_doc, :pii
end
73 changes: 73 additions & 0 deletions app/services/encrypted_redis_struct_storage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Use this class to store a plain Struct in redis. It will be stored
# encrypted and by default will expire, the struct must have a +redis_key_prefix+
# class method
#
# @example
# MyStruct = Struct.new(:id, :a, :b) do
# def self.redis_key_prefix
# 'mystruct'
# end
# end
#
# struct = MyStruct.new('id123', 'a', 'b')
#
# EncryptedRedisStructStorage.store(struct)
# s = EncryptedRedisStructStorage.load('id123', type: MyStruct)
module EncryptedRedisStructStorage
module_function

def load(id, type:)
check_for_id_property!(type)

ciphertext = REDIS_POOL.with { |client| client.read(key(id, type: type)) }
return nil if ciphertext.blank?

json = Encryption::Encryptors::SessionEncryptor.new.decrypt(ciphertext)
data = JSON.parse(json, symbolize_names: true)
type.new.tap do |struct|
struct.id = id
init_fields(struct: struct, data: data)
end
end

def store(struct, expires_in: 60)
check_for_id_property!(struct.class)
check_for_empty_id!(struct.id)

payload = struct.as_json
payload.delete('id')

REDIS_POOL.with do |client|
client.write(
key(struct.id, type: struct.class),
Encryption::Encryptors::SessionEncryptor.new.encrypt(payload.to_json),
expires_in: expires_in,
)
end
end

def key(id, type:)
return [type.redis_key_prefix, id].join(':') if type.respond_to?(:redis_key_prefix)
raise "#{self} expected #{type.name} to have defined class method redis_key_prefix"
end

# Assigns member fields from a hash. That way, it doesn't matter
# if a Struct was created with keyword_init or not (and we can't currently
# reflect on that field)
# @param [Struct] struct
# @param [Hash] data
def init_fields(struct:, data:)
data.slice(*struct.members).each do |key, value|
struct[key] = value
end
end

def check_for_id_property!(type)
return if type.members.include?(:id)
raise "#{self} expected #{type.name} to have an id property"
end

def check_for_empty_id!(id)
raise ArgumentError, 'id cannot be empty' if id.blank?
end
end
9 changes: 7 additions & 2 deletions app/services/flow/flow_state_machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,15 @@ def issuer

def fsm_initialize
klass = self.class
flow = klass::FSM_SETTINGS[:flow]
@name = klass.name.underscore.gsub('_controller', '')
klass::FSM_SETTINGS.each { |key, value| instance_variable_set("@#{key}", value) }
@step_url = klass::FSM_SETTINGS[:step_url]
@final_url = klass::FSM_SETTINGS[:final_url]
@analytics_id = klass::FSM_SETTINGS[:analytics_id]
@view = klass::FSM_SETTINGS[:view]

current_session[@name] ||= {}
@flow = @flow.new(self, current_session, @name)
@flow = flow.new(self, current_session, @name)
end

def render_update(step, result)
Expand Down
3 changes: 0 additions & 3 deletions app/services/idv/session.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module Idv
class Session
VALID_SESSION_ATTRIBUTES = %i[
async_result_id
async_result_started_at
address_verification_mechanism
applicant
vendor_phone_confirmation
Expand All @@ -15,7 +13,6 @@ class Session
profile_step_params
personal_key
resolution_successful
selected_jurisdiction
step_attempts
].freeze

Expand Down
14 changes: 9 additions & 5 deletions app/services/idv/steps/verify_base_step.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ class VerifyBaseStep < DocAuthBaseStep
def perform_resolution_and_check_ssn
pii_from_doc = flow_session[:pii_from_doc]
# do resolution first to prevent ssn time/discovery. resolution time order > than db call
result = perform_resolution(pii_from_doc)
result = check_ssn(pii_from_doc) if result.success?
summarize_result_and_throttle_failures(result)
idv_result = perform_resolution(pii_from_doc)
add_proofing_costs(idv_result)
response = idv_result_to_form_response(idv_result)
response = check_ssn(pii_from_doc) if response.success?
summarize_result_and_throttle_failures(response)
end

def summarize_result_and_throttle_failures(summary_result)
Expand Down Expand Up @@ -53,8 +55,10 @@ def skip_legacy_steps

def perform_resolution(pii_from_doc)
stages = should_use_aamva?(pii_from_doc) ? %i[resolution state_id] : [:resolution]
idv_result = Idv::Agent.new(pii_from_doc).proof(*stages)
add_proofing_costs(idv_result)
Idv::Agent.new(pii_from_doc).proof(*stages)
end

def idv_result_to_form_response(idv_result)
FormResponse.new(
success: idv_success(idv_result),
errors: idv_errors(idv_result),
Expand Down
19 changes: 18 additions & 1 deletion app/services/idv/steps/verify_step.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
module Idv
module Steps
class VerifyStep < VerifyBaseStep
def call; end
def call
enqueue_job
end

private

def enqueue_job
pii_from_doc = flow_session[:pii_from_doc]

document_capture_session = DocumentCaptureSession.create(user_id: user_id,
requested_at: Time.zone.now)
document_capture_session.store_proofing_pii_from_doc(pii_from_doc)

flow_session[:idv_verify_step_document_capture_session_uuid] = document_capture_session.uuid

stages = should_use_aamva?(pii_from_doc) ? %w[resolution state_id] : ['resolution']
VendorProofJob.perform(document_capture_session.uuid, stages)
end
end
end
end
70 changes: 65 additions & 5 deletions app/services/idv/steps/verify_wait_step_show.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,76 @@
module Idv
module Steps
class VerifyWaitStepShow < VerifyBaseStep
State = Struct.new(:status, :pii, :result, keyword_init: true) do
def self.none
new(status: :none)
end

def self.timed_out
new(status: :timed_out)
end

def self.in_progress
new(status: :in_progress)
end

def self.done(pii:, result:)
new(status: :done, pii: pii, result: result)
end

private_class_method :new
end

def call
poll_with_meta_refresh(Figaro.env.poll_rate_for_verify_in_seconds.to_i)
result = perform_resolution_and_check_ssn
if result.success?
mark_step_complete(:verify_wait)
else
# return if result says continue to wait else...

current_async_state = async_state

case current_async_state.status
when :none
mark_step_incomplete(:verify)
when :in_progress
nil
when :timed_out
mark_step_incomplete(:verify)
when :done
add_proofing_costs(current_async_state.result)
response = idv_result_to_form_response(current_async_state.result)
response = check_ssn(current_async_state.pii) if response.success?
summarize_result_and_throttle_failures(response)

if response.success?
delete_async
mark_step_complete(:verify_wait)
else
mark_step_incomplete(:verify)
end
end
end

private

def async_state
dcs_uuid = flow_session[:idv_verify_step_document_capture_session_uuid]
dcs = DocumentCaptureSession.find_by(uuid: dcs_uuid)
return State.none if dcs_uuid.nil?
return State.timed_out if dcs.nil?

proofing_job_result = dcs.load_proofing_result
return State.timed_out if proofing_job_result.nil?

if proofing_job_result.result
proofing_job_result.result.deep_symbolize_keys!
proofing_job_result.pii.deep_symbolize_keys!
State.done(pii: proofing_job_result.pii, result: proofing_job_result.result)
elsif proofing_job_result.pii
State.in_progress
end
end

def delete_async
flow_session.delete(:idv_verify_step_document_capture_session_uuid)
end
end
end
end
7 changes: 7 additions & 0 deletions app/services/proofing_document_capture_session_result.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

ProofingDocumentCaptureSessionResult = Struct.new(:id, :pii, :result, keyword_init: true) do
def self.redis_key_prefix
'dcs-proofing:result'
end
end
Loading