diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ed114631686..7a1333fb302 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -83,6 +83,11 @@ def analytics_user def irs_attempts_api_tracker @irs_attempts_api_tracker ||= IrsAttemptsApi::Tracker.new( session_id: irs_attempts_api_session_id, + request: request, + user: effective_user, + sp: current_sp, + device_fingerprint: cookies[:device], + sp_request_uri: decorated_session.request_url_params[:redirect_uri], enabled_for_session: irs_attempt_api_enabled_for_session?, ) end diff --git a/app/decorators/session_decorator.rb b/app/decorators/session_decorator.rb index 828fe0b6ad4..9af1f7e155f 100644 --- a/app/decorators/session_decorator.rb +++ b/app/decorators/session_decorator.rb @@ -47,6 +47,10 @@ def requested_more_recent_verification? def irs_attempts_api_session_id; end + def request_url_params + {} + end + private attr_reader :view_context diff --git a/app/services/agency_identity_linker.rb b/app/services/agency_identity_linker.rb index 25b14bba89e..28467bff9c8 100644 --- a/app/services/agency_identity_linker.rb +++ b/app/services/agency_identity_linker.rb @@ -9,6 +9,22 @@ def link_identity AgencyIdentity.new(user_id: @sp_identity.user_id, uuid: @sp_identity.uuid) end + # @return [AgencyIdentity, ServiceProviderIdentity] the AgencyIdentity for this user at this + # service provider or falls back to the ServiceProviderIdentity if one does not exist. + def self.for(user:, service_provider:) + agency = service_provider.agency + + ai = AgencyIdentity.where(user: user, agency: agency).take + return ai if ai.present? + + spi = ServiceProviderIdentity.where( + user: user, service_provider: service_provider.issuer, + ).take + + return nil unless spi.present? + new(spi).link_identity + end + def self.sp_identity_from_uuid_and_sp(uuid, service_provider) ai = AgencyIdentity.where(uuid: uuid).take criteria = if ai diff --git a/app/services/irs_attempts_api/tracker.rb b/app/services/irs_attempts_api/tracker.rb index 56837b29898..b9f29bbe02e 100644 --- a/app/services/irs_attempts_api/tracker.rb +++ b/app/services/irs_attempts_api/tracker.rb @@ -1,20 +1,36 @@ module IrsAttemptsApi class Tracker - attr_reader :session_id, :enabled_for_session - - def initialize(session_id:, enabled_for_session:) - @session_id = session_id + attr_reader :session_id, :enabled_for_session, :request, :user, :sp, :device_fingerprint, + :sp_request_uri + + def initialize(session_id:, request:, user:, sp:, device_fingerprint:, + sp_request_uri:, enabled_for_session:) + @session_id = session_id # IRS session ID + @request = request + @user = user + @sp = sp + @device_fingerprint = device_fingerprint + @sp_request_uri = sp_request_uri @enabled_for_session = enabled_for_session end def track_event(event_type, metadata = {}) return unless enabled? + event_metadata = { + user_agent: request&.user_agent, + unique_session_id: hashed_session_id, + user_uuid: AgencyIdentityLinker.for(user: user, service_provider: sp)&.uuid, + device_fingerprint: device_fingerprint, + user_ip_address: request&.remote_ip, + irs_application_url: sp_request_uri, + }.merge(metadata) + event = AttemptEvent.new( event_type: event_type, session_id: session_id, occurred_at: Time.zone.now, - event_metadata: metadata, + event_metadata: event_metadata, ) redis_client.write_event(jti: event.jti, jwe: event.to_jwe) @@ -25,6 +41,11 @@ def track_event(event_type, metadata = {}) private + def hashed_session_id + return nil unless user&.unique_session_id + Digest::SHA1.hexdigest(user&.unique_session_id) + end + def enabled? IdentityConfig.store.irs_attempt_api_enabled && @enabled_for_session end diff --git a/spec/services/agency_identity_linker_spec.rb b/spec/services/agency_identity_linker_spec.rb index 58e0f5c4d27..ab9b736aac8 100644 --- a/spec/services/agency_identity_linker_spec.rb +++ b/spec/services/agency_identity_linker_spec.rb @@ -6,8 +6,8 @@ before(:each) { init_env(user) } it 'links identities from 2 sps' do - sp1 = create_identity(user, 'http://localhost:3000', 'UUID1') - create_identity(user, 'urn:gov:gsa:openidconnect:test', 'UUID2') + sp1 = create_service_provider_identity(user, 'http://localhost:3000', 'UUID1') + create_service_provider_identity(user, 'urn:gov:gsa:openidconnect:test', 'UUID2') ai = AgencyIdentityLinker.new(sp1).link_identity expect(ai.uuid).to eq('UUID1') ai = AgencyIdentity.where(user_id: user.id).first @@ -15,20 +15,20 @@ end it 'does not allow 2 sp uuids to be reused after user deletes account' do - create_identity(user, 'http://localhost:3000', 'UUID1') - create_identity(user, 'urn:gov:gsa:openidconnect:test', 'UUID2') + create_service_provider_identity(user, 'http://localhost:3000', 'UUID1') + create_service_provider_identity(user, 'urn:gov:gsa:openidconnect:test', 'UUID2') user.destroy! expect(User.where(id: user.id).count).to eq(0) user2 = create(:user) - expect { create_identity(user2, 'sp3', 'UUID1') }. + expect { create_service_provider_identity(user2, 'sp3', 'UUID1') }. to raise_error ActiveRecord::RecordNotUnique - expect { create_identity(user2, 'sp4', 'UUID2') }. + expect { create_service_provider_identity(user2, 'sp4', 'UUID2') }. to raise_error ActiveRecord::RecordNotUnique end it 'does not allow agency_identity uuid to be reused after user deletes account' do - sp1 = create_identity(user, 'http://localhost:3000', 'UUID1') - create_identity(user, 'urn:gov:gsa:openidconnect:test', 'UUID2') + sp1 = create_service_provider_identity(user, 'http://localhost:3000', 'UUID1') + create_service_provider_identity(user, 'urn:gov:gsa:openidconnect:test', 'UUID2') AgencyIdentityLinker.new(sp1).link_identity expect(AgencyIdentity.where(user_id: user.id).count).to eq(1) expect(AgencyIdentity.where(uuid: 'UUID1').count).to eq(1) @@ -37,14 +37,14 @@ expect(AgencyIdentity.where(user_id: user.id).count).to eq(0) expect(AgencyIdentity.where(uuid: 'UUID1').count).to eq(0) user2 = create(:user) - expect { create_identity(user2, 'sp3', 'UUID1') }. + expect { create_service_provider_identity(user2, 'sp3', 'UUID1') }. to raise_error ActiveRecord::RecordNotUnique - expect { create_identity(user2, 'sp4', 'UUID2') }. + expect { create_service_provider_identity(user2, 'sp4', 'UUID2') }. to raise_error ActiveRecord::RecordNotUnique end it 'links identity with 1 sp' do - sp1 = create_identity(user, 'http://localhost:3000', 'UUID1') + sp1 = create_service_provider_identity(user, 'http://localhost:3000', 'UUID1') ai = AgencyIdentityLinker.new(sp1).link_identity expect(ai.uuid).to eq('UUID1') ai = AgencyIdentity.where(user_id: user.id).first @@ -52,7 +52,7 @@ end it 'does not link identity without an agency_id' do - sp1 = create_identity(user, 'sp:with:no:agency_id', 'UUID1') + sp1 = create_service_provider_identity(user, 'sp:with:no:agency_id', 'UUID1') ai = AgencyIdentityLinker.new(sp1).link_identity expect(ai.agency_id).to eq(nil) expect(ai.uuid).to eq('UUID1') @@ -61,7 +61,7 @@ end it 'returns the existing agency_identity if it exists' do - sp1 = create_identity(user, 'http://localhost:3000', 'UUID1') + sp1 = create_service_provider_identity(user, 'http://localhost:3000', 'UUID1') ai = AgencyIdentityLinker.new(sp1).link_identity expect(ai.uuid).to eq('UUID1') ai = AgencyIdentity.where(user_id: user.id).first @@ -73,7 +73,7 @@ before(:each) { init_env(user) } it 'returns sp_identity if it exists' do - create_identity(user, 'http://localhost:3000', 'UUID1') + create_service_provider_identity(user, 'http://localhost:3000', 'UUID1') AgencyIdentity.create(user_id: user.id, agency_id: 1, uuid: 'UUID2') sp_identity = AgencyIdentityLinker.sp_identity_from_uuid_and_sp( 'UUID2', @@ -92,12 +92,60 @@ end end + describe '#for' do + before(:each) { init_env(user) } + let(:sp) { create(:service_provider) } + let(:agency) { sp.agency } + let(:uuid) { SecureRandom.uuid } + + subject { described_class.for(user: user, service_provider: sp) } + + context 'when there is already an agency identity' do + before { create_agency_identity(user, agency, uuid) } + + it 'returns the existing agency identity' do + expect(subject).not_to be_nil + expect(subject.user_id).to eq user.id + expect(subject.agency_id).to eq agency.id + expect(subject.uuid).to eq uuid + end + end + + context 'when there is not an agency identity' do + context 'but there is a service provider identity' do + before { create_service_provider_identity(user, sp.issuer, uuid) } + it 'returns the service provider identity' do + expect(subject).not_to be_nil + expect(subject.user_id).to eq user.id + expect(subject.agency_id).to eq agency.id + expect(subject.uuid).to eq uuid + end + + it 'persists the service provider identity as an agency identity' do + expect(subject.uuid).to eq uuid + ai = AgencyIdentity.where(user: user, agency: agency).take + expect(subject).to eq ai + end + end + + context 'and there is no service provider identity' do + it 'returns nil' do + expect(subject).to be_nil + end + end + end + end + def init_env(user) ServiceProviderIdentity.where(user_id: user.id).delete_all AgencyIdentity.where(user_id: user.id).delete_all end - def create_identity(user, service_provider, uuid) + def create_service_provider_identity(user, service_provider, uuid) ServiceProviderIdentity.create(user_id: user.id, service_provider: service_provider, uuid: uuid) end + + def create_agency_identity(user, agency, uuid) + AgencyIdentity.create!(user: user, agency: agency, uuid: uuid) + end end diff --git a/spec/services/irs_attempts_api/tracker_spec.rb b/spec/services/irs_attempts_api/tracker_spec.rb index 0f6afe40909..5bb27dc40f0 100644 --- a/spec/services/irs_attempts_api/tracker_spec.rb +++ b/spec/services/irs_attempts_api/tracker_spec.rb @@ -5,13 +5,30 @@ allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return( irs_attempt_api_enabled, ) + allow(request).to receive(:user_agent).and_return('example/1.0') + allow(request).to receive(:remote_ip).and_return('192.0.2.1') end let(:irs_attempt_api_enabled) { true } let(:session_id) { 'test-session-id' } let(:enabled_for_session) { true } + let(:request) { instance_double(ActionDispatch::Request) } + let(:service_provider) { create(:service_provider) } + let(:device_fingerprint) { 'device_id' } + let(:sp_request_uri) { 'https://example.com/auth_page' } + let(:user) { create(:user) } - subject { described_class.new(session_id: session_id, enabled_for_session: enabled_for_session) } + subject do + described_class.new( + session_id: session_id, + request: request, + user: user, + sp: service_provider, + device_fingerprint: device_fingerprint, + sp_request_uri: sp_request_uri, + enabled_for_session: enabled_for_session, + ) + end describe '#track_event' do it 'records the event in redis' do