From ba65696afb20b7fc8469b0ea5ceb3870470b8e0e Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 19 Nov 2024 13:35:16 -0800 Subject: [PATCH] [DPC-4390] Log organization credential status via Sidekiq (#2327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 🎫 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 ## ℹ️ Context - this ticket was created in order to enable [DPC-4381](https://jira.cms.gov/browse/DPC-4381) ## 🧪 Validation - see dpc-portal/spec/jobs/log_organizations_access_spec.rb --- ...organizations_api_credential_status_job.rb | 53 +++++++++++ dpc-portal/config/schedule.yml | 5 ++ ...rganizations_api_credential_status_spec.rb | 90 +++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 dpc-portal/app/jobs/log_organizations_api_credential_status_job.rb create mode 100644 dpc-portal/spec/jobs/log_organizations_api_credential_status_spec.rb diff --git a/dpc-portal/app/jobs/log_organizations_api_credential_status_job.rb b/dpc-portal/app/jobs/log_organizations_api_credential_status_job.rb new file mode 100644 index 0000000000..0e5df0a6d4 --- /dev/null +++ b/dpc-portal/app/jobs/log_organizations_api_credential_status_job.rb @@ -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 diff --git a/dpc-portal/config/schedule.yml b/dpc-portal/config/schedule.yml index 510494c1ba..4fdc2edb33 100644 --- a/dpc-portal/config/schedule.yml +++ b/dpc-portal/config/schedule.yml @@ -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 diff --git a/dpc-portal/spec/jobs/log_organizations_api_credential_status_spec.rb b/dpc-portal/spec/jobs/log_organizations_api_credential_status_spec.rb new file mode 100644 index 0000000000..dc3a7fbaeb --- /dev/null +++ b/dpc-portal/spec/jobs/log_organizations_api_credential_status_spec.rb @@ -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