Updating worker to support using a self host key, a basic level of security, stopping builder from asking for API key if currently configured for self hosting, made the default values for self hosting make sense for a basic local installation, this should be final.

This commit is contained in:
mike12345567 2021-01-06 16:58:29 +00:00
parent 1c553a75df
commit 882cfa700b
16 changed files with 133 additions and 67 deletions

View File

@ -14,8 +14,8 @@ services:
LOGO_URL: ${LOGO_URL} LOGO_URL: ${LOGO_URL}
PORT: 4002 PORT: 4002
HOSTING_URL: ${HOSTING_URL} HOSTING_URL: ${HOSTING_URL}
MINIO_PORT: ${MINIO_PORT}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
PROXY_PORT: ${MAIN_PORT}
depends_on: depends_on:
- worker-service - worker-service
@ -33,6 +33,7 @@ services:
COUCH_DB_USERNAME: ${COUCH_DB_USER} COUCH_DB_USERNAME: ${COUCH_DB_USER}
COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD} COUCH_DB_PASSWORD: ${COUCH_DB_PASSWORD}
RAW_COUCH_DB_URL: http://couchdb-service:5984 RAW_COUCH_DB_URL: http://couchdb-service:5984
SELF_HOST_KEY: ${HOSTING_KEY}
depends_on: depends_on:
- minio-service - minio-service
- couch-init - couch-init
@ -40,7 +41,7 @@ services:
minio-service: minio-service:
image: minio/minio image: minio/minio
volumes: volumes:
- data1:/data - minio_data:/data
ports: ports:
- "${MINIO_PORT}:9000" - "${MINIO_PORT}:9000"
environment: environment:
@ -89,4 +90,5 @@ services:
volumes: volumes:
couchdb_data: couchdb_data:
driver: local driver: local
data1: minio_data:
driver: local

View File

@ -33,8 +33,8 @@ static_resources:
- match: { prefix: "/db/" } - match: { prefix: "/db/" }
route: route:
prefix_rewrite: "/"
cluster: couchdb-service cluster: couchdb-service
prefix_rewrite: "/"
# minio is on the default route because this works # minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy # best, minio + AWS SDK doesn't handle path proxy

View File

@ -1,14 +1,27 @@
# Use the main port in the builder for your self hosting URL, e.g. localhost:10000
MAIN_PORT=10000
# Use this password when configuring your self hosting settings
# This should be updated
HOSTING_KEY=budibase
# This section contains customisation options
HOSTING_URL=http://localhost
LOGO_URL=https://logoipsum.com/logo/logo-15.svg
HOSTING_URL=http://localhost
# This section contains all secrets pertaining to the system
# These should be updated
JWT_SECRET=testsecret
MINIO_ACCESS_KEY=budibase MINIO_ACCESS_KEY=budibase
MINIO_SECRET_KEY=budibase MINIO_SECRET_KEY=budibase
COUCH_DB_PASSWORD=budibase COUCH_DB_PASSWORD=budibase
COUCH_DB_USER=budibase COUCH_DB_USER=budibase
WORKER_API_KEY=budibase WORKER_API_KEY=budibase
BUDIBASE_ENVIRONMENT=PRODUCTION
HOSTING_URL=http://localhost # This section contains variables that do not need to be altered under normal circumstances
LOGO_URL=https://logoipsum.com/logo/logo-15.svg
MAIN_PORT=10000
APP_PORT=4002 APP_PORT=4002
WORKER_PORT=4003 WORKER_PORT=4003
MINIO_PORT=4004 MINIO_PORT=4004
COUCH_DB_PORT=4005 COUCH_DB_PORT=4005
JWT_SECRET=testsecret BUDIBASE_ENVIRONMENT=PRODUCTION

View File

@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
docker-compose --env-file hosting.properties up docker-compose --env-file hosting.properties up --build

View File

