18F/identity-idp

View on GitHub
lib/data_pull.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

require_relative './script_base'

class DataPull
  attr_reader :argv, :stdout, :stderr

  def initialize(argv:, stdout:, stderr:)
    @argv = argv
    @stdout = stdout
    @stderr = stderr
  end

  def script_base
    @script_base ||= ScriptBase.new(
      argv:,
      stdout:,
      stderr:,
      subtask_class: subtask(argv.shift),
      banner: banner,
      reason_arg: false,
    )
  end

  def run
    script_base.run
  end

  def banner
    basename = File.basename($PROGRAM_NAME)
    <<~EOS
      #{basename} [subcommand] [arguments] [options]

      Example usage:

        * #{basename} email-lookup uuid1 uuid2

        * #{basename} events-summary uuid1 uuid2

        * #{basename} ig-request uuid1 uuid2 --requesting-issuer=ABC:DEF:GHI

        * #{basename} profile-summary uuid1 uuid2

        * #{basename} uuid-convert partner-uuid1 partner-uuid2

        * #{basename} uuid-export uuid1 uuid2 --requesting-issuer=ABC:DEF:GHI

        * #{basename} uuid-lookup email1@example.com email2@example.com
      Options:
    EOS
  end

  # @api private
  # A subtask is a class that has a run method, the type signature should look like:
  # +#run(args: Array<String>, config: Config) -> Result+
  # @return [Class,nil]
  def subtask(name)
    {
      'email-lookup' => EmailLookup,
      'events-summary' => EventsSummary,
      'ig-request' => InspectorGeneralRequest,
      'profile-summary' => ProfileSummary,
      'uuid-convert' => UuidConvert,
      'uuid-export' => UuidExport,
      'uuid-lookup' => UuidLookup,
    }[name]
  end

  class UuidLookup
    def run(args:, config:)
      emails = args

      table = []
      table << %w[email uuid]

      uuids = []

      emails.each do |email|
        user = User.find_with_email(email)
        if user
          table << [email, user.uuid]
          uuids << user.uuid
        elsif config.include_missing?
          table << [email, '[NOT FOUND]']
        end
      end

      ScriptBase::Result.new(
        subtask: 'uuid-lookup',
        table:,
        uuids:,
      )
    end
  end

  class UuidConvert
    def run(args:, config:)
      partner_uuids = args

      table = []
      table << %w[partner_uuid source internal_uuid deleted]
      identities = ServiceProviderIdentity.
        includes(:user, :deleted_user, :agency).
        where(uuid: partner_uuids).
        order(:uuid)

      identities.each do |identity|
        table << [
          identity.uuid,
          identity.agency.name,
          (identity.user || identity.deleted_user).uuid,
          identity.deleted_user ? true : nil,
        ]
      end

      if config.include_missing?
        (partner_uuids - identities.map(&:uuid)).each do |missing_uuid|
          table << [missing_uuid, '[NOT FOUND]', '[NOT FOUND]', nil]
        end
      end

      ScriptBase::Result.new(
        subtask: 'uuid-convert',
        uuids: identities.map { |u| (u.user || u.deleted_user).uuid },
        table:,
      )
    end
  end

  class EmailLookup
    def run(args:, config:)
      uuids = args

      users = User.includes(:email_addresses).where(uuid: uuids).order(:uuid)

      table = []
      table << %w[uuid email confirmed_at]

      users.each do |user|
        user.email_addresses.sort_by(&:id).each do |email_address|
          table << [user.uuid, email_address.email, email_address.confirmed_at]
        end
      end

      if config.include_missing?
        (uuids - users.map(&:uuid)).each do |missing_uuid|
          table << [missing_uuid, '[NOT FOUND]', nil]
        end
      end

      ScriptBase::Result.new(
        subtask: 'email-lookup',
        uuids: users.map(&:uuid),
        table:,
      )
    end
  end

  class InspectorGeneralRequest
    def run(args:, config:)
      require 'data_requests/deployed'
      ActiveRecord::Base.connection.execute('SET statement_timeout = 0')
      uuids = args

      users, missing_uuids = uuids.map do |uuid|
        DataRequests::Deployed::LookupUserByUuid.new(uuid).call || uuid
      end.partition { |u| u.is_a?(User) }

      shared_device_users = DataRequests::Deployed::LookupSharedDeviceUsers.new(users).call

      output = shared_device_users.map do |user|
        DataRequests::Deployed::CreateUserReport.new(user, config.requesting_issuers).call
      end

      if config.include_missing?
        output += missing_uuids.map do |uuid|
          {
            user_id: nil,
            login_uuid: nil,
            requesting_issuer_uuid: uuid,
            email_addresses: [],
            mfa_configurations: {
              phone_configurations: [],
              auth_app_configurations: [],
              webauthn_configurations: [],
              piv_cac_configurations: [],
              backup_code_configurations: [],
            },
            user_events: [],
            not_found: true,
          }
        end
      end

      ScriptBase::Result.new(
        subtask: 'ig-request',
        uuids: users.map(&:uuid),
        json: output,
      )
    end
  end

  class ProfileSummary
    def run(args:, config:)
      uuids = args

      users = User.includes(:profiles).where(uuid: uuids).order(:uuid)

      table = []
      table << %w[
        uuid
        profile_id
        status
        activated_timestamp
        disabled_reason
        gpo_verification_pending_timestamp
        fraud_review_pending_timestamp
        fraud_rejection_timestamp
      ]

      users.each do |user|
        if user.profiles.any?
          user.profiles.sort_by(&:id).each do |profile|
            table << [
              user.uuid,
              profile.id,
              profile.active ? 'active' : 'inactive',
              profile.activated_at,
              profile.deactivation_reason,
              profile.gpo_verification_pending_at,
              profile.fraud_review_pending_at,
              profile.fraud_rejection_at,
            ]
          end
        elsif config.include_missing?
          table << [user.uuid, '[HAS NO PROFILE]', nil, nil, nil, nil, nil, nil]
        end
      end

      if config.include_missing?
        (uuids - users.map(&:uuid)).each do |missing_uuid|
          table << [missing_uuid, '[UUID NOT FOUND]', nil, nil, nil, nil, nil, nil]
        end
      end

      ScriptBase::Result.new(
        subtask: 'profile-summary',
        uuids: users.map(&:uuid),
        table:,
      )
    end
  end

  class UuidExport
    def run(args:, config:)
      login_uuids = args

      uuids = []
      table = []
      table << %w[login_uuid agency issuer external_uuid]

      User.includes(:agency_identities, identities: { service_provider_record: :agency }).
        where(uuid: login_uuids).
        then do |scope|
          if config.requesting_issuers.present?
            scope.where(service_provider_record: { issuer: config.requesting_issuers })
          else
            scope
          end
        end.each do |user|
          user.identities.each do |identity|
            uuids << user.uuid
            external_uuid = user.agency_identities&.find do |a_i|
                              a_i.agency == identity.service_provider_record.agency
                            end&.uuid || identity.uuid
            table << [
              user.uuid,
              identity.service_provider_record.agency&.name,
              identity.service_provider_record.issuer,
              external_uuid,
            ]
          end
        end

      if config.include_missing?
        (login_uuids - uuids.uniq).each do |missing_uuid|
          table << [missing_uuid, '[NOT FOUND]', '[NOT FOUND]', '[NOT FOUND]']
        end
      end

      ScriptBase::Result.new(
        subtask: 'uuid-export',
        uuids: uuids.uniq,
        table:,
      )
    end
  end

  class EventsSummary
    def run(args:, config:)
      uuids = args

      sql = <<-SQL
        SELECT
          users.uuid AS uuid
        , events.created_at::date AS date
        , COUNT(events.id) AS events_count
        FROM users
        JOIN events ON users.id = events.user_id
        WHERE users.uuid IN (:uuids)
        GROUP BY
          users.uuid
        , events.created_at::date
        ORDER BY
          users.uuid ASC
        , events.created_at::date DESC
      SQL

      results = ActiveRecord::Base.connection.execute(
        ApplicationRecord.sanitize_sql_array([sql, { uuids: uuids }]),
      )

      table = []
      table << %w[uuid date events_count]

      results.each do |row|
        table << [row['uuid'], row['date'], row['events_count']]
      end

      found_uuids = results.map { |r| r['uuid'] }.uniq

      if config.include_missing?
        (uuids - found_uuids).each do |missing_uuid|
          table << [missing_uuid, '[UUID NOT FOUND]', nil]
        end
      end

      ScriptBase::Result.new(
        subtask: 'events-summary',
        uuids: found_uuids,
        table:,
      )
    end
  end
end