Tests complete + backwards compatibility for deployment

This commit is contained in:
Rory Powell 2022-03-22 00:23:22 +00:00
parent 715d42d3e6
commit 0a4b1eb552
43 changed files with 294 additions and 5109 deletions

View File

@ -98,10 +98,6 @@ spec:
value: http://worker-service:{{ .Values.services.worker.port }} value: http://worker-service:{{ .Values.services.worker.port }}
- name: PLATFORM_URL - name: PLATFORM_URL
value: {{ .Values.globals.platformUrl | quote }} value: {{ .Values.globals.platformUrl | quote }}
- name: USE_QUOTAS
value: {{ .Values.globals.useQuotas | quote }}
- name: EXCLUDE_QUOTAS_TENANTS
value: {{ .Values.globals.excludeQuotasTenants | quote }}
- name: ACCOUNT_PORTAL_URL - name: ACCOUNT_PORTAL_URL
value: {{ .Values.globals.accountPortalUrl | quote }} value: {{ .Values.globals.accountPortalUrl | quote }}
- name: ACCOUNT_PORTAL_API_KEY - name: ACCOUNT_PORTAL_API_KEY

View File

@ -93,15 +93,13 @@ globals:
logLevel: info logLevel: info
selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup selfHosted: "1" # set to 0 for budibase cloud environment, set to 1 for self-hosted setup
multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs multiTenancy: "0" # set to 0 to disable multiple orgs, set to 1 to enable multiple orgs
useQuotas: "0"
excludeQuotasTenants: "" # comma seperated list of tenants to exclude from quotas
accountPortalUrl: "" accountPortalUrl: ""
accountPortalApiKey: "" accountPortalApiKey: ""
cookieDomain: "" cookieDomain: ""
platformUrl: "" platformUrl: ""
httpMigrations: "0" httpMigrations: "0"
google: google:
clientId: "" clientId: ""
secret: "" secret: ""
createSecrets: true # creates an internal API key, JWT secrets and redis password for you createSecrets: true # creates an internal API key, JWT secrets and redis password for you

View File