@ -6,14 +6,18 @@
import analytics from "analytics" import analytics from "analytics"
import { onMount } from "svelte" import { onMount } from "svelte"
let selfhosted = false
let hostingInfo let hostingInfo
let selfhosted = false
async function save() { async function save() {
if (!selfhosted) {
return
}
hostingInfo.type = selfhosted ? "self" : "cloud" hostingInfo.type = selfhosted ? "self" : "cloud"
if (!selfhosted && hostingInfo._rev) {
hostingInfo = {
type: hostingInfo.type,
_id: hostingInfo._id,
_rev: hostingInfo._rev,
}
}
try { try {
await hostingStore.actions.save(hostingInfo) await hostingStore.actions.save(hostingInfo)
notifier.success(`Settings saved.`) notifier.success(`Settings saved.`)
@ -22,6 +26,15 @@
} }
} }
function updateSelfHosting(event) {
if (hostingInfo.type === "cloud" && event.target.checked) {
hostingInfo.hostingUrl = "localhost:10000"
hostingInfo.useHttps = false
hostingInfo.selfHostKey = "budibase"
}
}
onMount(async () => { onMount(async () => {
hostingInfo = await hostingStore.actions.fetch() hostingInfo = await hostingStore.actions.fetch()
selfhosted = hostingInfo.type === "self" selfhosted = hostingInfo.type === "self"
@ -31,8 +44,7 @@
<ModalContent <ModalContent
title="Builder settings" title="Builder settings"
confirmText="Save" confirmText="Save"
onConfirm={save} onConfirm={save}>
showConfirmButton={selfhosted}>
<h5>Theme</h5> <h5>Theme</h5>
<ThemeEditor /> <ThemeEditor />
<h5>Hosting</h5> <h5>Hosting</h5>
@ -40,9 +52,10 @@
This section contains settings that relate to the deployment and hosting of This section contains settings that relate to the deployment and hosting of
apps made in this builder. apps made in this builder.
</p> </p>
<Toggle thin text="Self hosted" bind:checked={selfhosted} /> <Toggle thin text="Self hosted" on:change={updateSelfHosting} bind:checked={selfhosted} />
{#if selfhosted} {#if selfhosted}
<Input bind:value={hostingInfo.hostingUrl} label="Apps URL" /> <Input bind:value={hostingInfo.hostingUrl} label="Hosting URL" />
<Input bind:value={hostingInfo.selfHostKey} label="Hosting Key" />
<Toggle thin text="HTTPS" bind:checked={hostingInfo.useHttps} /> <Toggle thin text="HTTPS" bind:checked={hostingInfo.useHttps} />
{/if} {/if}
</ModalContent> </ModalContent>

View File

@ -1,6 +1,6 @@
<script> <script>
import { writable } from "svelte/store" import { writable } from "svelte/store"
import { store, automationStore, backendUiStore } from "builderStore" import { store, automationStore, backendUiStore, hostingStore } from "builderStore"
import { string, object } from "yup" import { string, object } from "yup"
import api, { get } from "builderStore/api" import api, { get } from "builderStore/api"
import Form from "@svelteschool/svelte-forms" import Form from "@svelteschool/svelte-forms"
@ -12,6 +12,7 @@
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
import { post } from "builderStore/api" import { post } from "builderStore/api"
import analytics from "analytics" import analytics from "analytics"
import {onMount} from "svelte"
//Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly //Move this to context="module" once svelte-forms is updated so that it can bind to stores correctly
const createAppStore = writable({ currentStep: 0, values: {} }) const createAppStore = writable({ currentStep: 0, values: {} })
@ -62,20 +63,27 @@
}, },
] ]
let steps = [ let steps = []
{
component: API, onMount(async () => {
errors, let hostingInfo = await hostingStore.actions.fetch()
}, steps = []
{ // only validate API key for Cloud
if (hostingInfo.type === "cloud") {
steps.push({
component: API,
errors,
})
}
steps.push({
component: Info, component: Info,
errors, errors,
}, })
{ steps.push({
component: User, component: User,
errors, errors,
}, })
] })
if (hasKey) { if (hasKey) {
validationSchemas.shift() validationSchemas.shift()

View File

@ -84,7 +84,10 @@ async function deployApp(deployment) {
} catch (err) { } catch (err) {
deployment.setStatus(DeploymentStatus.FAILURE, err.message) deployment.setStatus(DeploymentStatus.FAILURE, err.message)
await storeLocalDeploymentHistory(deployment) await storeLocalDeploymentHistory(deployment)
throw new Error(`Deployment Failed: ${err.message}`) throw {
...err,
message: `Deployment Failed: ${err.message}`,
}
} }
} }

