Merge branch 'BUDI-8282/validate-configuration' into BUDI-8282/validate-configuration-for-hidden-views

This commit is contained in:
Adria Navarro 2024-06-04 11:37:54 +02:00
commit 44708a27ff
4 changed files with 178 additions and 132 deletions

View File

@ -3,7 +3,8 @@ import { Ctx } from "@budibase/types"
function validate( function validate(
schema: Joi.ObjectSchema | Joi.ArraySchema, schema: Joi.ObjectSchema | Joi.ArraySchema,
property: string property: string,
opts: { errorPrefix: string } = { errorPrefix: `Invalid ${property}` }
) { ) {
// Return a Koa middleware function // Return a Koa middleware function
return (ctx: Ctx, next: any) => { return (ctx: Ctx, next: any) => {
@ -29,16 +30,26 @@ function validate(
const { error } = schema.validate(params) const { error } = schema.validate(params)
if (error) { if (error) {
ctx.throw(400, `Invalid ${property} - ${error.message}`) let message = error.message
if (opts.errorPrefix) {
message = `Invalid ${property} - ${message}`
}
ctx.throw(400, message)
} }
return next() return next()
} }
} }
export function body(schema: Joi.ObjectSchema | Joi.ArraySchema) { export function body(
return validate(schema, "body") schema: Joi.ObjectSchema | Joi.ArraySchema,
opts?: { errorPrefix: string }
) {
return validate(schema, "body", opts)
} }
export function params(schema: Joi.ObjectSchema | Joi.ArraySchema) { export function params(
return validate(schema, "params") schema: Joi.ObjectSchema | Joi.ArraySchema,
opts?: { errorPrefix: string }
) {
return validate(schema, "params", opts)
} }

View File

@ -368,7 +368,8 @@ describe.each([
await config.api.viewV2.create(newView, { await config.api.viewV2.create(newView, {
status: 400, status: 400,
body: { body: {
message: 'You can\'t make read only the required field "name"', message:
'You can\'t make field "name" readonly because it is a required field.',
status: 400, status: 400,
}, },
}) })
@ -1479,7 +1480,7 @@ describe.each([
body: { body: {
status: 400, status: 400,
message: message:
'Invalid body - Required field "name" is missing in view "view a"', 'To make field "name" required, this field must be present and writable in views: view a.',
}, },
} }
) )
@ -1510,7 +1511,7 @@ describe.each([
body: { body: {
status: 400, status: 400,
message: message:
'Invalid body - Required field "name" is missing in view "view a"', 'To make field "name" required, this field must be present and writable in views: view a.',
}, },
} }
) )

View File

