Skip to content
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

feat: handle offline and no grant or token situations #109

Merged
merged 16 commits into from
Apr 1, 2024
76 changes: 63 additions & 13 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,65 @@
import { logger, getSymbol } from './lib/logger.js'
import { options, configValid } from './lib/args.js'
if (!configValid) {
logger.error({ component: 'main', message: 'invalid configuration... Exiting'})
logger.error({ component, message: 'invalid configuration... Exiting'})
logger.end()
process.exit(1)
}
import startFsEventWatcher from './lib/events.js'
import { getOpenIDConfiguration, getToken } from './lib/auth.js'
import * as auth from './lib/auth.js'
import * as api from './lib/api.js'
import { serializeError } from 'serialize-error'
import { initScanner } from './lib/scan.js'
import semverGte from 'semver/functions/gte.js'
import Alarm from './lib/alarm.js'
import * as CONSTANTS from './lib/consts.js'

const minApiVersion = '1.2.7'
const component = 'index'

process.on('SIGINT', () => {
logger.info({
component: 'main',
component,
message: 'received SIGINT, exiting'
})
process.exit(0)
})
})

run()
Alarm.on('shutdown', (exitCode) => {
logger.error({
component,
message: `received shutdown event with code ${exitCode}, exiting`
})
process.exit(exitCode)
})

Alarm.on('alarmRaised', (alarmType) => {
logger.error({
component,
message: `Alarm raised: ${alarmType}`
})
})

Alarm.on('alarmLowered', (alarmType) => {
logger.info({
component,
message: `Alarm lowered: ${alarmType}`
})
})

await run()

