@bowtie/sls
Version:
Serverless helpers & utilities
481 lines (377 loc) • 11.6 kB
JavaScript
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