Skip to content

Commit

Permalink
[DPC-4390] Log organization credential status via Sidekiq (#2327)
Browse files Browse the repository at this point in the history
## 🎫 Ticket
[DPC-4390](https://jira.cms.gov/browse/DPC-4390)

## 🛠 Changes
- added new sidekiq job to call api and get status of organization's api
access
- log credential status for ea organization
- log aggregate stats of total organizations with/without api access

<!-- What was added, updated, or removed in this PR? -->

## ℹ️ Context
- this ticket was created in order to enable
[DPC-4381](https://jira.cms.gov/browse/DPC-4381)

<!-- Why were these changes made? Add background context suitable for a
non-technical audience. -->

<!-- If any of the following security implications apply, this PR must
not be merged without Stephen Walter's approval. Explain in this section
and add @SJWalter11 as a reviewer.
  - Adds a new software dependency or dependencies.
  - Modifies or invalidates one or more of our security controls.
  - Stores or transmits data that was not stored or transmitted before.
- Requires additional review of security implications for other reasons.
-->

## 🧪 Validation

<!-- How were the changes verified? Did you fully test the acceptance
criteria in the ticket? Provide reproducible testing instructions and
screenshots if applicable. -->
 - see dpc-portal/spec/jobs/log_organizations_access_spec.rb
  • Loading branch information
lukey-luke authored Nov 19, 2024
1 parent ca9ebf9 commit ba65696
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 0 deletions.
53 changes: 53 additions & 0 deletions dpc-portal/app/jobs/log_organizations_api_credential_status_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

# A background job that determines the number of active Provider Organizations that
# have access to make API calls.
# This is determined by checking for tokens, public keys, and ip addresses for all
# organizations that are in the portal with completed Terms of Service agreement.
class LogOrganizationsApiCredentialStatusJob < ApplicationJob
queue_as :portal

def perform
@start = Time.now
organizations_credential_aggregate_status = {
have_active_credentials: 0,
have_incomplete_or_no_credentials: 0
}
ProviderOrganization.where.not(terms_of_service_accepted_by: nil).find_each do |organization|
credential_status = fetch_credential_status(organization.dpc_api_organization_id)
Rails.logger.info(['Credential status for organization',
{ name: organization.name,
dpc_api_org_id: organization.dpc_api_organization_id,
credential_status: }])
update_organization_aggregate_hash(organizations_credential_aggregate_status, credential_status)
end
Rails.logger.info(['Organizations API credential status', organizations_credential_aggregate_status])
end

def update_organization_aggregate_hash(aggregate_stats, credential_status)
if credential_status[:num_tokens].zero? || credential_status[:num_keys].zero? || credential_status[:num_ips].zero?
aggregate_stats[:have_incomplete_or_no_credentials] += 1
else
aggregate_stats[:have_active_credentials] += 1
end
aggregate_stats
end

def fetch_credential_status(organization_id)
tokens = dpc_client.get_client_tokens(organization_id)
current_datetime = DateTime.now
active_tokens = tokens['entities'].select { |tok| tok['expiresAt'] > current_datetime }
pub_keys = dpc_client.get_public_keys(organization_id)
ip_addresses = dpc_client.get_ip_addresses(organization_id)

{
num_tokens: active_tokens.length,
num_keys: pub_keys['count'],
num_ips: ip_addresses['count']
}
end

def dpc_client
@dpc_client ||= DpcClient.new
end
end
5 changes: 5 additions & 0 deletions dpc-portal/config/schedule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ verify_non_dpc_health_job:
check_dpc: false
check_cpi: true
check_idp: true

log_organizations_api_credential_status_job:
cron: "0 0 * * *"
class: "LogOrganizationsApiCredentialStatusJob"
queue: portal
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe LogOrganizationsApiCredentialStatusJob, type: :job do
include ActiveJob::TestHelper

let(:mock_dpc_client) { instance_double(DpcClient) }
before do
allow(DpcClient).to receive(:new).and_return(mock_dpc_client)
end

let(:user) do
create(:user, provider: :openid_connect, uid: '12345',
verification_status: 'rejected', verification_reason: 'ao_med_sanctions')
end
let(:provider_organization) do
create(
:provider_organization,
name: 'Test',
dpc_api_organization_id: 'foo',
terms_of_service_accepted_by: user,
terms_of_service_accepted_at: 1.day.ago
)
end
let(:mock_no_tokens_response) do
{
'entities' => [],
'count' => 0,
'created_at' => '2024-11-19T16:52:49.760+00:00'
}
end
let(:mock_one_token_response) do
{
'entities' => [
{
'id' => 'f5b5559d-17b1-4951-a704-2f74b9b8587f',
'tokenType' => 'MACAROON',
'label' => 'Token for organization 46ac7ad6-7487-4dd0-baa0-6e2c8cae76a0.',
'createdAt' => '2024-08-14T16:51:10.702+00:00',
'expiresAt' => '2025-08-14T16:51:10.687+00:00'
}
],
'count' => 1,
'created_at' => '2024-11-19T16:52:49.760+00:00'
}
end

describe 'perform' do
it 'organization has no api credentials' do
provider_organization.save!

expect(mock_dpc_client).to receive(:get_client_tokens).and_return(mock_no_tokens_response).once
expect(mock_dpc_client).to receive(:get_public_keys).and_return({ 'count' => 0 }).once
expect(mock_dpc_client).to receive(:get_ip_addresses).and_return({ 'count' => 0 }).once
allow(Rails.logger).to receive(:info)
expect(Rails.logger).to receive(:info).with(['Organizations API credential status',
{ have_active_credentials: 0,
have_incomplete_or_no_credentials: 1 }])

described_class.perform_now
end
end
it 'updates log with 1 organization that has all 3 credentials' do
provider_organization.save!

expect(mock_dpc_client).to receive(:get_client_tokens).and_return(mock_one_token_response).once
expect(mock_dpc_client).to receive(:get_public_keys).and_return({ 'count' => 2 }).once
expect(mock_dpc_client).to receive(:get_ip_addresses).and_return({ 'count' => 3 }).once
allow(Rails.logger).to receive(:info)
expect(Rails.logger).to receive(:info).with(['Organizations API credential status',
{ have_active_credentials: 1,
have_incomplete_or_no_credentials: 0 }])

described_class.perform_now
end
it 'updates log with 1 organization that has partial credentials' do
provider_organization.save!

expect(mock_dpc_client).to receive(:get_client_tokens).and_return(mock_one_token_response).once
expect(mock_dpc_client).to receive(:get_public_keys).and_return({ 'count' => 2 }).once
expect(mock_dpc_client).to receive(:get_ip_addresses).and_return({ 'count' => 0 }).once
allow(Rails.logger).to receive(:info)
expect(Rails.logger).to receive(:info).with(['Organizations API credential status',
{ have_active_credentials: 0,
have_incomplete_or_no_credentials: 1 }])

described_class.perform_now
end
end

0 comments on commit ba65696

Please sign in to comment.