zalando/zappr

View on GitHub
server/routes/api.js

Summary

Maintainability
C
1 day
Test Coverage
import crypto from 'crypto'
import url from 'url'
import qs from 'qs'
import nconf from '../nconf'
import { githubService } from '../service/GithubService'
import { requireAuth } from './auth'
import { hookHandler } from '../handler/HookHandler'
import { checkRunner } from '../checks/CheckRunner'
import { checkHandler } from '../handler/CheckHandler'
import { repositoryHandler } from '../handler/RepositoryHandler'
import ZapprConfiguration from '../zapprfile/Configuration'
import { getCheckByType } from '../checks'
import { logger } from '../../common/debug'

const error = logger('api', 'error')
const warn = logger('api', 'warn')
const info = logger('api', 'info')
const NODE_ENV = nconf.get('NODE_ENV')
const PROD_ENV = 'production'
const GITHUB_HOOK_SECRET = nconf.get('GITHUB_HOOK_SECRET')
const GITHUB_SIGNATURE_HEADER = 'x-hub-signature'
const GITHUB_EVENT_HEADER = 'x-github-event'

function validateIsCalledFromGithub(ctx, next) {
  const {header, body} = ctx.request
  const actualSignature = header[GITHUB_SIGNATURE_HEADER]
  // not require signature header for backwards compatibility
  if (!actualSignature) {
    warn(`Request from host ${header.host} is missing X-Hub-Signature header!`)
    return next()
  }
  const sha1 = crypto.createHmac('sha1', GITHUB_HOOK_SECRET)
  // use buffer otherwise unicde (emojis! 💩) break the hash
  const hmac = sha1.update(new Buffer(JSON.stringify(body))).digest('hex')
  const expectedSignature = `sha1=${hmac}`
  if (actualSignature !== expectedSignature) {
    error(`Hook for ${body.repository.full_name} called with invalid signature "${actualSignature}"`
      + `(expected: "${expectedSignature}") from ${header.host}!`)
    ctx.throw(400)
  }

  return next()
}

/**
 * Environment variables endpoint.
 */
export function env(router) {
  return router.get('/api/env', requireAuth, ctx => {
    ctx.body = {
      'NODE_ENV': nconf.get('NODE_ENV')
    }
  })
}

/**
 * Repository collection.
 */
export function repos(router) {
  return router.get('/api/repos', requireAuth, async(ctx) => {
    try {
      const user = ctx.req.user
      const all = ctx.request.query.all == 'true'
      const repos = await repositoryHandler.onGetAll(user, all)
      ctx.body = repos.map(repo => repo.toJSON())
    } catch (e) {
      ctx.throw(503, e)
    }
  })
}

/**
 * Single repository.
 */