View File

@ -1,4 +1,3 @@
const env = require("../../../environment")
const AWS = require("aws-sdk") const AWS = require("aws-sdk")
const { const {
deployToObjectStore, deployToObjectStore,
@ -9,26 +8,34 @@ const {
getWorkerUrl, getWorkerUrl,
getCouchUrl, getCouchUrl,
getMinioUrl, getMinioUrl,
getSelfHostKey,
} = require("../../../utilities/builder/hosting") } = require("../../../utilities/builder/hosting")
exports.preDeployment = async function() { exports.preDeployment = async function() {
const url = `${await getWorkerUrl()}/api/deploy` const url = `${await getWorkerUrl()}/api/deploy`
const json = await fetchCredentials(url, { try {
apiKey: env.BUDIBASE_API_KEY, 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 // 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() { exports.postDeployment = async function() {

View File

@ -17,6 +17,7 @@ exports.fetchCredentials = async function(url, body) {
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
body: JSON.stringify(body), body: JSON.stringify(body),
headers: { "Content-Type": "application/json" },
}) })
const json = await response.json() const json = await response.json()
@ -26,7 +27,7 @@ exports.fetchCredentials = async function(url, body) {
if (response.status !== 200) { if (response.status !== 200) {
throw new Error( throw new Error(
`Error fetching temporary credentials for api key: ${body.apiKey}` `Error fetching temporary credentials: ${JSON.stringify(json)}`
) )
} }

View File

@ -15,13 +15,17 @@ exports.fetchInfo = async ctx => {
exports.save = async ctx => { exports.save = async ctx => {
const db = new CouchDB(BUILDER_CONFIG_DB) const db = new CouchDB(BUILDER_CONFIG_DB)
const { type } = ctx.request.body const { type } = ctx.request.body
if (type === HostingTypes.CLOUD) { if (type === HostingTypes.CLOUD && ctx.request.body._rev) {
ctx.throw(400, "Cannot update Cloud hosting information") ctx.body = await db.remove({
...ctx.request.body,
_id: HOSTING_DOC,
})
} else {
ctx.body = await db.put({
...ctx.request.body,
_id: HOSTING_DOC,
})
} }
ctx.body = await db.put({
...ctx.request.body,
_id: HOSTING_DOC,
})
} }
exports.fetch = async ctx => { exports.fetch = async ctx => {

View File

@ -20,7 +20,7 @@ const env = require("../../../environment")
function objectStoreUrl() { function objectStoreUrl() {
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
// TODO: need a better way to handle this, probably reverse proxy // TODO: need a better way to handle this, probably reverse proxy
return `${env.HOSTING_URL}:${env.MINIO_PORT}/app-assets/assets` return `${env.HOSTING_URL}:${env.PROXY_PORT}/app-assets/assets`
} else { } else {
return "https://cdn.app.budi.live/assets" return "https://cdn.app.budi.live/assets"
} }

View File

@ -38,7 +38,7 @@ module.exports = {
LOCAL_TEMPLATES: process.env.LOCAL_TEMPLATES, LOCAL_TEMPLATES: process.env.LOCAL_TEMPLATES,
// self hosting features // self hosting features
HOSTING_URL: process.env.HOSTING_URL, HOSTING_URL: process.env.HOSTING_URL,
MINIO_PORT: process.env.MINIO_PORT, PROXY_PORT: process.env.PROXY_PORT,
LOGO_URL: process.env.LOGO_URL, LOGO_URL: process.env.LOGO_URL,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value

View File

@ -1,6 +1,8 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants") const { BUILDER_CONFIG_DB, HOSTING_DOC } = require("../../constants")
const PROD_HOSTING_URL = "app.budi.live"
function getProtocol(hostingInfo) { function getProtocol(hostingInfo) {
return hostingInfo.useHttps ? "https://" : "http://" return hostingInfo.useHttps ? "https://" : "http://"
} }
@ -30,7 +32,8 @@ exports.getHostingInfo = async () => {
doc = { doc = {
_id: HOSTING_DOC, _id: HOSTING_DOC,
type: exports.HostingTypes.CLOUD, type: exports.HostingTypes.CLOUD,
hostingUrl: "app.budi.live", hostingUrl: PROD_HOSTING_URL,
selfHostKey: "",
templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com", templatesUrl: "prod-budi-templates.s3-eu-west-1.amazonaws.com",
useHttps: true, useHttps: true,
} }
@ -62,6 +65,11 @@ exports.getCouchUrl = async () => {
return getURLWithPath("/db") return getURLWithPath("/db")
} }
exports.getSelfHostKey = async () => {
const hostingInfo = await exports.getHostingInfo()
return hostingInfo.selfHostKey
}
exports.getTemplatesUrl = async (appId, type, name) => { exports.getTemplatesUrl = async (appId, type, name) => {
const hostingInfo = await exports.getHostingInfo() const hostingInfo = await exports.getHostingInfo()
const protocol = getProtocol(hostingInfo) const protocol = getProtocol(hostingInfo)

View File

@ -10,9 +10,11 @@ const PUBLIC_READ_POLICY = {
Statement: [ Statement: [
{ {
Effect: "Allow", Effect: "Allow",
Principal: "*", Principal: {
AWS: ["*"]
},
Action: "s3:GetObject", Action: "s3:GetObject",
Resource: `arn:aws:s3:::${APP_BUCKET}/*`, Resource: [`arn:aws:s3:::${APP_BUCKET}/*`],
}, },
], ],
} }
@ -63,19 +65,18 @@ async function getMinioSession() {
Bucket: APP_BUCKET, Bucket: APP_BUCKET,
}) })
.promise() .promise()
} else if (err.statusCode === 403) {
await objClient
.putBucketPolicy({
Bucket: APP_BUCKET,
Policy: JSON.stringify(PUBLIC_READ_POLICY),
})
.promise()
} }
else { else {
throw err throw err
} }
} }
// TODO: need to do something better than this // always make sure policy is correct
await objClient
.putBucketPolicy({
Bucket: APP_BUCKET,
Policy: JSON.stringify(PUBLIC_READ_POLICY),
})
.promise()
// Ideally want to send back some pre-signed URLs for files that are to be uploaded // Ideally want to send back some pre-signed URLs for files that are to be uploaded
return { return {
accessKeyId: env.MINIO_ACCESS_KEY, accessKeyId: env.MINIO_ACCESS_KEY,

View File

@ -10,6 +10,7 @@ module.exports = {
RAW_MINIO_URL: process.env.RAW_MINIO_URL, RAW_MINIO_URL: process.env.RAW_MINIO_URL,
COUCH_DB_PORT: process.env.COUCH_DB_PORT, COUCH_DB_PORT: process.env.COUCH_DB_PORT,
MINIO_PORT: process.env.MINIO_PORT, MINIO_PORT: process.env.MINIO_PORT,
SELF_HOST_KEY: process.env.SELF_HOST_KEY,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value
module.exports[key] = value module.exports[key] = value

View File

@ -1,4 +1,9 @@
const env = require("../environment")
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
// TODO: need to check the API key provided in the header if (!ctx.request.body.selfHostKey || env.SELF_HOST_KEY !== ctx.request.body.selfHostKey) {
await next() ctx.throw(401, "Deployment unauthorised")
} else {
await next()
}
} }