-
Notifications
You must be signed in to change notification settings - Fork 166
Add ability to read events from the IRS Attempts API #6375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b80130e
13d9145
598e970
fb3752e
382346a
a2417cd
d4e5b73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| ## | ||
| # This controller implements the Poll-based delivery method for Security Event | ||
| # Tokens as described RFC 8936 | ||
| # | ||
| # ref: https://datatracker.ietf.org/doc/html/rfc8936 | ||
| # | ||
| module Api | ||
| class IrsAttemptsApiController < ApplicationController | ||
| before_action :render_404_if_disabled | ||
| before_action :authenticate_client | ||
|
|
||
| respond_to :json | ||
|
|
||
| def create | ||
| acknowledge_acked_events | ||
| render json: { sets: security_event_tokens } | ||
| analytics.irs_attempts_api_events(**analytics_properties) | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def render_404_if_disabled | ||
| render_not_found unless IdentityConfig.store.irs_attempt_api_enabled | ||
| end | ||
|
|
||
| def authenticate_client | ||
| bearer, token = request.authorization.split(' ', 2) | ||
| if bearer != 'Bearer' || !valid_auth_tokens.include?(token) | ||
| render json: { status: 401, description: 'Unauthorized' }, status: :unauthorized | ||
| end | ||
| end | ||
|
|
||
| def acknowledge_acked_events | ||
| redis_client.delete_events(ack_event_ids) | ||
| end | ||
|
|
||
| def ack_event_ids | ||
| params['ack'] || [] | ||
| end | ||
|
|
||
| def requested_event_count | ||
| count = if params['maxEvents'] | ||
| params['maxEvents'].to_i | ||
| else | ||
| IdentityConfig.store.irs_attempt_api_event_count_default | ||
| end | ||
|
|
||
| [count, IdentityConfig.store.irs_attempt_api_event_count_max].min | ||
| end | ||
|
|
||
| def security_event_tokens | ||
| @security_event_tokens ||= redis_client.read_events(requested_event_count) | ||
| end | ||
|
|
||
| def security_event_token_errors | ||
| return unless params['setErrs'].present? | ||
| params['setErrs'].to_json | ||
| end | ||
|
|
||
| def redis_client | ||
| @redis_client ||= IrsAttemptsApi::RedisClient.new | ||
| end | ||
|
|
||
| def valid_auth_tokens | ||
| IdentityConfig.store.irs_attempt_api_auth_tokens | ||
| end | ||
|
|
||
| def analytics_properties | ||
| { | ||
| acknowledged_event_count: ack_event_ids.count, | ||
| rendered_event_count: security_event_tokens.keys.count, | ||
| set_errors: security_event_token_errors, | ||
| } | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| require 'rails_helper' | ||
|
|
||
| RSpec.describe Api::IrsAttemptsApiController do | ||
| before do | ||
| stub_analytics | ||
|
|
||
| allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true) | ||
|
|
||
| IrsAttemptsApi::RedisClient.clear_attempts! | ||
| existing_events | ||
|
|
||
| request.headers['Authorization'] = "Bearer #{auth_token}" | ||
| end | ||
|
|
||
| let(:auth_token) do | ||
| IdentityConfig.store.irs_attempt_api_auth_tokens.first | ||
| end | ||
| let(:existing_events) do | ||
| 3.times.map do | ||
| jti, jwe = IrsAttemptsApi::EncryptedEventTokenBuilder.new( | ||
| event_type: :test_event, | ||
| session_id: 'test-session-id', | ||
| occurred_at: Time.zone.now, | ||
| event_metadata: {}, | ||
| ).build_event_token | ||
| IrsAttemptsApi::RedisClient.new.write_event(jti: jti, jwe: jwe) | ||
| [jti, jwe] | ||
| end | ||
| end | ||
| let(:existing_event_jtis) { existing_events.map(&:first) } | ||
|
|
||
| describe '#create' do | ||
| it 'renders a 404 if disabled' do | ||
| allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(false) | ||
|
|
||
| post :create, params: {} | ||
|
|
||
| expect(response.status).to eq(404) | ||
| end | ||
|
|
||
| it 'authenticates the client' do | ||
| request.headers['Authorization'] = auth_token # Missing Bearer prefix | ||
|
|
||
| post :create, params: {} | ||
|
|
||
| expect(response.status).to eq(401) | ||
|
|
||
| request.headers['Authorization'] = 'garbage-fake-token-nobody-likes' | ||
|
|
||
| post :create, params: {} | ||
|
|
||
| expect(response.status).to eq(401) | ||
| end | ||
|
|
||
| it 'allows events to be acknowledged' do | ||
| post :create, params: { ack: existing_event_jtis } | ||
|
|
||
| expect(response).to be_ok | ||
|
|
||
| expected_response = { | ||
| 'sets' => {}, | ||
| } | ||
| expect(JSON.parse(response.body)).to eq(expected_response) | ||
| expect(IrsAttemptsApi::RedisClient.new.read_events).to be_empty | ||
| expect(@analytics).to have_logged_event( | ||
| 'IRS Attempt API: Events submitted', | ||
| acknowledged_event_count: 3, | ||
| rendered_event_count: 0, | ||
| set_errors: nil, | ||
| ) | ||
| end | ||
|
|
||
| it 'renders new events' do | ||
| post :create, params: {} | ||
|
|
||
| expect(response).to be_ok | ||
|
|
||
| expected_response = { | ||
| 'sets' => existing_events.to_h, | ||
| } | ||
| expect(JSON.parse(response.body)).to eq(expected_response) | ||
| expect(@analytics).to have_logged_event( | ||
| 'IRS Attempt API: Events submitted', | ||
| acknowledged_event_count: 0, | ||
| rendered_event_count: 3, | ||
| set_errors: nil, | ||
| ) | ||
| end | ||
|
|
||
| it 'logs errors from the client' do | ||
| set_errors = { abc123: { description: 'it is b0rken' } } | ||
|
|
||
| post :create, params: { setErrs: set_errors } | ||
|
|
||
| expect(response).to be_ok | ||
| expect(@analytics).to have_logged_event( | ||
| 'IRS Attempt API: Events submitted', | ||
| acknowledged_event_count: 0, | ||
| rendered_event_count: 3, | ||
| set_errors: set_errors.to_json, | ||
| ) | ||
| end | ||
|
|
||
| it 'respects the maxEvents param' do | ||
| post :create, params: { maxEvents: 2 } | ||
|
|
||
| expect(response).to be_ok | ||
| expect(JSON.parse(response.body)['sets'].keys.length).to eq(2) | ||
| end | ||
|
|
||
| it 'does not render more than the configured maximum event allowance' do | ||
| allow(IdentityConfig.store).to receive(:irs_attempt_api_event_count_max).and_return(2) | ||
|
|
||
| post :create, params: { maxEvents: 5 } | ||
|
|
||
| expect(response).to be_ok | ||
| expect(JSON.parse(response.body)['sets'].keys.length).to eq(2) | ||
| end | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||||||||||||||
| require 'rails_helper' | ||||||||||||||||||
|
|
||||||||||||||||||
| RSpec.describe 'IRS attempts API' do | ||||||||||||||||||
| before do | ||||||||||||||||||
| allow(IdentityConfig.store).to receive(:irs_attempt_api_enabled).and_return(true) | ||||||||||||||||||
| IrsAttemptsApi::RedisClient.clear_attempts! | ||||||||||||||||||
| events_to_acknowledge | ||||||||||||||||||
| events_to_render | ||||||||||||||||||
| end | ||||||||||||||||||
|
|
||||||||||||||||||
| let(:events_to_acknowledge) do | ||||||||||||||||||
|
||||||||||||||||||
| events_to_acknowledge | |
| events_to_render | |
| end | |
| let(:events_to_acknowledge) do | |
| end | |
| let!(:events_to_acknowledge) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so this gets tricky because of the .clear_attempts! call in the before action. That runs after the let! block so the events get created and then immediately destroyed 😭
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the logs are JSON, to save ourselves a JSON parse later, should this just be the JSON array or hash or whatever?
Also is this accepting data wholesale from partners into our logs? I'm a little wary of giving partners freeform access to write to our logs because PII may end up there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, yeah I hadn't considered PII but was nervous about just dropping stuff in here. Not super sure how to approach it cause I imagine we do want to be able to see these errors.