SSL support for digitalocean, started utility function for BB logout, bunch of minor bug fixes

This commit is contained in:
Martin McKeaveney 2021-10-12 16:13:54 +01:00
parent 5ae5d8ddf0
commit 14370c81b9
15 changed files with 131 additions and 14 deletions

View File

@ -0,0 +1,10 @@
{
"folders": [
{
"path": "../.."
},
{
"path": "../../../budibase-infra"
}
]
}

View File

@ -42,8 +42,9 @@ module.exports = (
internal = false internal = false
if (authCookie) { if (authCookie) {
let error = null let error = null
const sessionId = authCookie.sessionId, const sessionId = authCookie.sessionId
userId = authCookie.userId const userId = authCookie.userId
const session = await getSession(userId, sessionId) const session = await getSession(userId, sessionId)
if (!session) { if (!session) {
error = "No session found" error = "No session found"

View File

@ -24,17 +24,24 @@ exports.createASession = async (userId, session) => {
await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS) await client.store(makeSessionID(userId, sessionId), session, EXPIRY_SECONDS)
} }
exports.invalidateSessions = async (userId, sessionId = null) => { exports.invalidateSessions = async (userId, sessionIds = null) => {
let sessions = [] let sessions = []
if (sessionId) {
sessions.push({ key: makeSessionID(userId, sessionId) }) // If no sessionIds, get all the sessions for the user
} else { if (!sessionIds) {
sessions = await getSessionsForUser(userId) sessions = await getSessionsForUser(userId)
sessions.forEach( sessions.forEach(
session => session =>
(session.key = makeSessionID(session.userId, session.sessionId)) (session.key = makeSessionID(session.userId, session.sessionId))
) )
} else {
// use the passed array of sessionIds
sessions = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
sessions = sessions.map(sessionId => ({
key: makeSessionID(userId, sessionId),
}))
} }
const client = await redis.getSessionClient() const client = await redis.getSessionClient()
const promises = [] const promises = []
for (let session of sessions) { for (let session of sessions) {

View File

@ -7,7 +7,7 @@ const {
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const { options } = require("./middleware/passport/jwt") const { options } = require("./middleware/passport/jwt")
const { createUserEmailView } = require("./db/views") const { createUserEmailView } = require("./db/views")
const { Headers, UserStatus } = require("./constants") const { Headers, UserStatus, Cookies } = require("./constants")
const { const {
getGlobalDB, getGlobalDB,
updateTenantId, updateTenantId,
@ -19,6 +19,10 @@ const accounts = require("./cloud/accounts")
const { hash } = require("./hashing") const { hash } = require("./hashing")
const userCache = require("./cache/user") const userCache = require("./cache/user")
const env = require("./environment") const env = require("./environment")
const {
getSessionsForUser,
invalidateSessions,
} = require("./security/sessions")
const APP_PREFIX = DocumentTypes.APP + SEPARATOR const APP_PREFIX = DocumentTypes.APP + SEPARATOR
@ -235,3 +239,23 @@ exports.saveUser = async (
} }
} }
} }
/**
* Logs a user out from budibase. Re-used across account portal and builder.
*/
exports.logout = async ({ ctx, userId, sessionId, keepActiveSession }) => {
let sessions = await getSessionsForUser(userId)
if (keepActiveSession) {
sessions = sessions.filter(session => session.sessionId !== sessionId)
}
await invalidateSessions(
userId,
sessions.map(({ sessionId }) => sessionId)
)
// clear cookies
this.clearCookie(ctx, Cookies.Auth)
this.clearCookie(ctx, Cookies.CurrentApp)
}

View File

@ -5,7 +5,7 @@
// *********************************************** // ***********************************************
// //
Cypress.on('uncaught:exception', (err, runnable) => { Cypress.on("uncaught:exception", () => {
return false return false
}) })

View File

@ -32,6 +32,7 @@
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type const LINK_TYPE = FIELDS.LINK.type
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const PROHIBITED_COLUMN_NAMES = ["type", "_id", "_rev"]
const { hide } = getContext(Context.Modal) const { hide } = getContext(Context.Modal)
let fieldDefinitions = cloneDeep(FIELDS) let fieldDefinitions = cloneDeep(FIELDS)
@ -66,7 +67,11 @@
(field.type === LINK_TYPE && !field.tableId) || (field.type === LINK_TYPE && !field.tableId) ||
Object.keys($tables.draft?.schema ?? {}).some( Object.keys($tables.draft?.schema ?? {}).some(
key => key !== originalName && key === field.name key => key !== originalName && key === field.name
) ) ||
columnNameInvalid
$: columnNameInvalid = PROHIBITED_COLUMN_NAMES.some(
name => field.name === name
)
// used to select what different options can be displayed for column type // used to select what different options can be displayed for column type
$: canBeSearched = $: canBeSearched =
@ -200,6 +205,9 @@
label="Name" label="Name"
bind:value={field.name} bind:value={field.name}
disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)} disabled={uneditable || (linkEditDisabled && field.type === LINK_TYPE)}
error={columnNameInvalid
? "type, _id and _rev are disallowed as column names"
: ""}
/> />
<Select <Select

View File

@ -1,10 +1,18 @@
<script> <script>
import { Label, Input, Layout, Toggle, Button } from "@budibase/bbui" import {
Label,
Input,
Layout,
Toggle,
Button,
TextArea,
} from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte" import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "helpers" import { capitalise } from "helpers"
export let integration export let integration
export let schema export let schema
let addButton let addButton
</script> </script>
@ -29,6 +37,15 @@
<Label>{capitalise(configKey)}</Label> <Label>{capitalise(configKey)}</Label>
<Toggle text="" bind:value={integration[configKey]} /> <Toggle text="" bind:value={integration[configKey]} />
</div> </div>
{:else if schema[configKey].type === "longForm"}
<div class="form-row">
<Label>{capitalise(configKey)}</Label>
<TextArea
type={schema[configKey].type}
on:change
bind:value={integration[configKey]}
/>
</div>
{:else} {:else}
<div class="form-row"> <div class="form-row">
<Label>{capitalise(configKey)}</Label> <Label>{capitalise(configKey)}</Label>

View File

@ -60,7 +60,7 @@
</Modal> </Modal>
<Modal bind:this={externalDatasourceModal}> <Modal bind:this={externalDatasourceModal}>
<DatasourceConfigModal {integration} /> <DatasourceConfigModal {integration} {modal} />
</Modal> </Modal>
<Modal bind:this={modal}> <Modal bind:this={modal}>

View File

@ -64,7 +64,7 @@
? "Fetch tables from database" ? "Fetch tables from database"
: "Save and continue to query"} : "Save and continue to query"}
cancelText="Back" cancelText="Back"
size="M" size="L"
> >
<Layout noPadding> <Layout noPadding>
<Body size="XS" <Body size="XS"

View File

@ -77,7 +77,7 @@ exports.run = async function ({ inputs }) {
const { status, message } = await getFetchResponse(response) const { status, message } = await getFetchResponse(response)
return { return {
httpStatus: status, httpStatus: status,
success: status === 200, success: status === 200 || status === 204,
response: message, response: message,
} }
} }