@ -56,7 +56,8 @@ exports.createApiKeyView = async () => {
await db.put(designDoc) await db.put(designDoc)
} }
exports.createUserBuildersView = async db => { exports.createUserBuildersView = async () => {
const db = getGlobalDB()
let designDoc let designDoc
try { try {
designDoc = await db.get("_design/database") designDoc = await db.get("_design/database")
@ -82,6 +83,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
const CreateFuncByName = { const CreateFuncByName = {
[ViewNames.USER_BY_EMAIL]: exports.createUserEmailView, [ViewNames.USER_BY_EMAIL]: exports.createUserEmailView,
[ViewNames.BY_API_KEY]: exports.createApiKeyView, [ViewNames.BY_API_KEY]: exports.createApiKeyView,
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
} }
// can pass DB in if working with something specific // can pass DB in if working with something specific
if (!db) { if (!db) {

View File

@ -28,8 +28,7 @@ module.exports = {
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
PLATFORM_URL: process.env.PLATFORM_URL, PLATFORM_URL: process.env.PLATFORM_URL,
USE_QUOTAS: process.env.USE_QUOTAS, TENANT_FEATURE_FLAGS: process.env.TENANT_FEATURE_FLAGS,
EXCLUDE_QUOTAS_TENANTS: process.env.EXCLUDE_QUOTAS_TENANTS,
isTest, isTest,
_set(key, value) { _set(key, value) {
process.env[key] = value process.env[key] = value

View File

@ -0,0 +1,52 @@
const env = require("../environment")
const tenancy = require("../tenancy")
/**
* Read the TENANT_FEATURE_FLAGS env var and return an array of features flags for each tenant.
* The env var is formatted as:
* tenant1:feature1:feature2,tenant2:feature1
*/
const getFeatureFlags = () => {
if (!env.TENANT_FEATURE_FLAGS) {
return
}
const tenantFeatureFlags = {}
env.TENANT_FEATURE_FLAGS.split(",").forEach(tenantToFeatures => {
const [tenantId, ...features] = tenantToFeatures.split(":")
features.forEach(feature => {
if (!tenantFeatureFlags[tenantId]) {
tenantFeatureFlags[tenantId] = []
}
tenantFeatureFlags[tenantId].push(feature)
})
})
return tenantFeatureFlags
}
const TENANT_FEATURE_FLAGS = getFeatureFlags()
exports.isEnabled = featureFlag => {
const tenantId = tenancy.getTenantId()
return (
TENANT_FEATURE_FLAGS &&
TENANT_FEATURE_FLAGS[tenantId] &&
TENANT_FEATURE_FLAGS[tenantId].includes(featureFlag)
)
}
exports.getTenantFeatureFlags = tenantId => {
if (TENANT_FEATURE_FLAGS && TENANT_FEATURE_FLAGS[tenantId]) {
return TENANT_FEATURE_FLAGS[tenantId]
}
return []
}
exports.FeatureFlag = {
LICENSING: "LICENSING",
}

View File

@ -19,4 +19,5 @@ module.exports = {
env: require("./environment"), env: require("./environment"),
accounts: require("./cloud/accounts"), accounts: require("./cloud/accounts"),
tenancy: require("./tenancy"), tenancy: require("./tenancy"),
featureFlags: require("./featureFlags"),
} }

View File

@ -55,4 +55,4 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
} }
} }
// expose for testing // expose for testing
exports.authenticate = buildVerifyFn exports.buildVerifyFn = buildVerifyFn

View File

@ -129,4 +129,4 @@ exports.strategyFactory = async function (config, callbackUrl, saveUserFn) {
} }
// expose for testing // expose for testing
exports.authenticate = buildVerifyFn exports.buildVerifyFn = buildVerifyFn

View File

@ -58,8 +58,10 @@ describe("google", () => {
it("delegates authentication to third party common", async () => { it("delegates authentication to third party common", async () => {
const google = require("../google") const google = require("../google")
const mockSaveUserFn = jest.fn()
const authenticate = await google.buildVerifyFn(mockSaveUserFn)
await google.authenticate( await authenticate(
data.accessToken, data.accessToken,
data.refreshToken, data.refreshToken,
profile, profile,
@ -69,7 +71,8 @@ describe("google", () => {
expect(authenticateThirdParty).toHaveBeenCalledWith( expect(authenticateThirdParty).toHaveBeenCalledWith(
user, user,
true, true,
mockDone) mockDone,
mockSaveUserFn)
}) })
}) })
}) })

View File

@ -83,8 +83,10 @@ describe("oidc", () => {
async function doAuthenticate() { async function doAuthenticate() {
const oidc = require("../oidc") const oidc = require("../oidc")
const mockSaveUserFn = jest.fn()
const authenticate = await oidc.buildVerifyFn(mockSaveUserFn)
await oidc.authenticate( await authenticate(
issuer, issuer,
sub, sub,
profile, profile,

View File

@ -147,7 +147,9 @@ exports.getGlobalUserByEmail = async email => {
} }
exports.getBuildersCount = async () => { exports.getBuildersCount = async () => {
const builders = await queryGlobalView(ViewNames.BUILDERS) const builders = await queryGlobalView(ViewNames.USER_BY_BUILDERS, {
include_docs: false,
})
return builders.total_rows return builders.total_rows
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"url": "https://github.com/Budibase/budibase.git" "url": "https://github.com/Budibase/budibase.git"
}, },
"scripts": { "scripts": {
"build": "rimraf dist/ && tsc && mv dist/src/* dist/ && rmdir dist/src/ && yarn postbuild", "build": "rimraf dist/ && tsc -p tsconfig.build.json && mv dist/src/* dist/ && rmdir dist/src/ && yarn postbuild",
"postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/", "postbuild": "copyfiles -u 1 src/**/*.svelte dist/ && copyfiles -u 1 src/**/*.hbs dist/ && copyfiles -u 1 src/**/*.json dist/",
"test": "jest --coverage --maxWorkers=2", "test": "jest --coverage --maxWorkers=2",
"test:watch": "jest --watch", "test:watch": "jest --watch",
@ -81,6 +81,7 @@
"@koa/router": "8.0.0", "@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1", "@sendgrid/mail": "7.1.1",
"@sentry/node": "6.17.7", "@sentry/node": "6.17.7",
"@types/koa__router": "^8.0.11",
"airtable": "0.10.1", "airtable": "0.10.1",
"arangojs": "7.2.0", "arangojs": "7.2.0",
"aws-sdk": "^2.767.0", "aws-sdk": "^2.767.0",
@ -146,7 +147,7 @@
"@types/apidoc": "^0.50.0", "@types/apidoc": "^0.50.0",
"@types/bull": "^3.15.1", "@types/bull": "^3.15.1",
"@types/google-spreadsheet": "^3.1.5", "@types/google-spreadsheet": "^3.1.5",
"@types/jest": "^26.0.23", "@types/jest": "^27.4.1",
"@types/koa": "^2.13.3", "@types/koa": "^2.13.3",
"@types/koa-router": "^7.4.2", "@types/koa-router": "^7.4.2",
"@types/lodash": "^4.14.179", "@types/lodash": "^4.14.179",

View File

@ -389,7 +389,7 @@ const destroyApp = async (ctx: any) => {
const db = getAppDB() const db = getAppDB()
const result = await db.destroy() const result = await db.destroy()
if (ctx.query.unpublish) { if (ctx.query?.unpublish) {
await quotas.removePublishedApp() await quotas.removePublishedApp()
} else { } else {
await quotas.removeApp() await quotas.removeApp()
@ -408,7 +408,7 @@ const destroyApp = async (ctx: any) => {
} }
const preDestroyApp = async (ctx: any) => { const preDestroyApp = async (ctx: any) => {
const rows = await getUniqueRows([ctx.appId]) const rows = await getUniqueRows([ctx.params.appId])
ctx.rowCount = rows.length ctx.rowCount = rows.length
} }

View File

@ -1,9 +1,9 @@
const Router = require("@koa/router") import Router from "@koa/router"
import * as controller from "../controllers/application" import * as controller from "../controllers/application"
import authorized from "../../middleware/authorized" import authorized from "../../middleware/authorized"
const { BUILDER } = require("@budibase/backend-core/permissions") import { BUILDER } from "@budibase/backend-core/permissions"
const router = Router() const router = new Router()
router router
.post("/api/applications/:appId/sync", authorized(BUILDER), controller.sync) .post("/api/applications/:appId/sync", authorized(BUILDER), controller.sync)

View File

@ -127,4 +127,4 @@ applyRoutes(queryEndpoints, PermissionTypes.QUERY, "queryId")
// needs to be applied last for routing purposes, don't override other endpoints // needs to be applied last for routing purposes, don't override other endpoints
applyRoutes(rowEndpoints, PermissionTypes.TABLE, "tableId", "rowId") applyRoutes(rowEndpoints, PermissionTypes.TABLE, "tableId", "rowId")
module.exports = publicRouter export default publicRouter

View File

@ -1,4 +1,4 @@
const Router = require("@koa/router") import Router from "@koa/router"
import * as rowController from "../controllers/row" import * as rowController from "../controllers/row"
import authorized from "../../middleware/authorized" import authorized from "../../middleware/authorized"
import { paramResource, paramSubResource } from "../../middleware/resourceId" import { paramResource, paramSubResource } from "../../middleware/resourceId"
@ -8,7 +8,7 @@ const {
} = require("@budibase/backend-core/permissions") } = require("@budibase/backend-core/permissions")
const { internalSearchValidator } = require("./utils/validators") const { internalSearchValidator } = require("./utils/validators")
const router = Router() const router = new Router()
router router
/** /**

View File

@ -1,4 +1,4 @@
const Router = require("@koa/router") import Router from "@koa/router"
import * as controller from "../controllers/static" import * as controller from "../controllers/static"
import { budibaseTempDir } from "../../utilities/budibaseDir" import { budibaseTempDir } from "../../utilities/budibaseDir"
import authorized from "../../middleware/authorized" import authorized from "../../middleware/authorized"
@ -10,10 +10,10 @@ import {
import * as env from "../../environment" import * as env from "../../environment"
import { paramResource } from "../../middleware/resourceId" import { paramResource } from "../../middleware/resourceId"
const router = Router() const router = new Router()
/* istanbul ignore next */ /* istanbul ignore next */
router.param("file", async (file, ctx, next) => { router.param("file", async (file: any, ctx: any, next: any) => {
ctx.file = file && file.includes(".") ? file : "index.html" ctx.file = file && file.includes(".") ? file : "index.html"
if (!ctx.file.startsWith("budibase-client")) { if (!ctx.file.startsWith("budibase-client")) {
return next() return next()

View File

@ -1,31 +1,38 @@
const rowController = require("../../../controllers/row") import * as rowController from "../../../controllers/row"
const appController = require("../../../controllers/application") import * as appController from "../../../controllers/application"
const { AppStatus } = require("../../../../db/utils") import { AppStatus } from "../../../../db/utils"
const { BUILTIN_ROLE_IDS } = require("@budibase/backend-core/roles") import { BUILTIN_ROLE_IDS } from "@budibase/backend-core/roles"
const { TENANT_ID } = require("../../../../tests/utilities/structures") import { TENANT_ID } from "../../../../tests/utilities/structures"
const { getAppDB, doInAppContext } = require("@budibase/backend-core/context") import { getAppDB, doInAppContext } from "@budibase/backend-core/context"
const env = require("../../../../environment") import * as env from "../../../../environment"
function Request(appId, params) { class Request {
this.appId = appId appId: any
this.params = params params: any
this.request = {} request: any
body: any
constructor(appId: any, params: any) {
this.appId = appId
this.params = params
this.request = {}
}
} }
function runRequest(appId, controlFunc, request) { function runRequest(appId: any, controlFunc: any, request?: any) {
return doInAppContext(appId, async () => { return doInAppContext(appId, async () => {
return controlFunc(request) return controlFunc(request)
}) })
} }
exports.getAllTableRows = async config => { export const getAllTableRows = async (config: any) => {
const req = new Request(config.appId, { tableId: config.table._id }) const req = new Request(config.appId, { tableId: config.table._id })
await runRequest(config.appId, rowController.fetch, req) await runRequest(config.appId, rowController.fetch, req)
return req.body return req.body
} }
exports.clearAllApps = async (tenantId = TENANT_ID) => { export const clearAllApps = async (tenantId = TENANT_ID) => {
const req = { query: { status: AppStatus.DEV }, user: { tenantId } } const req: any = { query: { status: AppStatus.DEV }, user: { tenantId } }
await appController.fetch(req) await appController.fetch(req)
const apps = req.body const apps = req.body
if (!apps || apps.length <= 0) { if (!apps || apps.length <= 0) {
@ -34,11 +41,11 @@ exports.clearAllApps = async (tenantId = TENANT_ID) => {
for (let app of apps) { for (let app of apps) {
const { appId } = app const { appId } = app
const req = new Request(null, { appId }) const req = new Request(null, { appId })
await runRequest(appId, appController.delete, req) await runRequest(appId, appController.destroy, req)
} }
} }
exports.clearAllAutomations = async config => { export const clearAllAutomations = async (config: any) => {
const automations = await config.getAllAutomations() const automations = await config.getAllAutomations()
for (let auto of automations) { for (let auto of automations) {
await doInAppContext(config.appId, async () => { await doInAppContext(config.appId, async () => {
@ -47,7 +54,12 @@ exports.clearAllAutomations = async config => {
} }
} }
exports.createRequest = (request, method, url, body) => { export const createRequest = (
request: any,
method: any,
url: any,
body: any
) => {
let req let req
if (method === "POST") req = request.post(url).send(body) if (method === "POST") req = request.post(url).send(body)
@ -59,7 +71,12 @@ exports.createRequest = (request, method, url, body) => {
return req return req
} }
exports.checkBuilderEndpoint = async ({ config, method, url, body }) => { export const checkBuilderEndpoint = async ({
config,
method,
url,
body,
}: any) => {
const headers = await config.login({ const headers = await config.login({
userId: "us_fail", userId: "us_fail",
builder: false, builder: false,
@ -71,14 +88,14 @@ exports.checkBuilderEndpoint = async ({ config, method, url, body }) => {
.expect(403) .expect(403)
} }
exports.checkPermissionsEndpoint = async ({ export const checkPermissionsEndpoint = async ({
config, config,
method, method,
url, url,
body, body,
passRole, passRole,
failRole, failRole,
}) => { }: any) => {
const passHeader = await config.login({ const passHeader = await config.login({
roleId: passRole, roleId: passRole,
prodApp: true, prodApp: true,
@ -106,11 +123,11 @@ exports.checkPermissionsEndpoint = async ({
.expect(403) .expect(403)
} }
exports.getDB = () => { export const getDB = () => {
return getAppDB() return getAppDB()
} }
exports.testAutomation = async (config, automation) => { export const testAutomation = async (config: any, automation: any) => {
return runRequest(automation.appId, async () => { return runRequest(automation.appId, async () => {
return await config.request return await config.request
.post(`/api/automations/${automation._id}/test`) .post(`/api/automations/${automation._id}/test`)
@ -126,7 +143,7 @@ exports.testAutomation = async (config, automation) => {
}) })
} }
exports.runInProd = async func => { export const runInProd = async (func: any) => {
const nodeEnv = env.NODE_ENV const nodeEnv = env.NODE_ENV
const workerId = env.JEST_WORKER_ID const workerId = env.JEST_WORKER_ID
env._set("NODE_ENV", "PRODUCTION") env._set("NODE_ENV", "PRODUCTION")

View File

@ -1,4 +1,3 @@
import { quotas } from "@budibase/pro"
import { save } from "../../api/controllers/row" import { save } from "../../api/controllers/row"
import { cleanUpRow, getError } from "../automationUtils" import { cleanUpRow, getError } from "../automationUtils"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
@ -78,7 +77,7 @@ export async function run({ inputs, appId, emitter }: any) {
try { try {
inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row) inputs.row = await cleanUpRow(inputs.row.tableId, inputs.row)
await quotas.addRow(() => save(ctx)) await save(ctx)
return { return {
row: inputs.row, row: inputs.row,
response: ctx.body, response: ctx.body,

View File

@ -1,7 +1,6 @@
import { destroy } from "../../api/controllers/row" import { destroy } from "../../api/controllers/row"
import { buildCtx } from "./utils" import { buildCtx } from "./utils"
import { getError } from "../automationUtils" import { getError } from "../automationUtils"
import { quotas } from "@budibase/pro"
export const definition = { export const definition = {
description: "Delete a row from your database", description: "Delete a row from your database",
@ -74,7 +73,6 @@ export async function run({ inputs, appId, emitter }: any) {
try { try {
await destroy(ctx) await destroy(ctx)
await quotas.removeRow()
return { return {
response: ctx.body, response: ctx.body,
row: ctx.row, row: ctx.row,

View File

@ -1,4 +1,3 @@
jest.mock("../../utilities/usageQuota")
jest.mock("../../threads/automation") jest.mock("../../threads/automation")
jest.mock("../../utilities/redis", () => ({ jest.mock("../../utilities/redis", () => ({
init: jest.fn(), init: jest.fn(),

View File

@ -1,10 +1,8 @@
jest.mock("../../utilities/usageQuota") import * as setup from "./utilities"
const usageQuota = require("../../utilities/usageQuota")
const setup = require("./utilities")
describe("test the create row action", () => { describe("test the create row action", () => {
let table, row let table: any
let row: any
let config = setup.getConfig() let config = setup.getConfig()
beforeEach(async () => { beforeEach(async () => {
@ -36,20 +34,11 @@ describe("test the create row action", () => {
row: { row: {
tableId: "invalid", tableId: "invalid",
invalid: "invalid", invalid: "invalid",
} },
}) })
expect(res.success).toEqual(false) expect(res.success).toEqual(false)
}) })
it("check usage quota attempts", async () => {
await setup.runInProd(async () => {
await setup.runStep(setup.actions.CREATE_ROW.stepId, {
row
})
expect(usageQuota.update).toHaveBeenCalledWith("rows", 1)
})
})
it("should check invalid inputs return an error", async () => { it("should check invalid inputs return an error", async () => {
const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, {}) const res = await setup.runStep(setup.actions.CREATE_ROW.stepId, {})
expect(res.success).toEqual(false) expect(res.success).toEqual(false)

View File

@ -1,10 +1,9 @@
jest.mock("../../utilities/usageQuota")
const usageQuota = require("../../utilities/usageQuota")
const setup = require("./utilities") const setup = require("./utilities")
describe("test the delete row action", () => { describe("test the delete row action", () => {
let table, row, inputs let table: any
let row: any
let inputs: any
let config = setup.getConfig() let config = setup.getConfig()
beforeEach(async () => { beforeEach(async () => {
@ -37,7 +36,6 @@ describe("test the delete row action", () => {
it("check usage quota attempts", async () => { it("check usage quota attempts", async () => {
await setup.runInProd(async () => { await setup.runInProd(async () => {
await setup.runStep(setup.actions.DELETE_ROW.stepId, inputs) await setup.runStep(setup.actions.DELETE_ROW.stepId, inputs)
expect(usageQuota.update).toHaveBeenCalledWith("rows", -1)
}) })
}) })

View File

@ -18,7 +18,6 @@ exports.afterAll = () => {
exports.runInProd = async fn => { exports.runInProd = async fn => {
env._set("NODE_ENV", "production") env._set("NODE_ENV", "production")
env._set("USE_QUOTAS", 1)
let error let error
try { try {
await fn() await fn()
@ -26,7 +25,6 @@ exports.runInProd = async fn => {
error = err error = err
} }
env._set("NODE_ENV", "jest") env._set("NODE_ENV", "jest")
env._set("USE_QUOTAS", null)
if (error) { if (error) {
throw error throw error
} }

View File

@ -38,8 +38,6 @@ module.exports = {
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS, ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY, MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY, MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
USE_QUOTAS: process.env.USE_QUOTAS,
EXCLUDE_QUOTAS_TENANTS: process.env.EXCLUDE_QUOTAS_TENANTS,
REDIS_URL: process.env.REDIS_URL, REDIS_URL: process.env.REDIS_URL,
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,

View File

@ -1,8 +1,8 @@
const { import {
getUserRoleHierarchy, getUserRoleHierarchy,
getRequiredResourceRole, getRequiredResourceRole,
BUILTIN_ROLE_IDS, BUILTIN_ROLE_IDS,
} = require("@budibase/backend-core/roles") } from "@budibase/backend-core/roles"
const { const {
PermissionTypes, PermissionTypes,
doesHaveBasePermission, doesHaveBasePermission,
@ -12,7 +12,7 @@ const { isWebhookEndpoint } = require("./utils")
const { buildCsrfMiddleware } = require("@budibase/backend-core/auth") const { buildCsrfMiddleware } = require("@budibase/backend-core/auth")
const { getAppId } = require("@budibase/backend-core/context") const { getAppId } = require("@budibase/backend-core/context")
function hasResource(ctx) { function hasResource(ctx: any) {
return ctx.resourceId != null return ctx.resourceId != null
} }
@ -24,7 +24,12 @@ const csrf = buildCsrfMiddleware()
* - Builders can access all resources. * - Builders can access all resources.
* - Otherwise the user must have the required role. * - Otherwise the user must have the required role.
*/ */
const checkAuthorized = async (ctx, resourceRoles, permType, permLevel) => { const checkAuthorized = async (
ctx: any,
resourceRoles: any,
permType: any,
permLevel: any
) => {
// check if this is a builder api and the user is not a builder // check if this is a builder api and the user is not a builder
const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global const isBuilder = ctx.user && ctx.user.builder && ctx.user.builder.global
const isBuilderApi = permType === PermissionTypes.BUILDER const isBuilderApi = permType === PermissionTypes.BUILDER
@ -39,10 +44,10 @@ const checkAuthorized = async (ctx, resourceRoles, permType, permLevel) => {
} }
const checkAuthorizedResource = async ( const checkAuthorizedResource = async (
ctx, ctx: any,
resourceRoles, resourceRoles: any,
permType, permType: any,
permLevel permLevel: any
) => { ) => {
// get the user's roles // get the user's roles
const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC const roleId = ctx.roleId || BUILTIN_ROLE_IDS.PUBLIC
@ -53,7 +58,9 @@ const checkAuthorizedResource = async (
// check if the user has the required role // check if the user has the required role
if (resourceRoles.length > 0) { if (resourceRoles.length > 0) {
// deny access if the user doesn't have the required resource role // deny access if the user doesn't have the required resource role
const found = userRoles.find(role => resourceRoles.indexOf(role._id) !== -1) const found = userRoles.find(
(role: any) => resourceRoles.indexOf(role._id) !== -1
)
if (!found) { if (!found) {
ctx.throw(403, permError) ctx.throw(403, permError)
} }
@ -63,9 +70,8 @@ const checkAuthorizedResource = async (
} }
} }
module.exports = export = (permType: any, permLevel: any = null) =>
(permType, permLevel = null) => async (ctx: any, next: any) => {
async (ctx, next) => {
// webhooks don't need authentication, each webhook unique // webhooks don't need authentication, each webhook unique
// also internal requests (between services) don't need authorized // also internal requests (between services) don't need authorized
if (isWebhookEndpoint(ctx) || ctx.internal) { if (isWebhookEndpoint(ctx) || ctx.internal) {
@ -81,7 +87,7 @@ module.exports =
await builderMiddleware(ctx, permType) await builderMiddleware(ctx, permType)
// get the resource roles // get the resource roles
let resourceRoles = [] let resourceRoles: any = []
const appId = getAppId() const appId = getAppId()
if (appId && hasResource(ctx)) { if (appId && hasResource(ctx)) {
resourceRoles = await getRequiredResourceRole(permLevel, ctx) resourceRoles = await getRequiredResourceRole(permLevel, ctx)

View File

@ -1,134 +0,0 @@
jest.mock("../../db")
jest.mock("../../utilities/usageQuota")
jest.mock("@budibase/backend-core/tenancy", () => ({
getTenantId: () => "testing123"
}))
const usageQuotaMiddleware = require("../usageQuota")
const usageQuota = require("../../utilities/usageQuota")
const CouchDB = require("../../db")
const env = require("../../environment")
class TestConfiguration {
constructor() {
this.throw = jest.fn()
this.next = jest.fn()
this.middleware = usageQuotaMiddleware
this.ctx = {
throw: this.throw,
next: this.next,
appId: "test",
request: {
body: {}
},
req: {
method: "POST",
url: "/applications"
}
}
usageQuota.useQuotas = () => true
}
executeMiddleware() {
return this.middleware(this.ctx, this.next)
}
setProd(bool) {
if (bool) {
env.isDev = () => false
env.isProd = () => true
this.ctx.user = { tenantId: "test" }
} else {
env.isDev = () => true
env.isProd = () => false
}
}
setMethod(method) {
this.ctx.req.method = method
}
setUrl(url) {
this.ctx.req.url = url
}
setBody(body) {
this.ctx.request.body = body
}
setFiles(files) {
this.ctx.request.files = { file: files }
}
}
describe("usageQuota middleware", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
it("skips the middleware if there is no usage property or method", async () => {
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
})
it("passes through to next middleware if document already exists", async () => {
config.setProd(true)
config.setBody({
_id: "test",
_rev: "test",
})
CouchDB.mockImplementationOnce(() => ({
get: async () => true
}))
await config.executeMiddleware()
expect(config.next).toHaveBeenCalled()
})
it("throws if request has _id, but the document no longer exists", async () => {
config.setBody({
_id: "123",
_rev: "test",
})
config.setProd(true)
CouchDB.mockImplementationOnce(() => ({
get: async () => {
throw new Error()
}
}))
await config.executeMiddleware()
expect(config.throw).toHaveBeenCalledWith(404, `${config.ctx.request.body._id} does not exist`)
})
it("calculates and persists the correct usage quota for the relevant action", async () => {
config.setUrl("/rows")
await config.executeMiddleware()
expect(usageQuota.update).toHaveBeenCalledWith("rows", 1)
expect(config.next).toHaveBeenCalled()
})
// it("calculates the correct file size from a file upload call and adds it to quota", async () => {
// config.setUrl("/upload")
// config.setProd(true)
// config.setFiles([
// {
// size: 100
// },
// {
// size: 10000
// },
// ])
// await config.executeMiddleware()
// expect(usageQuota.update).toHaveBeenCalledWith("storage", 10100)
// expect(config.next).toHaveBeenCalled()
// })
})

View File

@ -1,4 +1,3 @@
const env = require("../../../environment")
const TestConfig = require("../../../tests/utilities/TestConfiguration") const TestConfig = require("../../../tests/utilities/TestConfiguration")
const syncApps = jest.fn() const syncApps = jest.fn()
@ -14,7 +13,6 @@ describe("run", () => {
beforeEach(async () => { beforeEach(async () => {
await config.init() await config.init()
env._set("USE_QUOTAS", 1)
}) })
afterAll(config.end) afterAll(config.end)

View File

@ -1,37 +0,0 @@
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const TestConfig = require("../../../../tests/utilities/TestConfiguration")
const { getUsageQuotaDoc, update, Properties } = require("../../../../utilities/usageQuota")
const syncApps = require("../syncApps")
const env = require("../../../../environment")
describe("syncApps", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
env._set("USE_QUOTAS", 1)
})
afterAll(config.end)
it("runs successfully", async () => {
// create the usage quota doc and mock usages
const db = getGlobalDB()
await getUsageQuotaDoc(db)
await update(Properties.APPS, 3)
let usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.apps).toEqual(3)
// create an extra app to test the migration
await config.createApp("quota-test")
// migrate
await syncApps.run()
// assert the migration worked
usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.apps).toEqual(2)
})
})

View File

@ -0,0 +1,32 @@
import TestConfig from "../../../../tests/utilities/TestConfiguration"
import * as syncApps from "../syncApps"
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro"
describe("syncApps", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
})
afterAll(config.end)
it("runs successfully", async () => {
// create the usage quota doc and mock usages
await quotas.getQuotaUsage()
await quotas.setUsage(3, StaticQuotaName.APPS, QuotaUsageType.STATIC)
let usageDoc = await quotas.getQuotaUsage()
expect(usageDoc.usageQuota.apps).toEqual(3)
// create an extra app to test the migration
await config.createApp("quota-test")
// migrate
await syncApps.run()
// assert the migration worked
usageDoc = await quotas.getQuotaUsage()
expect(usageDoc.usageQuota.apps).toEqual(2)
})
})

View File

@ -1,43 +0,0 @@
const { getGlobalDB } = require("@budibase/backend-core/tenancy")
const TestConfig = require("../../../../tests/utilities/TestConfiguration")
const { getUsageQuotaDoc, update, Properties } = require("../../../../utilities/usageQuota")
const syncRows = require("../syncRows")
const env = require("../../../../environment")
describe("syncRows", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
env._set("USE_QUOTAS", 1)
})
afterAll(config.end)
it("runs successfully", async () => {
// create the usage quota doc and mock usages
const db = getGlobalDB()
await getUsageQuotaDoc(db)
await update(Properties.ROW, 300)
let usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.rows).toEqual(300)
// app 1
await config.createTable()
await config.createRow()
// app 2
await config.createApp("second-app")
await config.createTable()
await config.createRow()
await config.createRow()
// migrate
await syncRows.run()
// assert the migration worked
usageDoc = await getUsageQuotaDoc(db)
expect(usageDoc.usageQuota.rows).toEqual(3)
})
})

View File

@ -0,0 +1,38 @@
import TestConfig from "../../../../tests/utilities/TestConfiguration"
import * as syncRows from "../syncRows"
import { quotas, QuotaUsageType, StaticQuotaName } from "@budibase/pro"
describe("syncRows", () => {
let config = new TestConfig(false)
beforeEach(async () => {
await config.init()
})
afterAll(config.end)
it("runs successfully", async () => {
// create the usage quota doc and mock usages
await quotas.getQuotaUsage()
await quotas.setUsage(300, StaticQuotaName.ROWS, QuotaUsageType.STATIC)
let usageDoc = await quotas.getQuotaUsage()
expect(usageDoc.usageQuota.rows).toEqual(300)
// app 1
await config.createTable()
await config.createRow()
// app 2
await config.createApp("second-app")
await config.createTable()
await config.createRow()
await config.createRow()
// migrate
await syncRows.run()
// assert the migration worked
usageDoc = await quotas.getQuotaUsage()
expect(usageDoc.usageQuota.rows).toEqual(3)
})
})

View File

@ -3,3 +3,5 @@ declare module "@budibase/backend-core/tenancy"
declare module "@budibase/backend-core/db" declare module "@budibase/backend-core/db"
declare module "@budibase/backend-core/context" declare module "@budibase/backend-core/context"
declare module "@budibase/backend-core/cache" declare module "@budibase/backend-core/cache"
declare module "@budibase/backend-core/permissions"
declare module "@budibase/backend-core/roles"

View File

@ -1,72 +0,0 @@
const getTenantId = jest.fn()
jest.mock("@budibase/backend-core/tenancy", () => ({
getTenantId
}))
const usageQuota = require("../../usageQuota")
const env = require("../../../environment")
class TestConfiguration {
constructor() {
this.enableQuotas()
}
enableQuotas = () => {
env.USE_QUOTAS = 1
}
disableQuotas = () => {
env.USE_QUOTAS = null
}
setTenantId = (tenantId) => {
getTenantId.mockReturnValue(tenantId)
}
setExcludedTenants = (tenants) => {
env.EXCLUDE_QUOTAS_TENANTS = tenants
}
reset = () => {
this.disableQuotas()
this.setExcludedTenants(null)
}
}
describe("usageQuota", () => {
let config
beforeEach(() => {
config = new TestConfiguration()
})
afterEach(() => {
config.reset()
})
describe("useQuotas", () => {
it("works when no settings have been provided", () => {
config.reset()
expect(usageQuota.useQuotas()).toBe(false)
})
it("honours USE_QUOTAS setting", () => {
config.disableQuotas()
expect(usageQuota.useQuotas()).toBe(false)
config.enableQuotas()
expect(usageQuota.useQuotas()).toBe(true)
})
it("honours EXCLUDE_QUOTAS_TENANTS setting", () => {
config.setTenantId("test")
// tenantId is in the list
config.setExcludedTenants("test, test2, test2")
expect(usageQuota.useQuotas()).toBe(false)
config.setExcludedTenants("test,test2,test2")
expect(usageQuota.useQuotas()).toBe(false)
// tenantId is not in the list
config.setTenantId("other")
expect(usageQuota.useQuotas()).toBe(true)
})
})
})

View File

@ -0,0 +1,10 @@
{
// Used for building with tsc
"extends": "./tsconfig.json",
"exclude": [
"node_modules",
"**/*.json",
"**/*.spec.js",
"**/*.spec.ts"
]
}

View File

@ -19,7 +19,7 @@
"exclude": [ "exclude": [
"node_modules", "node_modules",
"**/*.json", "**/*.json",
"**/*.spec.ts", "**/*.spec.js",
"**/*.spec.js" // "**/*.spec.ts" // don't exclude spec.ts files for editor support
] ]
} }

View File

@ -2531,13 +2531,13 @@
dependencies: dependencies:
"@types/istanbul-lib-report" "*" "@types/istanbul-lib-report" "*"
"@types/jest@^26.0.23": "@types/jest@^27.4.1":
version "26.0.24" version "27.4.1"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.24.tgz#943d11976b16739185913a1936e0de0c4a7d595a" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
integrity sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w== integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
dependencies: dependencies:
jest-diff "^26.0.0" jest-matcher-utils "^27.0.0"
pretty-format "^26.0.0" pretty-format "^27.0.0"
"@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.8": "@types/json-schema@*", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.8":
version "7.0.9" version "7.0.9"
@ -2577,6 +2577,13 @@
"@types/koa-compose" "*" "@types/koa-compose" "*"
"@types/node" "*" "@types/node" "*"
"@types/koa__router@^8.0.11":
version "8.0.11"
resolved "https://registry.yarnpkg.com/@types/koa__router/-/koa__router-8.0.11.tgz#d7b37e6db934fc072ea1baa2ab92bc8ac4564f3e"
integrity sha512-WXgKWpBsbS14kzmzD9LeFapOIa678h7zvUHxDwXwSx4ETKXhXLVUAToX6jZ/U7EihM7qwyD9W/BZvB0MRu7MTQ==
dependencies:
"@types/koa" "*"
"@types/lodash@^4.14.179": "@types/lodash@^4.14.179":
version "4.14.180" version "4.14.180"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
@ -7375,7 +7382,7 @@ jest-diff@^24.9.0:
jest-get-type "^24.9.0" jest-get-type "^24.9.0"
pretty-format "^24.9.0" pretty-format "^24.9.0"
jest-diff@^26.0.0, jest-diff@^26.6.2: jest-diff@^26.6.2:
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394"
integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==
@ -7614,7 +7621,7 @@ jest-matcher-utils@^26.6.2:
jest-get-type "^26.3.0" jest-get-type "^26.3.0"
pretty-format "^26.6.2" pretty-format "^26.6.2"
jest-matcher-utils@^27.5.1: jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1:
version "27.5.1" version "27.5.1"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
@ -10413,7 +10420,7 @@ pretty-format@^24.9.0:
ansi-styles "^3.2.0" ansi-styles "^3.2.0"
react-is "^16.8.4" react-is "^16.8.4"
pretty-format@^26.0.0, pretty-format@^26.6.2: pretty-format@^26.6.2:
version "26.6.2" version "26.6.2"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93"
integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==
@ -10423,7 +10430,7 @@ pretty-format@^26.0.0, pretty-format@^26.6.2:
ansi-styles "^4.0.0" ansi-styles "^4.0.0"
react-is "^17.0.1" react-is "^17.0.1"
pretty-format@^27.5.1: pretty-format@^27.0.0, pretty-format@^27.5.1:
version "27.5.1" version "27.5.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==

View File

@ -75,7 +75,8 @@ describe("/api/global/auth", () => {
afterEach(() => { afterEach(() => {
expect(strategyFactory).toBeCalledWith( expect(strategyFactory).toBeCalledWith(
chosenConfig, chosenConfig,
`http://localhost:10000/api/global/auth/${TENANT_ID}/oidc/callback` `http://localhost:10000/api/global/auth/${TENANT_ID}/oidc/callback`,
expect.any(Function)
) )
}) })

File diff suppressed because it is too large Load Diff