export function repo(router) {
  return router
  .get('/api/repos/:id', requireAuth, async(ctx) => {
    try {
      const query = qs.parse(url.parse(ctx.req.url).query)
      const user = ctx.req.user
      const id = parseInt(ctx.params.id)
      const autoSync = query.autoSync === 'true'
      const repo = await repositoryHandler.onGetOne(id, user, false, autoSync)
      ctx.body = repo
    } catch (e) {
      ctx.throw(404, e)
    }
  })
  .post('/api/repos/:id/refreshTokens', requireAuth, async(ctx) => { 
    try{
      const user = ctx.req.user;
      const id = parseInt(ctx.params.id);
      const updatedTokens = await checkHandler.onRefreshTokens(id, user);
      info('updated Checks with new access token for repository: ', id);
      ctx.response.status = 200;
      ctx.body = { 
        message: `${updatedTokens[0]} checks were updated`,
      };
    } catch(e) {
      ctx.throw(404, e)
    }
  })
  .get('/api/repos/:id/zapprfile', requireAuth, async(ctx) => {
    const user = ctx.req.user
    const id = parseInt(ctx.params.id, 10)
    let repo
    try {
      repo = await repositoryHandler.onGetOne(id, user, true)
    } catch (e) {
      ctx.throw(404, e)
    }
    const zapprFileContent = await githubService.readZapprFile(repo.json.owner.login, repo.json.name, user.accessToken)
    const config = new ZapprConfiguration(zapprFileContent, repo)

    const message = zapprFileContent === '' ?
      'No Zapprfile found, using default config' :
      (config.isValid() ?
        '' :
        config.getParseError())

    ctx.body = {
      config: config.getConfiguration(),
      message,
      valid: config.isValid()
    }
    ctx.response.status = 200
  })
  .put('/api/repos/:id/:type', requireAuth, async(ctx) => {
    try {
      const user = ctx.req.user
      const id = parseInt(ctx.params.id)
      const type = ctx.params.type
      const repo = await repositoryHandler.onGetOne(id, user, true)
      const token = user.accessToken
      const owner = repo.json.owner.login
      const name = repo.json.name
      const defaultBranch = repo.json.default_branch
      const zapprFile = await githubService.readZapprFile(owner, name, token)
      if (!repo.welcomed) {
        try {
          if (zapprFile.length === 0) {
            await githubService.proposeZapprfile(owner, name, defaultBranch, token)
            info(`${owner}/${name}: Welcome to Zappr.`)
          } else {
            info(`${owner}/${name}: Welcome to Zappr (no PR needed).`)
          }
          await repositoryHandler.onWelcome(id)
        } catch (e) {
          error(`${owner}/${name}: Could not welcome. ${e.message}`)
        }
      }
      const check = await checkHandler.onEnableCheck(user, repo, type)
      const checkContext = getCheckByType(type).CONTEXT
      if (checkContext) {
        // autobranch doesn't have a context
        const config = new ZapprConfiguration(zapprFile, repo)
        if (NODE_ENV !== PROD_ENV) {
          // blocking in dev and tests
          await githubService.protectBranch(owner, name, defaultBranch, checkContext, token)
          await checkRunner.handleExistingPullRequests(repo, getCheckByType(type).TYPE, {config: config.getConfiguration(), token})
        } else {
          // not blocking in production
          githubService.protectBranch(owner, name, defaultBranch, checkContext, token)
          checkRunner.handleExistingPullRequests(repo, getCheckByType(type).TYPE, {config: config.getConfiguration(), token})
        }
      }
      ctx.response.status = 201
      ctx.body = check.toJSON()
    } catch (e) {
      ctx.throw(503, e)
    }
  })
  .delete('/api/repos/:id/:type', requireAuth, async(ctx) => {
    try {
      const user = ctx.req.user
      const id = parseInt(ctx.params.id)
      const repo = await repositoryHandler.onGetOne(id, user)
      const type = ctx.params.type
      const checkContext = getCheckByType(type).CONTEXT
      // check first if user has permissions 
      if (checkContext) {
        if (NODE_ENV !== PROD_ENV) {
          try {
            // block when not in prod
            // => so we can test the API calls
            await checkRunner.release(repo, type, user.accessToken)
            await githubService.removeRequiredStatusCheck(repo.json.owner.login, repo.json.name, repo.json.default_branch, checkContext, user.accessToken)
          } catch (e) {
            ctx.throw(503, e)
            error(`${repo.json.full_name}: Could not remove status check. ${e.detail}`)
          }
        } else {
          // BugFix machinery/zappr-deploy/issues/14 (Zalando internal reference)
          // https://github.com/zalando/zappr/pull/560/
          // not block when in prod
          try {
            checkRunner.release(repo, type, user.accessToken)
              .catch(e => error(`${repo.json.full_name} [${type}]: Could not release pull requests. ${e.message}`))
            await githubService.removeRequiredStatusCheck(repo.json.owner.login, repo.json.name, repo.json.default_branch, checkContext, user.accessToken)
          } catch (e) {
            ctx.throw(503, e)
            error(`${repo.json.full_name}: Could not remove status check. ${e.detail}`)
          }
        }
      }
      await checkHandler.onDisableCheck(user, repo, type)
      ctx.response.status = 204
      ctx.body = null
    } catch (e) {
      ctx.throw(503, e)
    }
  })
  .post('/api/hook', validateIsCalledFromGithub, async(ctx) => {
    const {header, body} = ctx.request
    const event = header[GITHUB_EVENT_HEADER]
    const hookResult = await hookHandler.onHandleHook(event, body)
    ctx.response.status = 200
    ctx.body = hookResult
  })
}