prevent cross tenant app access
This commit is contained in:
commit
191299697b
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -11,7 +11,8 @@
|
||||||
import ICONS from "./icons"
|
import ICONS from "./icons"
|
||||||
|
|
||||||
let openDataSources = []
|
let openDataSources = []
|
||||||
$: enrichedDataSources = $datasources.list.map(datasource => {
|
$: enrichedDataSources = Array.isArray($datasources.list)
|
||||||
|
? $datasources.list.map(datasource => {
|
||||||
const selected = $datasources.selected === datasource._id
|
const selected = $datasources.selected === datasource._id
|
||||||
const open = openDataSources.includes(datasource._id)
|
const open = openDataSources.includes(datasource._id)
|
||||||
const containsSelected = containsActiveEntity(datasource)
|
const containsSelected = containsActiveEntity(datasource)
|
||||||
|
@ -21,6 +22,7 @@
|
||||||
open: selected || open || containsSelected,
|
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
|
||||||
|
|
|
@ -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.`)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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) {
|
||||||
|
try {
|
||||||
await store.actions.initialise(pkg)
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue