UNPKG

@bowtie/sls

Version:

Serverless helpers & utilities

481 lines (377 loc) 11.6 kB
const fs = require('fs') const qs = require('qs') const path = require('path') const yaml = require('js-yaml') const fetch = require('node-fetch') const { Octokit } = require('@octokit/rest') const { parseServiceConfig } = require('../utils') const REQUIRED_RESPONSE_HEADERS = { /* Required for CORS support to work */ 'Access-Control-Allow-Origin': '*', /* Required for cookies, authorization headers with HTTPS */ 'Access-Control-Allow-Credentials': true } class BaseController { constructor(options = {}) { this.model = options.model this.defaultSort = options.defaultSort || 'id' this.defaultLimit = options.defaultLimit || 1000 } static get REQUIRED_RESPONSE_HEADERS() { return REQUIRED_RESPONSE_HEADERS } static async info(event, context) { const ctrl = new BaseController() const authorize = await ctrl._authorize('info', event, context) if (authorize === true) { return ctrl._ok(BaseController.service) } return ctrl._ok(BaseController.publicData) } static get publicData() { const { source } = this.service return { source } } static get service() { return parseServiceConfig() } async _githubAccess(event) { const service = this.constructor.service const { headers } = event const tokenParts = headers['Authorization'].split(' '); const token = tokenParts[tokenParts.length - 1]; const octokit = Octokit({ auth: token, userAgent: 'Bowtie CI' }) const repoParts = service.source.repo.split('/') const repoOwner = repoParts[0] const repoNameParts = repoParts.slice(1) const repoName = repoNameParts.join('/') const user = await octokit.users.getAuthenticated() const repo = await octokit.repos.get({ owner: repoOwner, repo: repoName }) const repository = repo.data if (!repository) { console.warn('No repo') return {} } const { permissions } = repository if (permissions) { return { repository, user: user.data, permission: Object.keys(permissions).find(perm => permissions[perm] === true) } } return {} } async _bitbucketAccess(event) { const service = this.constructor.service; const { headers } = event const tokenParts = headers['Authorization'].split(' '); const token = tokenParts[tokenParts.length - 1]; const options = { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Accept': 'application/json' } } const repoParts = service.source.repo.split('/') // const repoOwner = repoParts[0] const repoNameParts = repoParts.slice(1) const repoName = repoNameParts.join('/') const permsReqUrl = `https://api.bitbucket.org/2.0/user/permissions/repositories?q=repository.name%3D"${repoName}"` const permissionsResponse = await fetch(permsReqUrl, options) const permissionsData = await permissionsResponse.json() const repoPermissions = permissionsData.values.find(perm => perm.repository.full_name === service.source.repo) if (repoPermissions) { const { user, repository, permission } = repoPermissions return { user, repository, permission } } return {} } _accessForAction(action) { switch(action) { case 'logs': case 'tags': case 'show': case 'info': case 'read': case 'index': case 'stacks': return [ 'pull', 'push', 'read', 'write', 'admin' ] case 'write': case 'create': case 'update': case 'deploy': // TODO: Is this best default perms for download? case 'download': return [ 'push', 'write', 'admin' ] case 'admin': case 'destroy': case 'production': return [ 'admin' ] default: return [] } } _verifyAccess(action, access, perms = {}) { const service = this.constructor.service const { permissions = {} } = service const { user, repository, permission } = access if (!user || !permission) { return false } const applies = (rules, user, perm) => { if (!Array.isArray(rules)) { rules = [ rules ] } if (rules.length === 1 && rules[0] === '*') { return true } console.log('rules', rules) return ( rules.includes(perm) || (user.display_name && rules.includes(user.display_name)) || (user.nickname && rules.includes(user.nickname)) || (user.login && rules.includes(user.login)) || (user.email && rules.includes(user.email)) || (user.name && rules.includes(user.name)) ) } const required = this._accessForAction(action) const activePerms = perms[action] || permissions[action] console.log('Auth action', action, required, permission, activePerms) if (activePerms) { const { allow, deny } = activePerms console.log('Using perms', activePerms) if (deny && applies(deny, user, permission)) { return false } if (allow && applies(allow, user, permission)) { return true } } return required.includes(permission) } async _authorizedAccess(action, event, context) { const service = this.constructor.service; let access = {} if (service.source.type === 'BITBUCKET') { access = await this._bitbucketAccess(event) } else if (service.source.type === 'GITHUB') { access = await this._githubAccess(event) } return this._verifyAccess(action, access) ? access : null } async _isAuthorized(action, event, context) { const service = this.constructor.service; let access = {} if (service.source.type === 'BITBUCKET') { access = await this._bitbucketAccess(event) } else if (service.source.type === 'GITHUB') { access = await this._githubAccess(event) } return this._verifyAccess(action, access) } async _authorize(action, event, context) { const { headers = {} } = event if (!headers['Authorization']) { return this._unauthorized() } const tokenParts = headers['Authorization'].split(' '); const token = tokenParts[tokenParts.length - 1]; if (!token || token.trim() === '') { return this._unauthorized() } try { const isAuthorized = await this._isAuthorized(action, event, context) if (!isAuthorized) { return this._forbidden() } } catch (err) { console.warn(err) return this._unauthorized() } return true; } async _exec(action, event, context, auth = true, identify = false) { const { path, headers = {}, pathParameters = {}, requestContext, resource, httpMethod, queryStringParameters = {}, multiValueQueryStringParameters, stageVariables, body, isOffline } = event if (auth) { const authorize = await this._authorize(action, event, context) if (authorize !== true) { return authorize } } let parsedBody = {} try { if (body) { parsedBody = JSON.parse(body) } if (identify && requestContext && requestContext.identity) { const { sourceIp, userAgent } = requestContext.identity Object.assign(parsedBody, { sourceIp, userAgent }) } const compare = (a, b) => { if (a[this.defaultSort] < b[this.defaultSort]) return 1 if (b[this.defaultSort] < a[this.defaultSort]) return -1 return 0 } switch(action) { case 'index': let result = [] const queryParms = {} if (queryStringParameters) { // TODO: Expand on this ... enable .contains() .in(), etc ... whitelist allowed fileter params? // Object.keys(queryStringParameters).forEach(key => queryParms[key] = { eq: queryStringParameters[key] }) Object.assign(queryParms, queryStringParameters) } if (queryParms && Object.keys(queryParms).length > 0) { result = await this.model.scanAll(queryParms) // return this._ok(filtered) } else { result = await this.model.scanAll() // return this._ok(all) } result.sort(compare) if (this.defaultLimit) { result.length = this.defaultLimit } return this._ok(result) case 'create': const newItem = new this.model(parsedBody) await newItem.saveNotify() return this._created(newItem) case 'show': case 'update': case 'destroy': if (!pathParameters['id']) { return this._bad() } const item = await this.model.get(pathParameters['id']) if (!item) { return this._not_found() } if (action === 'update') { Object.assign(item, parsedBody) await item.saveNotify() } else if (action === 'destroy') { await item.delete() } return this._ok(item) default: return this._bad() } } catch (err) { console.error('Caught error:', err) return this._bad({ message: err.message || err }) } } async index(event, context) { return await this._exec('index', event, context) } async create(event, context) { return await this._exec('create', event, context) } async show(event, context) { return await this._exec('show', event, context) } async update(event, context) { return await this._exec('update', event, context) } async destroy(event, context) { return await this._exec('destroy', event, context) } _respond (options = {}) { const response = { statusCode: 400, headers: REQUIRED_RESPONSE_HEADERS } Object.assign(response, options) if (response.body && typeof response.body !== 'string') { response.body = JSON.stringify(response.body) } return response } _ok (body, options = {}) { console.log('OK') const response = Object.assign({ statusCode: 200, body }, options) return this._respond(response) } _created (body, options = {}) { console.log('Created') const response = Object.assign({ statusCode: 201, body: body || { message: 'Created' } }, options) return this._respond(response) } _bad (body, options = {}) { const response = Object.assign({ statusCode: 400, body: body || { message: 'Bad Request' } }, options) return this._respond(response) } _not_found (body, options = {}) { const response = Object.assign({ statusCode: 404, body: body || { message: 'Not Found' } }, options) return this._respond(response) } _unauthorized (body, options = {}) { const response = Object.assign({ statusCode: 401, body: body || { message: 'Unauthorized' } }, options) return this._respond(response) } _forbidden (body, options = {}) { const response = Object.assign({ statusCode: 403, body: body || { message: 'Forbidden' } }, options) return this._respond(response) } _error (body, options = {}) { console.log('ERROR') const response = Object.assign({ statusCode: 500, body: body || { message: 'Unknown Error' } }, options) return this._respond(response) } } module.exports = BaseController