Skip to content

Commit

Permalink
Add certs health check endpoint (LG-3929) (#181)
Browse files Browse the repository at this point in the history
* Run rake certs:remove_invalid
  • Loading branch information
zachmargolis committed Dec 16, 2020
1 parent 0d5a56f commit d766830
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 42 deletions.
20 changes: 20 additions & 0 deletions app/controllers/health/certs_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Health
class CertsController < ApplicationController
newrelic_ignore_apdex

def index
deadline = params[:deadline].present? ? Time.zone.parse(params[:deadline]) : 30.days.from_now

result = health_checker.check_certs(deadline: deadline)

render json: result.as_json,
status: result.healthy? ? :ok : :service_unavailable
end

private

def health_checker
@health_checker ||= HealthChecker.new
end
end
end
39 changes: 39 additions & 0 deletions app/services/health_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class HealthChecker
Result = Struct.new(:healthy, :info, keyword_init: true) do
alias_method :healthy?, :healthy
end

def initialize(certificates_store: CertificateStore.instance)
@certificates_store = certificates_store
end

# @param [Time] deadline
# @return [Result]
def check_certs(deadline:)
expiring_certs = certificates_store.select do |cert|
cert.expired?(deadline)
end

Result.new(
healthy: expiring_certs.empty?,
info: {
deadline: deadline,
expiring: expiring_certs.sort_by(&:not_after).map { |cert| cert_info(cert) },
}
)
end

private

attr_reader :certificates_store

# @param [Certificate] cert
def cert_info(cert)
{
expiration: cert.not_after,
subject: cert.subject.to_s,
issuer: cert.issuer.to_s,
key_id: cert.key_id,
}
end
end

This file was deleted.

2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get '/', to: 'identify#create'
post '/', to: 'verify#open'

get '/api/health/certs' => 'health/certs#index'
end
70 changes: 70 additions & 0 deletions spec/controllers/health/certs_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
require 'rails_helper'

RSpec.describe Health::CertsController do
describe '#index' do
subject(:action) { get :index, params: { deadline: deadline } }
let(:deadline) { nil }
let(:health_checker) { HealthChecker.new(certificates_store: certificates_store) }

it 'renders a status as JSON' do
action

expect(response.content_type).to eq('application/json')
expect(JSON.parse(response.body, symbolize_names: true)).to include(:healthy)
end

context 'certs health' do
before do
allow(controller).to receive(:health_checker).and_return(health_checker)
end

let(:expiring_cert) do
instance_double(
'Certificate',
expired?: true,
not_after: 15.days.from_now,
subject: OpenSSL::X509::Name.new([%w[CN cert1], %w[OU example]]),
issuer: OpenSSL::X509::Name.new([%w[CN issuer1], %w[OU example]]),
key_id: 'ab:cd:ef:gh:jk'
)
end

context 'with expiring certs' do
let(:certificates_store) { [expiring_cert] }

it 'returns a 503' do
action

expect(response.status).to eq(503)

body = JSON.parse(response.body, symbolize_names: true)
expect(body).to include(healthy: false)
expect(body[:info][:expiring].first[:key_id]).to eq(expiring_cert.key_id)
end
end

context 'with no expiring certs' do
let(:certificates_store) { [] }

it 'returns a 200' do
action

expect(response.status).to eq(200)
expect(JSON.parse(response.body, symbolize_names: true)).to include(healthy: true)
end

context 'with a deadline param' do
let(:deadline) { '2020-01-01' }

it 'checks certs with that deadline' do
expect(health_checker).to receive(:check_certs).
with(deadline: Time.zone.parse(deadline)).
and_call_original

action
end
end
end
end
end
end
62 changes: 62 additions & 0 deletions spec/services/health_checker_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require 'rails_helper'

RSpec.describe HealthChecker do
describe '#check_certs' do
subject(:health_checker) { HealthChecker.new(certificates_store: certificates_store) }
let(:deadline) { 30.days.from_now }

let(:expiring_cert) do
instance_double(
'Certificate',
expired?: true,
not_after: 15.days.from_now,
subject: OpenSSL::X509::Name.new([%w[CN cert1], %w[OU example]]),
issuer: OpenSSL::X509::Name.new([%w[CN issuer1], %w[OU example]]),
key_id: 'ab:cd:ef:gh:jk'
)
end

let(:not_expiring_cert) do
instance_double(
'Certificate',
expired?: false,
not_after: 45.days.from_now,
subject: OpenSSL::X509::Name.new([%w[CN cert2], %w[OU example]]),
issuer: OpenSSL::X509::Name.new([%w[CN issuer2], %w[OU example]]),
key_id: 'lm:no:pq:rs:tu'
)
end

context 'with certs that expire before the deadline' do
let(:certificates_store) { [expiring_cert, not_expiring_cert] }

it 'returns an unhealthy result with the expiring certs' do
result = health_checker.check_certs(deadline: deadline)

expect(result).to_not be_healthy
expect(result.info).to eq(
deadline: deadline,
expiring: [
{
expiration: expiring_cert.not_after,
subject: '/CN=cert1/OU=example',
issuer: '/CN=issuer1/OU=example',
key_id: expiring_cert.key_id,
},
]
)
end
end

context 'with no certs that expire before the deadline' do
let(:certificates_store) { [not_expiring_cert] }

it 'returns a healthy result with no certs' do
result = health_checker.check_certs(deadline: deadline)

expect(result).to be_healthy
expect(result.info).to eq(deadline: deadline, expiring: [])
end
end
end
end

0 comments on commit d766830

Please sign in to comment.