OperatorApiService.ts 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import express from 'express'
  2. import path from 'path'
  3. import { OpenApiValidatorOpts } from 'express-openapi-validator/dist/framework/types'
  4. import { Config } from '../../types/config'
  5. import { LoggingService } from '../logging'
  6. import jwt from 'jsonwebtoken'
  7. import { OperatorApiController } from './controllers/operator'
  8. import { HttpApiBase, HttpApiRoute } from './HttpApiBase'
  9. import { PublicApiService } from './PublicApiService'
  10. import _ from 'lodash'
  11. import { App } from '../../app'
  12. const OPENAPI_SPEC_PATH = path.join(__dirname, '../../api-spec/operator.yml')
  13. const JWT_TOKEN_MAX_AGE = '5m'
  14. export class OperatorApiService extends HttpApiBase {
  15. protected port: number
  16. protected operatorSecretKey: string
  17. protected config: Config
  18. protected app: App
  19. protected publicApi: PublicApiService
  20. protected logging: LoggingService
  21. public constructor(config: Config, app: App, logging: LoggingService, publicApi: PublicApiService) {
  22. super(config, logging.createLogger('OperatorApi'))
  23. if (!config.operatorApi) {
  24. throw new Error('Cannot construct OperatorApiService - missing operatorApi config!')
  25. }
  26. this.port = config.operatorApi.port
  27. this.operatorSecretKey = config.operatorApi.hmacSecret
  28. this.config = config
  29. this.app = app
  30. this.logging = logging
  31. this.publicApi = publicApi
  32. this.initApp()
  33. }
  34. protected openApiValidatorConfig(): OpenApiValidatorOpts {
  35. return {
  36. apiSpec: OPENAPI_SPEC_PATH,
  37. validateSecurity: {
  38. handlers: {
  39. OperatorAuth: this.operatorRequestValidator(),
  40. },
  41. },
  42. ...this.defaultOpenApiValidatorConfig(),
  43. }
  44. }
  45. protected routes(): HttpApiRoute[] {
  46. const controller = new OperatorApiController(this.config, this.app, this.publicApi, this.logging)
  47. return [
  48. ['post', '/api/v1/stop-api', controller.stopApi.bind(controller)],
  49. ['post', '/api/v1/start-api', controller.startApi.bind(controller)],
  50. ['post', '/api/v1/shutdown', controller.shutdown.bind(controller)],
  51. ['post', '/api/v1/set-worker', controller.setWorker.bind(controller)],
  52. ['post', '/api/v1/set-buckets', controller.setBuckets.bind(controller)],
  53. ]
  54. }
  55. private operatorRequestValidator() {
  56. return (req: express.Request): boolean => {
  57. const authHeader = req.headers.authorization
  58. if (!authHeader) {
  59. throw new Error('Authrorization header missing')
  60. }
  61. const [authType, token] = authHeader.split(' ')
  62. if (authType.toLowerCase() !== 'bearer') {
  63. throw new Error(`Unexpected authorization type: ${authType}`)
  64. }
  65. if (!token) {
  66. throw new Error(`Bearer token missing`)
  67. }
  68. const decoded = jwt.verify(token, this.operatorSecretKey, { maxAge: JWT_TOKEN_MAX_AGE }) as jwt.JwtPayload
  69. if (!_.isEqual(req.body, decoded.reqBody)) {
  70. throw new Error('Invalid token: Request body does not match')
  71. }
  72. if (req.originalUrl !== decoded.reqUrl) {
  73. throw new Error('Invalid token: Request url does not match')
  74. }
  75. return true
  76. }
  77. }
  78. }