prevent cross tenant app access

This commit is contained in:
Martin McKeaveney 2021-10-06 22:16:50 +01:00
commit 191299697b
13 changed files with 148 additions and 17 deletions

View File

@ -191,6 +191,12 @@ class RedisWrapper {
} }
} }
async getTTL(key) {
const db = this._db
const prefixedKey = addDbPrefix(db, key)
return CLIENT.ttl(prefixedKey)
}
async setExpiry(key, expirySeconds) { async setExpiry(key, expirySeconds) {
const db = this._db const db = this._db
const prefixedKey = addDbPrefix(db, key) const prefixedKey = addDbPrefix(db, key)

View File

@ -67,6 +67,14 @@ export const getFrontendStore = () => {
initialise: async pkg => { initialise: async pkg => {
const { layouts, screens, application, clientLibPath } = pkg const { layouts, screens, application, clientLibPath } = pkg
const components = await fetchComponentLibDefinitions(application.appId) const components = await fetchComponentLibDefinitions(application.appId)
// make sure app isn't locked
if (
components &&
components.status === 400 &&
components.message?.includes("lock")
) {
throw { ok: false, reason: "locked" }
}
store.update(state => ({ store.update(state => ({
...state, ...state,
libraries: application.componentLibraries, libraries: application.componentLibraries,

View File

@ -11,16 +11,18 @@
import ICONS from "./icons" import ICONS from "./icons"
let openDataSources = [] let openDataSources = []
$: enrichedDataSources = $datasources.list.map(datasource => { $: enrichedDataSources = Array.isArray($datasources.list)
const selected = $datasources.selected === datasource._id ? $datasources.list.map(datasource => {
const open = openDataSources.includes(datasource._id) const selected = $datasources.selected === datasource._id
const containsSelected = containsActiveEntity(datasource) const open = openDataSources.includes(datasource._id)
return { const containsSelected = containsActiveEntity(datasource)
...datasource, return {
selected, ...datasource,
open: selected || open || containsSelected, selected,
} open: selected || open || containsSelected,
}) }
})
: []
$: openDataSource = enrichedDataSources.find(x => x.open) $: openDataSource = enrichedDataSources.find(x => x.open)
$: { $: {
// Ensure the open data source is always included in the list of open // Ensure the open data source is always included in the list of open

View File

@ -3,7 +3,7 @@
import { ModalContent, notifications, Body, Layout } from "@budibase/bbui" import { ModalContent, notifications, Body, Layout } from "@budibase/bbui"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte" import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import { datasources } from "stores/backend" import { datasources, tables } from "stores/backend"
import { IntegrationNames } from "constants" import { IntegrationNames } from "constants"
import cloneDeep from "lodash/cloneDeepWith" import cloneDeep from "lodash/cloneDeepWith"
@ -21,7 +21,9 @@
let baseName = IntegrationNames[config.type] let baseName = IntegrationNames[config.type]
let name = let name =
existingTypeCount == 0 ? baseName : `${baseName}-${existingTypeCount + 1}` existingTypeCount === 0
? baseName
: `${baseName}-${existingTypeCount + 1}`
datasource.type = "datasource" datasource.type = "datasource"
datasource.source = config.type datasource.source = config.type
@ -37,6 +39,8 @@
// Create datasource // Create datasource
const resp = await datasources.save(datasource, datasource.plus) const resp = await datasources.save(datasource, datasource.plus)
// update the tables incase data source plus
await tables.fetch()
await datasources.select(resp._id) await datasources.select(resp._id)
$goto(`./datasource/${resp._id}`) $goto(`./datasource/${resp._id}`)
notifications.success(`Datasource updated successfully.`) notifications.success(`Datasource updated successfully.`)

View File

@ -44,6 +44,15 @@ export const OperatorOptions = {
}, },
} }
export const NoEmptyFilterStrings = [
OperatorOptions.StartsWith.value,
OperatorOptions.Like.value,
OperatorOptions.Equals.value,
OperatorOptions.NotEquals.value,
OperatorOptions.Contains.value,
OperatorOptions.NotContains.value,
]
/** /**
* Returns the valid operator options for a certain data type * Returns the valid operator options for a certain data type
* @param type the data type * @param type the data type

View File

@ -1,3 +1,26 @@
import { NoEmptyFilterStrings } from "../constants/lucene"
/**
* Removes any fields that contain empty strings that would cause inconsistent
* behaviour with how backend tables are filtered (no value means no filter).
*/
function cleanupQuery(query) {
if (!query) {
return query
}
for (let filterField of NoEmptyFilterStrings) {
if (!query[filterField]) {
continue
}
for (let [key, value] of Object.entries(query[filterField])) {
if (!value || value === "") {
delete query[filterField][key]
}
}
}
return query
}
/** /**
* Builds a lucene JSON query from the filter structure generated in the builder * Builds a lucene JSON query from the filter structure generated in the builder
* @param filter the builder filter structure * @param filter the builder filter structure
@ -76,6 +99,8 @@ export const luceneQuery = (docs, query) => {
if (!query) { if (!query) {
return docs return docs
} }
// make query consistent first
query = cleanupQuery(query)
// Iterates over a set of filters and evaluates a fail function against a doc // Iterates over a set of filters and evaluates a fail function against a doc
const match = (type, failFn) => doc => { const match = (type, failFn) => doc => {

View File

@ -8,7 +8,7 @@
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte" import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { auth, admin } from "stores/portal" import { auth, admin } from "stores/portal"
import { isActive, goto, layout } from "@roxi/routify" import { isActive, goto, layout, redirect } from "@roxi/routify"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
import { capitalise } from "helpers" import { capitalise } from "helpers"
import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte" import UpgradeModal from "../../../../components/upgrade/UpgradeModal.svelte"
@ -34,7 +34,16 @@
const pkg = await res.json() const pkg = await res.json()
if (res.ok) { if (res.ok) {
await store.actions.initialise(pkg) try {
await store.actions.initialise(pkg)
// edge case, lock wasn't known to client when it re-directed, or user went directly
} catch (err) {
if (!err.ok && err.reason === "locked") {
$redirect("../../")
} else {
throw err
}
}
await automationStore.actions.fetch() await automationStore.actions.fetch()
await roles.fetch() await roles.fetch()
return pkg return pkg

View File

@ -178,7 +178,12 @@ module External {
manyRelationships: ManyRelationship[] = [] manyRelationships: ManyRelationship[] = []
for (let [key, field] of Object.entries(table.schema)) { for (let [key, field] of Object.entries(table.schema)) {
// if set already, or not set just skip it // if set already, or not set just skip it
if (!row[key] || newRow[key] || field.autocolumn) { if ((!row[key] && row[key] !== "") || newRow[key] || field.autocolumn) {
continue
}
// if its an empty string then it means return the column to null (if possible)
if (row[key] === "") {
newRow[key] = null
continue continue
} }
// parse floats/numbers // parse floats/numbers

View File

@ -2,6 +2,7 @@ const {
DataSourceOperation, DataSourceOperation,
SortDirection, SortDirection,
FieldTypes, FieldTypes,
NoEmptyFilterStrings,
} = require("../../../constants") } = require("../../../constants")
const { const {
breakExternalTableId, breakExternalTableId,
@ -11,6 +12,19 @@ const ExternalRequest = require("./ExternalRequest")
const CouchDB = require("../../../db") const CouchDB = require("../../../db")
async function handleRequest(appId, operation, tableId, opts = {}) { async function handleRequest(appId, operation, tableId, opts = {}) {
// make sure the filters are cleaned up, no empty strings for equals, fuzzy or string
if (opts && opts.filters) {
for (let filterField of NoEmptyFilterStrings) {
if (!opts.filters[filterField]) {
continue
}
for (let [key, value] of Object.entries(opts.filters[filterField])) {
if (!value || value === "") {
delete opts.filters[filterField][key]
}
}
}
}
return new ExternalRequest(appId, operation, tableId, opts.datasource).run( return new ExternalRequest(appId, operation, tableId, opts.datasource).run(
opts opts
) )

View File

@ -6,6 +6,29 @@ exports.JobQueues = {
AUTOMATIONS: "automationQueue", AUTOMATIONS: "automationQueue",
} }
const FilterTypes = {
STRING: "string",
FUZZY: "fuzzy",
RANGE: "range",
EQUAL: "equal",
NOT_EQUAL: "notEqual",
EMPTY: "empty",
NOT_EMPTY: "notEmpty",
CONTAINS: "contains",
NOT_CONTAINS: "notContains",
ONE_OF: "oneOf",
}
exports.FilterTypes = FilterTypes
exports.NoEmptyFilterStrings = [
FilterTypes.STRING,
FilterTypes.FUZZY,
FilterTypes.EQUAL,
FilterTypes.NOT_EQUAL,
FilterTypes.CONTAINS,
FilterTypes.NOT_CONTAINS,
]
exports.FieldTypes = { exports.FieldTypes = {
STRING: "string", STRING: "string",
LONGFORM: "longform", LONGFORM: "longform",

View File

@ -55,6 +55,12 @@ function addFilters(
query = query[fnc](key, "ilike", `${value}%`) query = query[fnc](key, "ilike", `${value}%`)
}) })
} }
if (filters.fuzzy) {
iterate(filters.fuzzy, (key, value) => {
const fnc = allOr ? "orWhere" : "where"
query = query[fnc](key, "ilike", `%${value}%`)
})
}
if (filters.range) { if (filters.range) {
iterate(filters.range, (key, value) => { iterate(filters.range, (key, value) => {
if (!value.high || !value.low) { if (!value.high || !value.low) {
@ -135,6 +141,12 @@ function buildCreate(
const { endpoint, body } = json const { endpoint, body } = json
let query: KnexQuery = knex(endpoint.entityId) let query: KnexQuery = knex(endpoint.entityId)
const parsedBody = parseBody(body) const parsedBody = parseBody(body)
// make sure no null values in body for creation
for (let [key, value] of Object.entries(parsedBody)) {
if (value == null) {
delete parsedBody[key]
}
}
// mysql can't use returning // mysql can't use returning
if (opts.disableReturning) { if (opts.disableReturning) {
return query.insert(parsedBody) return query.insert(parsedBody)

View File

@ -33,7 +33,7 @@ async function checkDevAppLocks(ctx) {
return return
} }
if (!(await doesUserHaveLock(appId, ctx.user))) { if (!(await doesUserHaveLock(appId, ctx.user))) {
ctx.throw(403, "User does not hold app lock.") ctx.throw(400, "User does not hold app lock.")
} }
// they do have lock, update it // they do have lock, update it

View File

@ -4,9 +4,13 @@ const { Cookies } = require("@budibase/auth").constants
const { getRole } = require("@budibase/auth/roles") const { getRole } = require("@budibase/auth/roles")
const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles") const { BUILTIN_ROLE_IDS } = require("@budibase/auth/roles")
const { generateUserMetadataID } = require("../db/utils") const { generateUserMetadataID } = require("../db/utils")
const { dbExists } = require("@budibase/auth/db") const { dbExists, getTenantIDFromAppID } = require("@budibase/auth/db")
const { getTenantId } = require("@budibase/auth/tenancy")
const { getCachedSelf } = require("../utilities/global") const { getCachedSelf } = require("../utilities/global")
const CouchDB = require("../db") const CouchDB = require("../db")
const env = require("../environment")
const DEFAULT_TENANT_ID = "default"
module.exports = async (ctx, next) => { module.exports = async (ctx, next) => {
// try to get the appID from the request // try to get the appID from the request
@ -45,11 +49,21 @@ module.exports = async (ctx, next) => {
// retrieving global user gets the right role // retrieving global user gets the right role
roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC roleId = globalUser.roleId || BUILTIN_ROLE_IDS.BASIC
} }
// nothing more to do // nothing more to do
if (!appId) { if (!appId) {
return next() return next()
} }
// If user and app tenant Ids do not match, 403
if (env.MULTI_TENANCY && ctx.user) {
const userTenantId = getTenantId()
const tenantId = getTenantIDFromAppID(ctx.appId) || DEFAULT_TENANT_ID
if (tenantId !== userTenantId) {
ctx.throw(403, "Cannot access application.")
}
}
ctx.appId = appId ctx.appId = appId
if (roleId) { if (roleId) {
ctx.roleId = roleId ctx.roleId = roleId