@ -30,7 +30,7 @@ const validateViewSchemas: CustomValidator<Table> = (table, helpers) => {
) )
if (missingField) { if (missingField) {
return helpers.message({ return helpers.message({
custom: `Required field "${missingField}" is missing in view "${view.name}"`, custom: `To make field "${missingField}" required, this field must be present and writable in views: ${view.name}.`,
}) })
} }
} }
@ -40,8 +40,8 @@ const validateViewSchemas: CustomValidator<Table> = (table, helpers) => {
} }
export function tableValidator() { export function tableValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
_id: OPTIONAL_STRING, _id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
type: OPTIONAL_STRING.valid("table", "internal", "external"), type: OPTIONAL_STRING.valid("table", "internal", "external"),
@ -50,32 +50,39 @@ export function tableValidator() {
name: Joi.string().required(), name: Joi.string().required(),
views: Joi.object(), views: Joi.object(),
rows: Joi.array(), rows: Joi.array(),
}).custom(validateViewSchemas).unknown(true)) })
.custom(validateViewSchemas)
.unknown(true),
{ errorPrefix: "" }
)
} }
export function nameValidator() { export function nameValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
name: OPTIONAL_STRING, name: OPTIONAL_STRING,
})) })
)
} }
export function datasourceValidator() { export function datasourceValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
_id: Joi.string(), _id: Joi.string(),
_rev: Joi.string(), _rev: Joi.string(),
type: OPTIONAL_STRING.allow("datasource_plus"), type: OPTIONAL_STRING.allow("datasource_plus"),
relationships: Joi.array().items(Joi.object({ relationships: Joi.array().items(
Joi.object({
from: Joi.string().required(), from: Joi.string().required(),
to: Joi.string().required(), to: Joi.string().required(),
cardinality: Joi.valid("1:N", "1:1", "N:N").required() cardinality: Joi.valid("1:N", "1:1", "N:N").required(),
})), })
}).unknown(true)) ),
}).unknown(true)
)
} }
function filterObject() { function filterObject() {
// prettier-ignore
return Joi.object({ return Joi.object({
string: Joi.object().optional(), string: Joi.object().optional(),
fuzzy: Joi.object().optional(), fuzzy: Joi.object().optional(),
@ -92,8 +99,8 @@ function filterObject() {
} }
export function internalSearchValidator() { export function internalSearchValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
tableId: OPTIONAL_STRING, tableId: OPTIONAL_STRING,
query: filterObject(), query: filterObject(),
limit: OPTIONAL_NUMBER, limit: OPTIONAL_NUMBER,
@ -101,8 +108,11 @@ export function internalSearchValidator() {
sortOrder: OPTIONAL_STRING, sortOrder: OPTIONAL_STRING,
sortType: OPTIONAL_STRING, sortType: OPTIONAL_STRING,
paginate: Joi.boolean(), paginate: Joi.boolean(),
bookmark: Joi.alternatives().try(OPTIONAL_STRING, OPTIONAL_NUMBER).optional(), bookmark: Joi.alternatives()
})) .try(OPTIONAL_STRING, OPTIONAL_NUMBER)
.optional(),
})
)
} }
export function externalSearchValidator() { export function externalSearchValidator() {
@ -124,11 +134,13 @@ export function externalSearchValidator() {
} }
export function datasourceQueryValidator() { export function datasourceQueryValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
endpoint: Joi.object({ endpoint: Joi.object({
datasourceId: Joi.string().required(), datasourceId: Joi.string().required(),
operation: Joi.string().required().valid(...Object.values(DataSourceOperation)), operation: Joi.string()
.required()
.valid(...Object.values(DataSourceOperation)),
entityId: Joi.string().required(), entityId: Joi.string().required(),
}).required(), }).required(),
resource: Joi.object({ resource: Joi.object({
@ -141,12 +153,13 @@ export function datasourceQueryValidator() {
page: Joi.string().alphanum().optional(), page: Joi.string().alphanum().optional(),
limit: Joi.number().optional(), limit: Joi.number().optional(),
}).optional(), }).optional(),
})) })
)
} }
export function webhookValidator() { export function webhookValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
live: Joi.bool(), live: Joi.bool(),
_id: OPTIONAL_STRING, _id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
@ -156,38 +169,49 @@ export function webhookValidator() {
type: Joi.string().required().valid(WebhookActionType.AUTOMATION), type: Joi.string().required().valid(WebhookActionType.AUTOMATION),
target: Joi.string().required(), target: Joi.string().required(),
}).required(), }).required(),
}).unknown(true)) }).unknown(true)
)
} }
export function roleValidator() { export function roleValidator() {
const permLevelArray = Object.values(permissions.PermissionLevel) const permLevelArray = Object.values(permissions.PermissionLevel)
// prettier-ignore
return auth.joiValidator.body(Joi.object({ return auth.joiValidator.body(
Joi.object({
_id: OPTIONAL_STRING, _id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
name: Joi.string().regex(/^[a-zA-Z0-9_]*$/).required(), name: Joi.string()
.regex(/^[a-zA-Z0-9_]*$/)
.required(),
// this is the base permission ID (for now a built in) // this is the base permission ID (for now a built in)
permissionId: Joi.string().valid(...Object.values(permissions.BuiltinPermissionID)).required(), permissionId: Joi.string()
.valid(...Object.values(permissions.BuiltinPermissionID))
.required(),
permissions: Joi.object() permissions: Joi.object()
.pattern(/.*/, [Joi.string().valid(...permLevelArray)]) .pattern(/.*/, [Joi.string().valid(...permLevelArray)])
.optional(), .optional(),
inherits: OPTIONAL_STRING, inherits: OPTIONAL_STRING,
}).unknown(true)) }).unknown(true)
)
} }
export function permissionValidator() { export function permissionValidator() {
const permLevelArray = Object.values(permissions.PermissionLevel) const permLevelArray = Object.values(permissions.PermissionLevel)
// prettier-ignore
return auth.joiValidator.params(Joi.object({ return auth.joiValidator.params(
level: Joi.string().valid(...permLevelArray).required(), Joi.object({
level: Joi.string()
.valid(...permLevelArray)
.required(),
resourceId: Joi.string(), resourceId: Joi.string(),
roleId: Joi.string(), roleId: Joi.string(),
}).unknown(true)) }).unknown(true)
)
} }
export function screenValidator() { export function screenValidator() {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
showNavigation: OPTIONAL_BOOLEAN, showNavigation: OPTIONAL_BOOLEAN,
width: OPTIONAL_STRING, width: OPTIONAL_STRING,
@ -195,7 +219,9 @@ export function screenValidator() {
route: Joi.string().required(), route: Joi.string().required(),
roleId: Joi.string().required().allow(""), roleId: Joi.string().required().allow(""),
homeScreen: OPTIONAL_BOOLEAN, homeScreen: OPTIONAL_BOOLEAN,
}).required().unknown(true), })
.required()
.unknown(true),
props: Joi.object({ props: Joi.object({
_id: Joi.string().required(), _id: Joi.string().required(),
_component: Joi.string().required(), _component: Joi.string().required(),
@ -204,12 +230,14 @@ export function screenValidator() {
type: OPTIONAL_STRING, type: OPTIONAL_STRING,
table: OPTIONAL_STRING, table: OPTIONAL_STRING,
layoutId: OPTIONAL_STRING, layoutId: OPTIONAL_STRING,
}).required().unknown(true), })
}).unknown(true)) .required()
.unknown(true),
}).unknown(true)
)
} }
function generateStepSchema(allowStepTypes: string[]) { function generateStepSchema(allowStepTypes: string[]) {
// prettier-ignore
return Joi.object({ return Joi.object({
stepId: Joi.string().required(), stepId: Joi.string().required(),
id: Joi.string().required(), id: Joi.string().required(),
@ -219,33 +247,39 @@ function generateStepSchema(allowStepTypes: string[]) {
icon: Joi.string().required(), icon: Joi.string().required(),
params: Joi.object(), params: Joi.object(),
args: Joi.object(), args: Joi.object(),
type: Joi.string().required().valid(...allowStepTypes), type: Joi.string()
.required()
.valid(...allowStepTypes),
}).unknown(true) }).unknown(true)
} }
export function automationValidator(existing = false) { export function automationValidator(existing = false) {
// prettier-ignore return auth.joiValidator.body(
return auth.joiValidator.body(Joi.object({ Joi.object({
_id: existing ? Joi.string().required() : OPTIONAL_STRING, _id: existing ? Joi.string().required() : OPTIONAL_STRING,
_rev: existing ? Joi.string().required() : OPTIONAL_STRING, _rev: existing ? Joi.string().required() : OPTIONAL_STRING,
name: Joi.string().required(), name: Joi.string().required(),
type: Joi.string().valid("automation").required(), type: Joi.string().valid("automation").required(),
definition: Joi.object({ definition: Joi.object({
steps: Joi.array().required().items(generateStepSchema(["ACTION", "LOGIC"])), steps: Joi.array()
.required()
.items(generateStepSchema(["ACTION", "LOGIC"])),
trigger: generateStepSchema(["TRIGGER"]).allow(null), trigger: generateStepSchema(["TRIGGER"]).allow(null),
}).required().unknown(true), })
}).unknown(true)) .required()
.unknown(true),
}).unknown(true)
)
} }
export function applicationValidator(opts = { isCreate: true }) { export function applicationValidator(opts = { isCreate: true }) {
// prettier-ignore
const base: any = { const base: any = {
_id: OPTIONAL_STRING, _id: OPTIONAL_STRING,
_rev: OPTIONAL_STRING, _rev: OPTIONAL_STRING,
url: OPTIONAL_STRING, url: OPTIONAL_STRING,
template: Joi.object({ template: Joi.object({
templateString: OPTIONAL_STRING, templateString: OPTIONAL_STRING,
}) }),
} }
const appNameValidator = Joi.string() const appNameValidator = Joi.string()

View File

@ -84,7 +84,7 @@ async function guardViewSchema(
if (viewSchemaField.readonly) { if (viewSchemaField.readonly) {
throw new HTTPError( throw new HTTPError(
`You can't make read only the required field "${field.name}"`, `You can't make field "${field.name}" readonly because it is a required field.`,
400 400
) )
} }