async function run() {
try {
logger.info({
component: 'main',
component,
message: 'running',
pid: process.pid,
options: getObfuscatedConfig(options)
})

await preflightServices()
setupAlarmHandlers()
if (options.mode === 'events') {
startFsEventWatcher()
}
Expand All @@ -45,12 +71,25 @@ async function run() {
catch (e) {
logError(e)
logger.end()
process.exitCode = CONSTANTS.ERR_FAILINIT
}
}

function setupAlarmHandlers() {
const alarmHandlers = {
apiOffline: api.offlineRetryHandler,
authOffline: auth.offlineRetryHandler,
noGrant: () => Alarm.shutdown(CONSTANTS.ERR_NOGRANT),
noToken: () => Alarm.shutdown(CONSTANTS.ERR_NOTOKEN)
}
Alarm.on('alarmRaised', (alarmType) => {
alarmHandlers[alarmType]?.()
})
}

function logError(e) {
const errorObj = {
component: e.component || 'main',
component: e.component || 'index',
message: e.message,
}
if (e.request) {
Expand All @@ -74,7 +113,7 @@ function logError(e) {

async function hasMinApiVersion () {
const [remoteApiVersion] = await api.getDefinition('$.info.version')
logger.info({ component: 'main', message: `preflight API version`, minApiVersion, remoteApiVersion})
logger.info({ component, message: `preflight API version`, minApiVersion, remoteApiVersion})
if (semverGte(remoteApiVersion, minApiVersion)) {
return true
}
Expand All @@ -85,9 +124,9 @@ async function hasMinApiVersion () {

async function preflightServices () {
await hasMinApiVersion()
await getOpenIDConfiguration()
await getToken()
logger.info({ component: 'main', message: `preflight token request suceeded`})
await auth.getOpenIDConfiguration()
await auth.getToken()
logger.info({ component, message: `preflight token request suceeded`})
const promises = [
api.getCollection(options.collectionId),
api.getCollectionAssets(options.collectionId),
Expand All @@ -104,9 +143,10 @@ async function preflightServices () {
setInterval(refreshUser, 10 * 60000)
}
catch (e) {
logger.warn({ component: 'main', message: `preflight user request failed; token may be missing scope 'stig-manager:user:read'? Watcher will not set {"status": "accepted"}`})
logger.warn({ component, message: `preflight user request failed; token may be missing scope 'stig-manager:user:read'? Watcher will not set {"status": "accepted"}`})
Alarm.noGrant(false)
}
logger.info({ component: 'main', message: `prefilght api requests suceeded`})
logger.info({ component, message: `prefilght api requests suceeded`})
}

function getObfuscatedConfig (options) {
Expand All @@ -119,6 +159,11 @@ function getObfuscatedConfig (options) {

async function refreshUser() {
try {
if (Alarm.isAlarmed()) return
logger.info({
component,
message: 'refreshing user cache'
})
await api.getUser()
}
catch (e) {
Expand All @@ -128,6 +173,11 @@ async function refreshUser() {

async function refreshCollection() {
try {
if (Alarm.isAlarmed()) return
logger.info({
component,
message: 'refreshing collection cache'
})
await api.getCollection(options.collectionId)
}
catch (e) {
Expand Down
122 changes: 122 additions & 0 deletions lib/alarm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { EventEmitter } from "node:events"

/**
* Represents the state of each alarm type.
* @typedef {Object} Alarms
* @property {boolean} apiOffline - the STIG manager API is unreachable.
* @property {boolean} authOffline - the OIDC IdP is unreachable.
* @property {boolean} noToken - the OIDC IdP did not issue the client a token.
* @property {boolean} noGrant - the client has an insufficient grant on the configured Collection.
*/

/**
* @typedef {'apiOffline' | 'authOffline' | 'noToken' | 'noGrant'} AlarmType
*/

class Alarm extends EventEmitter {
/** @type {Alarms} */
#alarms

constructor () {
super()
this.#alarms = {
apiOffline: false,
authOffline: false,
noToken: false,
noGrant: false,
}
}

/**
* Emits 'alarmRaised' or 'alarmLowered' based on 'state', passing the alarmType
* @param {AlarmType} event
* @param {boolean} state
*/
#emitAlarmEvent (alarmType, state) {
if (alarmType === 'shutdown') {
this.emit('shutdown', state)
return
}
if (state) {
this.emit('alarmRaised', alarmType)
}
else {
this.emit('alarmLowered', alarmType)
}
}

/**
* Sets the state of the apiOffline alarm
* and emits an alarmRaised or alarmLowered event
* @param {boolean} state
*/
apiOffline (state) {
if (this.#alarms.apiOffline === state) return
this.#alarms.apiOffline = state
this.#emitAlarmEvent( 'apiOffline', state)
}

/**
* Sets the state of the authOffline alarm
* and emits an alarmRaised or alarmLowered event
* @param {boolean} state
*/
authOffline (state) {
if (this.#alarms.authOffline === state) return
this.#alarms.authOffline = state
this.#emitAlarmEvent( 'authOffline', state)
}

/**
* Sets the state of the noToken alarm
* and emits an alarmRaised or alarmLowered event
* @param {boolean} state
*/
noToken (state) {
if (this.#alarms.noToken === state) return
this.#alarms.noToken = state
this.#emitAlarmEvent( 'noToken', state)
}

/**
* Sets the state of the noGrant alarm
* and emits an alarmRaised or alarmLowered event
* @param {boolean} state
*/
noGrant (state) {
if (this.#alarms.noGrant === state) return
this.#alarms.noGrant = state
this.#emitAlarmEvent( 'noGrant', state)
}

/**
* Returns an array of the raised alarm types
* @returns {string[]}
*/
raisedAlarms () {
return Object.keys(this.#alarms).filter(key=>this.#alarms[key])
}

/**
* Returns true if any alarm is raised
* @returns {boolean}
*/
isAlarmed () {
return Object.values(this.#alarms).some(value=>value)
}

/**
* Emits a shutdown event with the provied exitCode
* @param {number} exitCode
*/
shutdown (exitCode) {
this.#emitAlarmEvent('shutdown', exitCode)
}

/** @type {Alarms} */
get alarms() {
return this.#alarms
}
}

export default new Alarm()
Loading