forked from tsai/budibase
104 changed files with 786 additions and 985 deletions
@ -1 +1,12 @@ |
|||||
# Budibase Authentication Library |
# Budibase Core backend library |
||||
|
|
||||
|
This library contains core functionality, like auth and security features |
||||
|
which are shared between backend services. |
||||
|
|
||||
|
#### Note about top level JS files |
||||
|
For the purposes of being able to do say `require("@budibase/auth/permissions")` we need to |
||||
|
specify the exports at the top-level of the module. |
||||
|
|
||||
|
For these files they should be limited to a single `require` of the file that should |
||||
|
be exported and then a single `module.exports = ...` to export the file in |
||||
|
commonJS. |
||||
@ -0,0 +1 @@ |
|||||
|
module.exports = require("./src/db/utils") |
||||
@ -0,0 +1 @@ |
|||||
|
module.exports = require("./src/security/permissions") |
||||
@ -0,0 +1,4 @@ |
|||||
|
module.exports = { |
||||
|
Client: require("./src/redis"), |
||||
|
utils: require("./src/redis/utils"), |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
module.exports = require("./src/security/roles") |
||||
@ -0,0 +1,74 @@ |
|||||
|
const { getDB } = require(".") |
||||
|
|
||||
|
class Replication { |
||||
|
/** |
||||
|
* |
||||
|
* @param {String} source - the DB you want to replicate or rollback to |
||||
|
* @param {String} target - the DB you want to replicate to, or rollback from |
||||
|
*/ |
||||
|
constructor({ source, target }) { |
||||
|
this.source = getDB(source) |
||||
|
this.target = getDB(target) |
||||
|
} |
||||
|
|
||||
|
promisify(operation, opts = {}) { |
||||
|
return new Promise(resolve => { |
||||
|
operation(this.target, opts) |
||||
|
.on("denied", function (err) { |
||||
|
// a document failed to replicate (e.g. due to permissions)
|
||||
|
throw new Error(`Denied: Document failed to replicate ${err}`) |
||||
|
}) |
||||
|
.on("complete", function (info) { |
||||
|
return resolve(info) |
||||
|
}) |
||||
|
.on("error", function (err) { |
||||
|
throw new Error(`Replication Error: ${err}`) |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Two way replication operation, intended to be promise based. |
||||
|
* @param {Object} opts - PouchDB replication options |
||||
|
*/ |
||||
|
sync(opts) { |
||||
|
this.replication = this.promisify(this.source.sync, opts) |
||||
|
return this.replication |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* One way replication operation, intended to be promise based. |
||||
|
* @param {Object} opts - PouchDB replication options |
||||
|
*/ |
||||
|
replicate(opts) { |
||||
|
this.replication = this.promisify(this.source.replicate.to, opts) |
||||
|
return this.replication |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Set up an ongoing live sync between 2 CouchDB databases. |
||||
|
* @param {Object} opts - PouchDB replication options |
||||
|
*/ |
||||
|
subscribe(opts = {}) { |
||||
|
this.replication = this.source.replicate |
||||
|
.to(this.target, { |
||||
|
live: true, |
||||
|
retry: true, |
||||
|
...opts, |
||||
|
}) |
||||
|
.on("error", function (err) { |
||||
|
throw new Error(`Replication Error: ${err}`) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async rollback() { |
||||
|
await this.target.destroy() |
||||
|
await this.replicate() |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.replication.cancel() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = Replication |
||||
@ -1,80 +0,0 @@ |
|||||
const AWS = require("aws-sdk") |
|
||||
const fetch = require("node-fetch") |
|
||||
const env = require("../../../environment") |
|
||||
const { |
|
||||
deployToObjectStore, |
|
||||
performReplication, |
|
||||
fetchCredentials, |
|
||||
} = require("./utils") |
|
||||
|
|
||||
/** |
|
||||
* Verifies the users API key and |
|
||||
* Verifies that the deployment fits within the quota of the user |
|
||||
* Links to the "check-api-key" lambda. |
|
||||
* @param {object} deployment - information about the active deployment, including the appId and quota. |
|
||||
*/ |
|
||||
exports.preDeployment = async function (deployment) { |
|
||||
const json = await fetchCredentials(env.DEPLOYMENT_CREDENTIALS_URL, { |
|
||||
apiKey: env.BUDIBASE_API_KEY, |
|
||||
appId: deployment.getAppId(), |
|
||||
quota: deployment.getQuota(), |
|
||||
}) |
|
||||
|
|
||||
// set credentials here, means any time we're verified we're ready to go
|
|
||||
if (json.credentials) { |
|
||||
AWS.config.update({ |
|
||||
accessKeyId: json.credentials.AccessKeyId, |
|
||||
secretAccessKey: json.credentials.SecretAccessKey, |
|
||||
sessionToken: json.credentials.SessionToken, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
return json |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Finalises the deployment, updating the quota for the user API key |
|
||||
* The verification process returns the levels to update to. |
|
||||
* Calls the "deployment-success" lambda. |
|
||||
* @param {object} deployment information about the active deployment, including the quota info. |
|
||||
* @returns {Promise<object>} The usage has been updated against the user API key. |
|
||||
*/ |
|
||||
exports.postDeployment = async function (deployment) { |
|
||||
const DEPLOYMENT_SUCCESS_URL = |
|
||||
env.DEPLOYMENT_CREDENTIALS_URL + "deploy/success" |
|
||||
|
|
||||
const response = await fetch(DEPLOYMENT_SUCCESS_URL, { |
|
||||
method: "POST", |
|
||||
body: JSON.stringify({ |
|
||||
apiKey: env.BUDIBASE_API_KEY, |
|
||||
quota: deployment.getQuota(), |
|
||||
}), |
|
||||
headers: { |
|
||||
"Content-Type": "application/json", |
|
||||
Accept: "application/json", |
|
||||
}, |
|
||||
}) |
|
||||
|
|
||||
if (response.status !== 200) { |
|
||||
throw new Error(`Error updating deployment quota for API Key`) |
|
||||
} |
|
||||
|
|
||||
return await response.json() |
|
||||
} |
|
||||
|
|
||||
exports.deploy = async function (deployment) { |
|
||||
const appId = deployment.getAppId() |
|
||||
const { bucket, accountId } = deployment.getVerification() |
|
||||
const metadata = { accountId } |
|
||||
await deployToObjectStore(appId, bucket, metadata) |
|
||||
} |
|
||||
|
|
||||
exports.replicateDb = async function (deployment) { |
|
||||
const appId = deployment.getAppId() |
|
||||
const verification = deployment.getVerification() |
|
||||
return performReplication( |
|
||||
appId, |
|
||||
verification.couchDbSession, |
|
||||
env.DEPLOYMENT_DB_URL |
|
||||
) |
|
||||
} |
|
||||
@ -1,39 +0,0 @@ |
|||||
const PouchDB = require("../../../db") |
|
||||
const { |
|
||||
DocumentTypes, |
|
||||
SEPARATOR, |
|
||||
UNICODE_MAX, |
|
||||
ViewNames, |
|
||||
} = require("../../../db/utils") |
|
||||
|
|
||||
exports.getAppQuota = async function (appId) { |
|
||||
const db = new PouchDB(appId) |
|
||||
|
|
||||
const rows = await db.allDocs({ |
|
||||
startkey: DocumentTypes.ROW + SEPARATOR, |
|
||||
endkey: DocumentTypes.ROW + SEPARATOR + UNICODE_MAX, |
|
||||
}) |
|
||||
|
|
||||
const users = await db.allDocs({ |
|
||||
startkey: DocumentTypes.USER + SEPARATOR, |
|
||||
endkey: DocumentTypes.USER + SEPARATOR + UNICODE_MAX, |
|
||||
}) |
|
||||
|
|
||||
const existingRows = rows.rows.length |
|
||||
const existingUsers = users.rows.length |
|
||||
|
|
||||
const designDoc = await db.get("_design/database") |
|
||||
|
|
||||
let views = 0 |
|
||||
for (let viewName of Object.keys(designDoc.views)) { |
|
||||
if (Object.values(ViewNames).indexOf(viewName) === -1) { |
|
||||
views++ |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
rows: existingRows, |
|
||||
users: existingUsers, |
|
||||
views: views, |
|
||||
} |
|
||||
} |
|
||||
@ -1,60 +0,0 @@ |
|||||
const AWS = require("aws-sdk") |
|
||||
const { |
|
||||
deployToObjectStore, |
|
||||
performReplication, |
|
||||
fetchCredentials, |
|
||||
} = require("./utils") |
|
||||
const { |
|
||||
getWorkerUrl, |
|
||||
getCouchUrl, |
|
||||
getSelfHostKey, |
|
||||
} = require("../../../utilities/builder/hosting") |
|
||||
|
|
||||
exports.preDeployment = async function () { |
|
||||
const url = `${await getWorkerUrl()}/api/deploy` |
|
||||
try { |
|
||||
const json = await fetchCredentials(url, { |
|
||||
selfHostKey: await getSelfHostKey(), |
|
||||
}) |
|
||||
|
|
||||
// response contains:
|
|
||||
// couchDbSession, bucket, objectStoreSession
|
|
||||
|
|
||||
// set credentials here, means any time we're verified we're ready to go
|
|
||||
if (json.objectStoreSession) { |
|
||||
AWS.config.update({ |
|
||||
accessKeyId: json.objectStoreSession.accessKeyId, |
|
||||
secretAccessKey: json.objectStoreSession.secretAccessKey, |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
return json |
|
||||
} catch (err) { |
|
||||
throw { |
|
||||
message: "Unauthorised to deploy, check self hosting key", |
|
||||
status: 401, |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.postDeployment = async function () { |
|
||||
// we don't actively need to do anything after deployment in self hosting
|
|
||||
} |
|
||||
|
|
||||
exports.deploy = async function (deployment) { |
|
||||
const appId = deployment.getAppId() |
|
||||
const verification = deployment.getVerification() |
|
||||
// no metadata, aws has account ID in metadata
|
|
||||
const metadata = {} |
|
||||
await deployToObjectStore(appId, verification.bucket, metadata) |
|
||||
} |
|
||||
|
|
||||
exports.replicateDb = async function (deployment) { |
|
||||
const appId = deployment.getAppId() |
|
||||
const verification = deployment.getVerification() |
|
||||
return performReplication( |
|
||||
appId, |
|
||||
verification.couchDbSession, |
|
||||
await getCouchUrl() |
|
||||
) |
|
||||
} |
|
||||
@ -1,136 +0,0 @@ |
|||||
const { join } = require("../../../utilities/centralPath") |
|
||||
const fs = require("fs") |
|
||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir") |
|
||||
const fetch = require("node-fetch") |
|
||||
const PouchDB = require("../../../db") |
|
||||
const CouchDB = require("pouchdb") |
|
||||
const { upload } = require("../../../utilities/fileSystem") |
|
||||
const { attachmentsRelativeURL } = require("../../../utilities") |
|
||||
|
|
||||
// TODO: everything in this file is to be removed
|
|
||||
|
|
||||
function walkDir(dirPath, callback) { |
|
||||
for (let filename of fs.readdirSync(dirPath)) { |
|
||||
const filePath = `${dirPath}/${filename}` |
|
||||
const stat = fs.lstatSync(filePath) |
|
||||
|
|
||||
if (stat.isFile()) { |
|
||||
callback(filePath) |
|
||||
} else { |
|
||||
walkDir(filePath, callback) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.fetchCredentials = async function (url, body) { |
|
||||
const response = await fetch(url, { |
|
||||
method: "POST", |
|
||||
body: JSON.stringify(body), |
|
||||
headers: { "Content-Type": "application/json" }, |
|
||||
}) |
|
||||
|
|
||||
const json = await response.json() |
|
||||
if (json.errors) { |
|
||||
throw new Error(json.errors) |
|
||||
} |
|
||||
|
|
||||
if (response.status !== 200) { |
|
||||
throw new Error( |
|
||||
`Error fetching temporary credentials: ${JSON.stringify(json)}` |
|
||||
) |
|
||||
} |
|
||||
|
|
||||
return json |
|
||||
} |
|
||||
|
|
||||
exports.prepareUpload = async function ({ s3Key, bucket, metadata, file }) { |
|
||||
const response = await upload({ |
|
||||
bucket, |
|
||||
metadata, |
|
||||
filename: s3Key, |
|
||||
path: file.path, |
|
||||
type: file.type, |
|
||||
}) |
|
||||
|
|
||||
// don't store a URL, work this out on the way out as the URL could change
|
|
||||
return { |
|
||||
size: file.size, |
|
||||
name: file.name, |
|
||||
url: attachmentsRelativeURL(response.Key), |
|
||||
extension: [...file.name.split(".")].pop(), |
|
||||
key: response.Key, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.deployToObjectStore = async function (appId, bucket, metadata) { |
|
||||
const appAssetsPath = join(budibaseAppsDir(), appId, "public") |
|
||||
|
|
||||
let uploads = [] |
|
||||
|
|
||||
// Upload HTML, CSS and JS for each page of the web app
|
|
||||
walkDir(appAssetsPath, function (filePath) { |
|
||||
const filePathParts = filePath.split("/") |
|
||||
const appAssetUpload = exports.prepareUpload({ |
|
||||
bucket, |
|
||||
file: { |
|
||||
path: filePath, |
|
||||
name: filePathParts.pop(), |
|
||||
}, |
|
||||
s3Key: filePath.replace(appAssetsPath, `assets/${appId}`), |
|
||||
metadata, |
|
||||
}) |
|
||||
uploads.push(appAssetUpload) |
|
||||
}) |
|
||||
|
|
||||
// Upload file attachments
|
|
||||
const db = new PouchDB(appId) |
|
||||
let fileUploads |
|
||||
try { |
|
||||
fileUploads = await db.get("_local/fileuploads") |
|
||||
} catch (err) { |
|
||||
fileUploads = { _id: "_local/fileuploads", uploads: [] } |
|
||||
} |
|
||||
|
|
||||
for (let file of fileUploads.uploads) { |
|
||||
if (file.uploaded) continue |
|
||||
|
|
||||
const attachmentUpload = exports.prepareUpload({ |
|
||||
file, |
|
||||
s3Key: `assets/${appId}/attachments/${file.processedFileName}`, |
|
||||
bucket, |
|
||||
metadata, |
|
||||
}) |
|
||||
|
|
||||
uploads.push(attachmentUpload) |
|
||||
|
|
||||
// mark file as uploaded
|
|
||||
file.uploaded = true |
|
||||
} |
|
||||
|
|
||||
db.put(fileUploads) |
|
||||
|
|
||||
try { |
|
||||
return await Promise.all(uploads) |
|
||||
} catch (err) { |
|
||||
console.error("Error uploading budibase app assets to s3", err) |
|
||||
throw err |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.performReplication = (appId, session, dbUrl) => { |
|
||||
return new Promise((resolve, reject) => { |
|
||||
const local = new PouchDB(appId) |
|
||||
|
|
||||
const remote = new CouchDB(`${dbUrl}/${appId}`, { |
|
||||
fetch: function (url, opts) { |
|
||||
opts.headers.set("Cookie", `${session};`) |
|
||||
return CouchDB.fetch(url, opts) |
|
||||
}, |
|
||||
}) |
|
||||
|
|
||||
const replication = local.sync(remote) |
|
||||
|
|
||||
replication.on("complete", () => resolve()) |
|
||||
replication.on("error", err => reject(err)) |
|
||||
}) |
|
||||
} |
|
||||
@ -1,86 +0,0 @@ |
|||||
const CouchDB = require("../../db") |
|
||||
const { StaticDatabases } = require("../../db/utils") |
|
||||
const { getDeployedApps } = require("../../utilities/workerRequests") |
|
||||
|
|
||||
const PROD_HOSTING_URL = "app.budi.live" |
|
||||
|
|
||||
function getProtocol(hostingInfo) { |
|
||||
return hostingInfo.useHttps ? "https://" : "http://" |
|
||||
} |
|
||||
|
|
||||
async function getURLWithPath(pathIfSelfHosted) { |
|
||||
const hostingInfo = await exports.getHostingInfo() |
|
||||
const protocol = getProtocol(hostingInfo) |
|
||||
const path = |
|
||||
hostingInfo.type === exports.HostingTypes.SELF ? pathIfSelfHosted : "" |
|
||||
return `${protocol}${hostingInfo.hostingUrl}${path}` |
|
||||
} |
|
||||
|
|
||||
exports.HostingTypes = { |
|
||||
CLOUD: "cloud", |
|
||||
SELF: "self", |
|
||||
} |
|
||||
|
|
||||
exports.getHostingInfo = async () => { |
|
||||
const db = new CouchDB(StaticDatabases.BUILDER_HOSTING.name) |
|
||||
let doc |
|
||||
try { |
|
||||
doc = await db.get(StaticDatabases.BUILDER_HOSTING.baseDoc) |
|
||||
} catch (err) { |
|
||||
// don't write this doc, want to be able to update these default props
|
|
||||
// for our servers with a new release without needing to worry about state of
|
|
||||
// PouchDB in peoples installations
|
|
||||
doc = { |
|
||||
_id: StaticDatabases.BUILDER_HOSTING.baseDoc, |
|
||||
type: exports.HostingTypes.CLOUD, |
|
||||
hostingUrl: PROD_HOSTING_URL, |
|
||||
selfHostKey: "", |
|
||||
templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com", |
|
||||
useHttps: true, |
|
||||
} |
|
||||
} |
|
||||
return doc |
|
||||
} |
|
||||
|
|
||||
exports.getAppUrl = async appId => { |
|
||||
const hostingInfo = await exports.getHostingInfo() |
|
||||
const protocol = getProtocol(hostingInfo) |
|
||||
let url |
|
||||
if (hostingInfo.type === exports.HostingTypes.CLOUD) { |
|
||||
url = `${protocol}${appId}.${hostingInfo.hostingUrl}` |
|
||||
} else { |
|
||||
url = `${protocol}${hostingInfo.hostingUrl}/app` |
|
||||
} |
|
||||
return url |
|
||||
} |
|
||||
|
|
||||
exports.getWorkerUrl = async () => { |
|
||||
return getURLWithPath("/worker") |
|
||||
} |
|
||||
|
|
||||
exports.getMinioUrl = async () => { |
|
||||
return getURLWithPath("/") |
|
||||
} |
|
||||
|
|
||||
exports.getCouchUrl = async () => { |
|
||||
return getURLWithPath("/db") |
|
||||
} |
|
||||
|
|
||||
exports.getSelfHostKey = async () => { |
|
||||
const hostingInfo = await exports.getHostingInfo() |
|
||||
return hostingInfo.selfHostKey |
|
||||
} |
|
||||
|
|
||||
exports.getTemplatesUrl = async (appId, type, name) => { |
|
||||
const hostingInfo = await exports.getHostingInfo() |
|
||||
const protocol = getProtocol(hostingInfo) |
|
||||
let path |
|
||||
if (type && name) { |
|
||||
path = `templates/type/${name}.tar.gz` |
|
||||
} else { |
|
||||
path = "manifest.json" |
|
||||
} |
|
||||
return `${protocol}${hostingInfo.templatesUrl}/${path}` |
|
||||
} |
|
||||
|
|
||||
exports.getDeployedApps = getDeployedApps |
|
||||
@ -0,0 +1,54 @@ |
|||||
|
const { Client, utils } = require("@budibase/auth/redis") |
||||
|
const { getGlobalIDFromUserMetadataID } = require("../db/utils") |
||||
|
|
||||
|
const APP_DEV_LOCK_SECONDS = 600 |
||||
|
const DB_NAME = utils.Databases.DEV_LOCKS |
||||
|
let devAppClient |
||||
|
|
||||
|
// we init this as we want to keep the connection open all the time
|
||||
|
// reduces the performance hit
|
||||
|
exports.init = async () => { |
||||
|
devAppClient = await new Client(DB_NAME).init() |
||||
|
} |
||||
|
|
||||
|
exports.doesUserHaveLock = async (devAppId, user) => { |
||||
|
const value = await devAppClient.get(devAppId) |
||||
|
if (!value) { |
||||
|
return true |
||||
|
} |
||||
|
// make sure both IDs are global
|
||||
|
const expected = getGlobalIDFromUserMetadataID(value._id) |
||||
|
const userId = getGlobalIDFromUserMetadataID(user._id) |
||||
|
return expected === userId |
||||
|
} |
||||
|
|
||||
|
exports.getAllLocks = async () => { |
||||
|
const locks = await devAppClient.scan() |
||||
|
return locks.map(lock => ({ |
||||
|
appId: lock.key, |
||||
|
user: lock.value, |
||||
|
})) |
||||
|
} |
||||
|
|
||||
|
exports.updateLock = async (devAppId, user) => { |
||||
|
// make sure always global user ID
|
||||
|
const globalId = getGlobalIDFromUserMetadataID(user._id) |
||||
|
const inputUser = { |
||||
|
...user, |
||||
|
userId: globalId, |
||||
|
_id: globalId, |
||||
|
} |
||||
|
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS) |
||||
|
} |
||||
|
|
||||
|
exports.clearLock = async (devAppId, user) => { |
||||
|
const value = await devAppClient.get(devAppId) |
||||
|
if (!value) { |
||||
|
return |
||||
|
} |
||||
|
const userId = getGlobalIDFromUserMetadataID(user._id) |
||||
|
if (value._id !== userId) { |
||||
|
throw "User does not hold lock, cannot clear it." |
||||
|
} |
||||
|
await devAppClient.delete(devAppId) |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
const { getAllRoles } = require("@budibase/auth/roles") |
||||
|
const { getAllApps } = require("@budibase/auth/db") |
||||
|
|
||||
|
exports.fetch = async ctx => { |
||||
|
// always use the dev apps as they'll be most up to date (true)
|
||||
|
const apps = await getAllApps(true) |
||||
|
const promises = [] |
||||
|
for (let app of apps) { |
||||
|
promises.push(getAllRoles(app._id)) |
||||
|
} |
||||
|
const roles = await Promise.all(promises) |
||||
|
const response = {} |
||||
|
for (let app of apps) { |
||||
|
response[app._id] = roles.shift() |
||||
|
} |
||||
|
ctx.body = response |
||||
|
} |
||||
|
|
||||
|
exports.find = async ctx => { |
||||
|
const appId = ctx.params.appId |
||||
|
ctx.body = { |
||||
|
roles: await getAllRoles(appId) |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue