diff --git a/README.md b/README.md
index 0f4cfe31c2..7d11ea570f 100644
--- a/README.md
+++ b/README.md
@@ -201,9 +201,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
seoulaja 🌍 |
Maurits Lourens ⚠️ 💻 |
-
- Rory Powell 🚇 ⚠️ 💻 |
-
diff --git a/lerna.json b/lerna.json
index 7564ea387c..2d503eaf59 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "1.0.46-alpha.3",
+ "version": "1.0.49-alpha.0",
"npmClient": "yarn",
"packages": [
"packages/*"
diff --git a/packages/backend-core/package.json b/packages/backend-core/package.json
index c4f7dd1ae8..18a6281aa0 100644
--- a/packages/backend-core/package.json
+++ b/packages/backend-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/backend-core",
- "version": "1.0.46-alpha.3",
+ "version": "1.0.49-alpha.0",
"description": "Budibase backend core libraries used in server and worker",
"main": "src/index.js",
"author": "Budibase",
diff --git a/packages/backend-core/src/auth.js b/packages/backend-core/src/auth.js
index 7f66d887ae..41d2bb1cc5 100644
--- a/packages/backend-core/src/auth.js
+++ b/packages/backend-core/src/auth.js
@@ -12,6 +12,7 @@ const {
tenancy,
appTenancy,
authError,
+ csrf,
} = require("./middleware")
// Strategies
@@ -42,4 +43,5 @@ module.exports = {
buildAppTenancyMiddleware: appTenancy,
auditLog,
authError,
+ buildCsrfMiddleware: csrf,
}
diff --git a/packages/backend-core/src/constants.js b/packages/backend-core/src/constants.js
index 8e6b01608e..559dc0e6b2 100644
--- a/packages/backend-core/src/constants.js
+++ b/packages/backend-core/src/constants.js
@@ -7,8 +7,8 @@ exports.Cookies = {
CurrentApp: "budibase:currentapp",
Auth: "budibase:auth",
Init: "budibase:init",
+ DatasourceAuth: "budibase:datasourceauth",
OIDC_CONFIG: "budibase:oidc:config",
- RETURN_URL: "budibase:returnurl",
}
exports.Headers = {
@@ -18,6 +18,7 @@ exports.Headers = {
TYPE: "x-budibase-type",
TENANT_ID: "x-budibase-tenant-id",
TOKEN: "x-budibase-token",
+ CSRF_TOKEN: "x-csrf-token",
}
exports.GlobalRoles = {
diff --git a/packages/backend-core/src/middleware/authenticated.js b/packages/backend-core/src/middleware/authenticated.js
index 87bd4d35ce..4978f7b9dc 100644
--- a/packages/backend-core/src/middleware/authenticated.js
+++ b/packages/backend-core/src/middleware/authenticated.js
@@ -60,6 +60,7 @@ module.exports = (
} else {
user = await getUser(userId, session.tenantId)
}
+ user.csrfToken = session.csrfToken
delete user.password
authenticated = true
} catch (err) {
diff --git a/packages/backend-core/src/middleware/csrf.js b/packages/backend-core/src/middleware/csrf.js
new file mode 100644
index 0000000000..12bd9473e6
--- /dev/null
+++ b/packages/backend-core/src/middleware/csrf.js
@@ -0,0 +1,78 @@
+const { Headers } = require("../constants")
+const { buildMatcherRegex, matches } = require("./matchers")
+
+/**
+ * GET, HEAD and OPTIONS methods are considered safe operations
+ *
+ * POST, PUT, PATCH, and DELETE methods, being state changing verbs,
+ * should have a CSRF token attached to the request
+ */
+const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"]
+
+/**
+ * There are only three content type values that can be used in cross domain requests.
+ * If any other value is used, e.g. application/json, the browser will first make a OPTIONS
+ * request which will be protected by CORS.
+ */
+const INCLUDED_CONTENT_TYPES = [
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ "text/plain",
+]
+
+/**
+ * Validate the CSRF token generated aganst the user session.
+ * Compare the token with the x-csrf-token header.
+ *
+ * If the token is not found within the request or the value provided
+ * does not match the value within the user session, the request is rejected.
+ *
+ * CSRF protection provided using the 'Synchronizer Token Pattern'
+ * https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
+ *
+ */
+module.exports = (opts = { noCsrfPatterns: [] }) => {
+ const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns)
+ return async (ctx, next) => {
+ // don't apply for excluded paths
+ const found = matches(ctx, noCsrfOptions)
+ if (found) {
+ return next()
+ }
+
+ // don't apply for the excluded http methods
+ if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) {
+ return next()
+ }
+
+ // don't apply when the content type isn't supported
+ let contentType = ctx.get("content-type")
+ ? ctx.get("content-type").toLowerCase()
+ : ""
+ if (
+ !INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length
+ ) {
+ return next()
+ }
+
+ // don't apply csrf when the internal api key has been used
+ if (ctx.internal) {
+ return next()
+ }
+
+ // apply csrf when there is a token in the session (new logins)
+ // in future there should be a hard requirement that the token is present
+ const userToken = ctx.user.csrfToken
+ if (!userToken) {
+ return next()
+ }
+
+ // reject if no token in request or mismatch
+ const requestToken = ctx.get(Headers.CSRF_TOKEN)
+ if (!requestToken || requestToken !== userToken) {
+ ctx.throw(403, "Invalid CSRF token")
+ }
+
+ return next()
+ }
+}
diff --git a/packages/backend-core/src/middleware/index.js b/packages/backend-core/src/middleware/index.js
index cf8676a2bc..0d01fb3952 100644
--- a/packages/backend-core/src/middleware/index.js
+++ b/packages/backend-core/src/middleware/index.js
@@ -7,6 +7,8 @@ const authenticated = require("./authenticated")
const auditLog = require("./auditLog")
const tenancy = require("./tenancy")
const appTenancy = require("./appTenancy")
+const datasourceGoogle = require("./passport/datasource/google")
+const csrf = require("./csrf")
module.exports = {
google,
@@ -18,4 +20,8 @@ module.exports = {
tenancy,
appTenancy,
authError,
+ datasource: {
+ google: datasourceGoogle,
+ },
+ csrf,
}
diff --git a/packages/backend-core/src/middleware/passport/datasource/google.js b/packages/backend-core/src/middleware/passport/datasource/google.js
new file mode 100644
index 0000000000..bfc2e4a61e
--- /dev/null
+++ b/packages/backend-core/src/middleware/passport/datasource/google.js
@@ -0,0 +1,76 @@
+const { getScopedConfig } = require("../../../db/utils")
+const { getGlobalDB } = require("../../../tenancy")
+const google = require("../google")
+const { Configs, Cookies } = require("../../../constants")
+const { clearCookie, getCookie } = require("../../../utils")
+const { getDB } = require("../../../db")
+
+async function preAuth(passport, ctx, next) {
+ const db = getGlobalDB()
+ // get the relevant config
+ const config = await getScopedConfig(db, {
+ type: Configs.GOOGLE,
+ workspace: ctx.query.workspace,
+ })
+ const publicConfig = await getScopedConfig(db, {
+ type: Configs.SETTINGS,
+ })
+ let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
+ const strategy = await google.strategyFactory(config, callbackUrl)
+
+ if (!ctx.query.appId || !ctx.query.datasourceId) {
+ ctx.throw(400, "appId and datasourceId query params not present.")
+ }
+
+ return passport.authenticate(strategy, {
+ scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"],
+ accessType: "offline",
+ prompt: "consent",
+ })(ctx, next)
+}
+
+async function postAuth(passport, ctx, next) {
+ const db = getGlobalDB()
+
+ const config = await getScopedConfig(db, {
+ type: Configs.GOOGLE,
+ workspace: ctx.query.workspace,
+ })
+
+ const publicConfig = await getScopedConfig(db, {
+ type: Configs.SETTINGS,
+ })
+
+ let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback`
+ const strategy = await google.strategyFactory(
+ config,
+ callbackUrl,
+ (accessToken, refreshToken, profile, done) => {
+ clearCookie(ctx, Cookies.DatasourceAuth)
+ done(null, { accessToken, refreshToken })
+ }
+ )
+
+ const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth)
+
+ return passport.authenticate(
+ strategy,
+ { successRedirect: "/", failureRedirect: "/error" },
+ async (err, tokens) => {
+ // update the DB for the datasource with all the user info
+ const db = getDB(authStateCookie.appId)
+ const datasource = await db.get(authStateCookie.datasourceId)
+ if (!datasource.config) {
+ datasource.config = {}
+ }
+ datasource.config.auth = { type: "google", ...tokens }
+ await db.put(datasource)
+ ctx.redirect(
+ `/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}`
+ )
+ }
+ )(ctx, next)
+}
+
+exports.preAuth = preAuth
+exports.postAuth = postAuth
diff --git a/packages/backend-core/src/security/sessions.js b/packages/backend-core/src/security/sessions.js
index ad21627bd9..bbe6be299d 100644
--- a/packages/backend-core/src/security/sessions.js
+++ b/packages/backend-core/src/security/sessions.js
@@ -1,4 +1,5 @@
const redis = require("../redis/authRedis")
+const { v4: uuidv4 } = require("uuid")
// a week in seconds
const EXPIRY_SECONDS = 86400 * 7
@@ -16,6 +17,9 @@ function makeSessionID(userId, sessionId) {
exports.createASession = async (userId, session) => {
const client = await redis.getSessionClient()
const sessionId = session.sessionId
+ if (!session.csrfToken) {
+ session.csrfToken = uuidv4()
+ }
session = {
createdAt: new Date().toISOString(),
lastAccessedAt: new Date().toISOString(),
diff --git a/packages/backend-core/src/utils.js b/packages/backend-core/src/utils.js
index 85dd32946f..8c00f2a8b8 100644
--- a/packages/backend-core/src/utils.js
+++ b/packages/backend-core/src/utils.js
@@ -96,12 +96,7 @@ exports.getCookie = (ctx, name) => {
* @param {string|object} value The value of cookie which will be set.
* @param {object} opts options like whether to sign.
*/
-exports.setCookie = (
- ctx,
- value,
- name = "builder",
- opts = { sign: true, requestDomain: false }
-) => {
+exports.setCookie = (ctx, value, name = "builder", opts = { sign: true }) => {
if (value && opts && opts.sign) {
value = jwt.sign(value, options.secretOrKey)
}
@@ -113,7 +108,7 @@ exports.setCookie = (
overwrite: true,
}
- if (environment.COOKIE_DOMAIN && !opts.requestDomain) {
+ if (environment.COOKIE_DOMAIN) {
config.domain = environment.COOKIE_DOMAIN
}
diff --git a/packages/backend-core/yarn.lock b/packages/backend-core/yarn.lock
index f28f2f932f..fc70e3d6a1 100644
--- a/packages/backend-core/yarn.lock
+++ b/packages/backend-core/yarn.lock
@@ -3410,9 +3410,9 @@ node-fetch@2.6.0:
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
node-fetch@^2.6.1:
- version "2.6.6"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
- integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+ integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
diff --git a/packages/bbui/package.json b/packages/bbui/package.json
index ecc5bf5a1f..d7757d6181 100644
--- a/packages/bbui/package.json
+++ b/packages/bbui/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/bbui",
"description": "A UI solution used in the different Budibase projects.",
- "version": "1.0.46-alpha.3",
+ "version": "1.0.49-alpha.0",
"license": "MPL-2.0",
"svelte": "src/index.js",
"module": "dist/bbui.es.js",
diff --git a/packages/bbui/src/Form/Core/Dropzone.svelte b/packages/bbui/src/Form/Core/Dropzone.svelte
index f7fed78b70..6b8022a36c 100644
--- a/packages/bbui/src/Form/Core/Dropzone.svelte
+++ b/packages/bbui/src/Form/Core/Dropzone.svelte
@@ -147,7 +147,9 @@
{:else}
-
{selectedImage.extension}
+
+ {selectedImage.name || "Unknown file"}
+
Preview not supported
{/if}
@@ -359,18 +361,21 @@
white-space: nowrap;
width: 0;
margin-right: 10px;
+ user-select: all;
}
.placeholder {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+ text-align: center;
}
.extension {
color: var(--spectrum-global-color-gray-600);
text-transform: uppercase;
font-weight: 600;
margin-bottom: 5px;
+ user-select: all;
}
.nav {
diff --git a/packages/builder/package.json b/packages/builder/package.json
index 474213899e..3982c0965d 100644
--- a/packages/builder/package.json
+++ b/packages/builder/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/builder",
- "version": "1.0.46-alpha.3",
+ "version": "1.0.49-alpha.0",
"license": "GPL-3.0",
"private": true,
"scripts": {
@@ -65,10 +65,10 @@
}
},
"dependencies": {
- "@budibase/bbui": "^1.0.46-alpha.3",
- "@budibase/client": "^1.0.46-alpha.3",
+ "@budibase/bbui": "^1.0.49-alpha.0",
+ "@budibase/client": "^1.0.49-alpha.0",
"@budibase/colorpicker": "1.1.2",
- "@budibase/string-templates": "^1.0.46-alpha.3",
+ "@budibase/string-templates": "^1.0.49-alpha.0",
"@sentry/browser": "5.19.1",
"@spectrum-css/page": "^3.0.1",
"@spectrum-css/vars": "^3.0.1",
diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js
index a5c6ceba54..a932799701 100644
--- a/packages/builder/src/builderStore/api.js
+++ b/packages/builder/src/builderStore/api.js
@@ -1,12 +1,20 @@
import { store } from "./index"
import { get as svelteGet } from "svelte/store"
import { removeCookie, Cookies } from "./cookies"
+import { auth } from "stores/portal"
const apiCall =
method =>
async (url, body, headers = { "Content-Type": "application/json" }) => {
headers["x-budibase-app-id"] = svelteGet(store).appId
headers["x-budibase-api-version"] = "1"
+
+ // add csrf token if authenticated
+ const user = svelteGet(auth).user
+ if (user && user.csrfToken) {
+ headers["x-csrf-token"] = user.csrfToken
+ }
+
const json = headers["Content-Type"] === "application/json"
const resp = await fetch(url, {
method: method,
diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js
index 23704556ad..5181e756c6 100644
--- a/packages/builder/src/builderStore/index.js
+++ b/packages/builder/src/builderStore/index.js
@@ -1,6 +1,5 @@
import { getFrontendStore } from "./store/frontend"
import { getAutomationStore } from "./store/automation"
-import { getHostingStore } from "./store/hosting"
import { getThemeStore } from "./store/theme"
import { derived, writable } from "svelte/store"
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
@@ -9,7 +8,6 @@ import { findComponent } from "./componentUtils"
export const store = getFrontendStore()
export const automationStore = getAutomationStore()
export const themeStore = getThemeStore()
-export const hostingStore = getHostingStore()
export const currentAsset = derived(store, $store => {
const type = $store.currentFrontEndType
diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js
index fdfe450edf..0d740e08e0 100644
--- a/packages/builder/src/builderStore/store/frontend.js
+++ b/packages/builder/src/builderStore/store/frontend.js
@@ -2,7 +2,6 @@ import { get, writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import {
allScreens,
- hostingStore,
currentAsset,
mainLayout,
selectedComponent,
@@ -100,7 +99,6 @@ export const getFrontendStore = () => {
version: application.version,
revertableVersion: application.revertableVersion,
}))
- await hostingStore.actions.fetch()
// Initialise backend stores
const [_integrations] = await Promise.all([
diff --git a/packages/builder/src/builderStore/store/hosting.js b/packages/builder/src/builderStore/store/hosting.js
deleted file mode 100644
index fb174c2663..0000000000
--- a/packages/builder/src/builderStore/store/hosting.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { writable } from "svelte/store"
-import api, { get } from "../api"
-
-const INITIAL_HOSTING_UI_STATE = {
- appUrl: "",
- deployedApps: {},
- deployedAppNames: [],
- deployedAppUrls: [],
-}
-
-export const getHostingStore = () => {
- const store = writable({ ...INITIAL_HOSTING_UI_STATE })
- store.actions = {
- fetch: async () => {
- const response = await api.get("/api/hosting/urls")
- const urls = await response.json()
- store.update(state => {
- state.appUrl = urls.app
- return state
- })
- },
- fetchDeployedApps: async () => {
- let deployments = await (await get("/api/hosting/apps")).json()
- store.update(state => {
- state.deployedApps = deployments
- state.deployedAppNames = Object.values(deployments).map(app => app.name)
- state.deployedAppUrls = Object.values(deployments).map(app => app.url)
- return state
- })
- return deployments
- },
- }
- return store
-}
diff --git a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
index 19543be4c5..0d73f3d36d 100644
--- a/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
+++ b/packages/builder/src/components/backend/DataTable/modals/CreateEditColumn.svelte
@@ -22,8 +22,10 @@
RelationshipTypes,
ALLOWABLE_STRING_OPTIONS,
ALLOWABLE_NUMBER_OPTIONS,
+ ALLOWABLE_JSON_OPTIONS,
ALLOWABLE_STRING_TYPES,
ALLOWABLE_NUMBER_TYPES,
+ ALLOWABLE_JSON_TYPES,
SWITCHABLE_TYPES,
} from "constants/backend"
import { getAutoColumnInformation, buildAutoColumn } from "builderStore/utils"
@@ -245,6 +247,11 @@
ALLOWABLE_NUMBER_TYPES.indexOf(field.type) !== -1
) {
return ALLOWABLE_NUMBER_OPTIONS
+ } else if (
+ originalName &&
+ ALLOWABLE_JSON_TYPES.indexOf(field.type) !== -1
+ ) {
+ return ALLOWABLE_JSON_OPTIONS
} else if (!external) {
return [
...Object.values(fieldDefinitions),
diff --git a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte
index 819fb32e45..8805505c8d 100644
--- a/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte
+++ b/packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/PlusConfigForm.svelte
@@ -188,7 +188,7 @@
{:else}
No tables found.
{/if}
-{#if plusTables?.length !== 0}
+{#if plusTables?.length !== 0 && integration.relationships}