View File

@ -20,12 +20,14 @@ export enum QueryTypes {
export enum DatasourceFieldTypes { export enum DatasourceFieldTypes {
STRING = "string", STRING = "string",
LONGFORM = "longForm",
BOOLEAN = "boolean", BOOLEAN = "boolean",
NUMBER = "number", NUMBER = "number",
PASSWORD = "password", PASSWORD = "password",
LIST = "list", LIST = "list",
OBJECT = "object", OBJECT = "object",
JSON = "json", JSON = "json",
FILE = "file",
} }
export enum SourceNames { export enum SourceNames {

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEQTCCAqmgAwIBAgIUMW63R07yQUF/38L/MpYzsfZd1iUwDQYJKoZIhvcNAQEM
BQAwOjE4MDYGA1UEAwwvNzNjZWI4ZTAtZGE3MC00MzIxLWIyOTEtYjM1Mzk0ZWMw
NDhiIFByb2plY3QgQ0EwHhcNMjEwNTE3MDU0MTQ0WhcNMzEwNTE1MDU0MTQ0WjA6
MTgwNgYDVQQDDC83M2NlYjhlMC1kYTcwLTQzMjEtYjI5MS1iMzUzOTRlYzA0OGIg
UHJvamVjdCBDQTCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBANNpSowg
GurJ0hE4/Lgmg4G3u0AuUqKEsIXikjLl/vTDZV4tzPDgXtrzyaXeI6BgS4/Rcyxg
KmGCMJIqaiUI/svSz7zMB4jJefBPRDm6CGU3KjoqNR84DYUZHLoRujvLm8WYROUL
qHgm3EN4OAkifoO1wqqQk6rZR+EMDF+WKQ38wsE9bCm+lYjpPGY4MOoqKZtxtj8H
1UnH05CC6I0Cv1GUKHYxFqqHhwt5ICa0MSNR6PZGvJXgZUj1uo0XhwEXrZHtbh22
U0vVLgaP/85YcpO6Q7qHL249yHsKF9rGZqlSzCHg5hWe+uzzwFAd7LH4D61LRBYk
xGjCK9vzIjylIRRhlpesA9RHUWCvFe7Tw6YLQqrMGnbBYfEVUW2peFy9y7CuZLTb
persBctuICq4UlBDFhy8zsjCSOkfy9jZIx9Se4+C4yMIl6st7x8jlozOeDHIxSvR
REcJUIy0udQ6AkCoccMHM/qh+IggsD4ubSUsBKDMcyYTWzsiIesbidhC/wIDAQAB
oz8wPTAdBgNVHQ4EFgQUpHa3jpKqkIYen1YiWtXuUDcJ/CUwDwYDVR0TBAgwBgEB
/wIBADALBgNVHQ8EBAMCAQYwDQYJKoZIhvcNAQEMBQADggGBABUe16pNHSF9ZlK5
V9Y45T7qI526zKOf1zUqZOtX35EaQRZ6yvwO4yl1B6BoFk1v1w8jFCmpRNwkBDYr
uGVfZ+mBiAF6Djgrkw5Yd5atLtsk8jLHzBk00gEt9VlAQgfivc+s9kRqig4dOG25
yiHNChRgMTvifQauXq71/5L6N1sgE8XEljj5/kY5C/YeM5/52ja/Bx5mHY5qtxxF
7+CIpUyTZSxuJUPp1F98tpTRiuJDIK60ahmFmvEUQthVZlAx1XzOidATeTtUEnvw
CVmQyabgp5ewmRNjER4DRJpbpRzf1UUrGxjVHCx8/mn6nf2or0AtsSNoFPGugU1z
hSqHk3SmEC8uSBVCLZuyIG496OE0RYIC7KG7Lg3X3UHCBHngpCKoA+V1z5P5kPg3
CPpNvQOuNIEpOEUPgjjbxbVDHsvZH7ix8OZ31K8ioEs9SvXXAW3fSnHxqEBe2LjN
zHHBiQEJSyL1u4Nx/NahJtP22DUfF6uHdHtpdhxEKVW6GvhNcw==
-----END CERTIFICATE-----

View File

@ -28,6 +28,8 @@ module PostgresModule {
user: string user: string
password: string password: string
ssl?: boolean ssl?: boolean
ca?: string
rejectUnauthorized?: boolean
} }
const SCHEMA: Integration = { const SCHEMA: Integration = {
@ -67,6 +69,16 @@ module PostgresModule {
default: false, default: false,
required: false, required: false,
}, },
rejectUnauthorized: {
type: DatasourceFieldTypes.BOOLEAN,
default: false,
required: false,
},
ca: {
type: DatasourceFieldTypes.LONGFORM,
default: false,
required: false,
},
}, },
query: { query: {
create: { create: {
@ -144,7 +156,13 @@ module PostgresModule {
let newConfig = { let newConfig = {
...this.config, ...this.config,
ssl: this.config.ssl ? { rejectUnauthorized: true } : undefined, // ssl: this.config.ssl ? { rejectUnauthorized: true } : undefined,
ssl: this.config.ssl
? {
rejectUnauthorized: this.config.rejectUnauthorized,
ca: this.config.ca,
}
: undefined,
} }
if (!this.pool) { if (!this.pool) {
this.pool = new Pool(newConfig) this.pool = new Pool(newConfig)

View File

@ -14,6 +14,7 @@ const {
isMultiTenant, isMultiTenant,
} = require("@budibase/auth/tenancy") } = require("@budibase/auth/tenancy")
const env = require("../../../environment") const env = require("../../../environment")
const { endSession } = require("../../../../../auth/sessions")
function googleCallbackUrl(config) { function googleCallbackUrl(config) {
// incase there is a callback URL from before // incase there is a callback URL from before
@ -121,8 +122,10 @@ exports.resetUpdate = async ctx => {
} }
exports.logout = async ctx => { exports.logout = async ctx => {
const authCookie = getCookie(ctx, Cookies.Auth)
clearCookie(ctx, Cookies.Auth) clearCookie(ctx, Cookies.Auth)
clearCookie(ctx, Cookies.CurrentApp) clearCookie(ctx, Cookies.CurrentApp)
await endSession(authCookie.sessionId)
ctx.body = { message: "User logged out." } ctx.body = { message: "User logged out." }
} }

View File

@ -171,7 +171,9 @@ exports.updateSelf = async ctx => {
const db = getGlobalDB() const db = getGlobalDB()
const user = await db.get(ctx.user._id) const user = await db.get(ctx.user._id)
if (ctx.request.body.password) { if (ctx.request.body.password) {
// changing password
ctx.request.body.password = await hash(ctx.request.body.password) ctx.request.body.password = await hash(ctx.request.body.password)
await invalidateSessions(ctx.user._id)
} }
// don't allow sending up an ID/Rev, always use the existing one // don't allow sending up an ID/Rev, always use the existing one
delete ctx.request.body._id delete ctx.request.body._id