Add Joi validation for delete button

This commit is contained in:
rijkvanzanten
2020-08-28 18:15:36 -04:00
parent 1fd719b51c
commit a9f384f1c6
9 changed files with 258 additions and 13 deletions

View File

@@ -101,12 +101,12 @@ export default class FieldsService {
const result = [...columnsWithSystem, ...aliasFieldsAsField];
// Filter the result so we only return the fields you have read access to
if (this.accountability) {
if (this.accountability && this.accountability.admin !== true) {
const permissions = await this.knex.select('collection', 'fields').from('directus_permissions').where({ role: this.accountability.role, action: 'read' });
const allowedFieldsInCollection: Record<string, string[]> = {};
permissions.forEach((permission) => {
allowedFieldsInCollection[permission.collection] = permission.fields.split(',');
allowedFieldsInCollection[permission.collection] = (permission.fields || '').split(',');
});
if (collection && allowedFieldsInCollection.hasOwnProperty(collection) === false) {
@@ -125,7 +125,7 @@ export default class FieldsService {
}
async readOne(collection: string, field: string) {
if (this.accountability) {
if (this.accountability && this.accountability.admin !== true) {
const permissions = await this.knex
.select('fields')
.from('directus_permissions')
@@ -137,7 +137,7 @@ export default class FieldsService {
if (!permissions) throw new ForbiddenException();
if (permissions.fields !== '*') {
const allowedFields = permissions.fields.split(',');
const allowedFields = (permissions.fields || '').split(',');
if (allowedFields.includes(field) === false) throw new ForbiddenException();
}
}

View File

@@ -1,6 +1,11 @@
import { Filter } from '../types';
import Joi, { AnySchema } from 'joi';
/**
* @TODO
* This is copy pasted between app and api. Make this a reusable module.
*/
export default function generateJoi(filter: Filter | null) {
filter = filter || {};
@@ -12,10 +17,6 @@ export default function generateJoi(filter: Filter | null) {
if (isField) {
const operator = Object.keys(value)[0];
/** @TODO
* - Extend with all operators
*/
if (operator === '_eq') {
schema[key] = Joi.any().equal(Object.values(value)[0]);
}
@@ -24,6 +25,30 @@ export default function generateJoi(filter: Filter | null) {
schema[key] = Joi.any().not(Object.values(value)[0]);
}
if (operator === '_contains') {
schema[key] = Joi.string().custom((value, helpers) => {
const contains = value.includes(Object.values(value)[0]);
if (contains === false) {
return helpers.error(`"${key}" must include "${Object.values(value)[0]}"`);
}
return value;
});
}
if (operator === '_ncontains') {
schema[key] = Joi.string().custom((value, helpers) => {
const contains = value.includes(Object.values(value)[0]);
if (contains === true) {
return helpers.error(`"${key}" can't include "${Object.values(value)[0]}"`);
}
return value;
});
}
if (operator === '_in') {
schema[key] = Joi.any().equal(...(Object.values(value)[0] as (string | number)[]));
}
@@ -36,9 +61,43 @@ export default function generateJoi(filter: Filter | null) {
schema[key] = Joi.number().greater(Number(Object.values(value)[0]));
}
if (operator === '_gte') {
schema[key] = Joi.number().min(Number(Object.values(value)[0]));
}
if (operator === '_lt') {
schema[key] = Joi.number().less(Number(Object.values(value)[0]));
}
if (operator === '_lte') {
schema[key] = Joi.number().max(Number(Object.values(value)[0]));
}
if (operator === '_null') {
schema[key] = Joi.any().valid(null);
}
if (operator === '_nnull') {
schema[key] = Joi.any().invalid(null);
}
if (operator === '_empty') {
schema[key] = Joi.any().valid('');
}
if (operator === '_nempty') {
schema[key] = Joi.any().invalid('');
}
if (operator === '_between') {
const values = Object.values(value)[0] as number[];
schema[key] = Joi.number().greater(values[0]).less(values[1]);
}
if (operator === '_nbetween') {
const values = Object.values(value)[0] as number[];
schema[key] = Joi.number().less(values[0]).greater(values[1]);
}
}
}

45
app/package-lock.json generated
View File

@@ -1274,6 +1274,11 @@
"integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==",
"dev": true
},
"@hapi/formula": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-2.0.0.tgz",
"integrity": "sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A=="
},
"@hapi/hoek": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz",
@@ -1292,6 +1297,11 @@
"@hapi/topo": "3.x.x"
}
},
"@hapi/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw=="
},
"@hapi/topo": {
"version": "3.1.6",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz",
@@ -14110,6 +14120,41 @@
"supports-color": "^7.0.0"
}
},
"joi": {
"version": "17.2.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.2.1.tgz",
"integrity": "sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==",
"requires": {
"@hapi/address": "^4.1.0",
"@hapi/formula": "^2.0.0",
"@hapi/hoek": "^9.0.0",
"@hapi/pinpoint": "^2.0.0",
"@hapi/topo": "^5.0.0"
},
"dependencies": {
"@hapi/address": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-4.1.0.tgz",
"integrity": "sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==",
"requires": {
"@hapi/hoek": "^9.0.0"
}
},
"@hapi/hoek": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.0.4.tgz",
"integrity": "sha512-EwaJS7RjoXUZ2cXXKZZxZqieGtc7RbvQhUy8FwDoMQtxWVi14tFjeFCYPZAM1mBCpOpiBpyaZbb9NeHc7eGKgw=="
},
"@hapi/topo": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz",
"integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==",
"requires": {
"@hapi/hoek": "^9.0.0"
}
}
}
},
"js-beautify": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.11.0.tgz",

View File

@@ -50,6 +50,7 @@
"date-fns": "^2.14.0",
"diff": "^4.0.2",
"htmlhint": "^0.14.1",
"joi": "^17.2.1",
"js-yaml": "^3.14.0",
"jshint": "^2.11.1",
"jsonlint": "^1.6.3",

View File

@@ -19,6 +19,8 @@
"public": "Public",
"public_description": "Controls what API data is available without authenticating.",
"not_allowed": "Not Allowed",
"nested_files_folders_will_be_moved": "Nested files and folders will be moved one level up.",
"markdown": "Markdown",

View File

@@ -366,7 +366,7 @@ export default defineComponent({
function useTable() {
const tableSort = computed(() => {
if (sort.value.startsWith('-')) {
if (sort.value?.startsWith('-')) {
return { by: sort.value.substring(1), desc: true };
} else {
return { by: sort.value, desc: false };

View File

@@ -44,14 +44,14 @@
</template>
<template #actions>
<v-dialog v-if="!isNew" v-model="confirmDelete">
<v-dialog v-if="!isNew" v-model="confirmDelete" :disabled="deleteAllowed === false">
<template #activator="{ on }">
<v-button
rounded
icon
class="action-delete"
v-tooltip.bottom="$t('delete_forever')"
:disabled="item === null"
v-tooltip.bottom="deleteAllowed ? $t('delete_forever') : $t('not_allowed')"
:disabled="item === null || deleteAllowed === false"
@click="on"
v-if="collectionInfo.meta.singleton === false"
>
@@ -185,6 +185,8 @@ import i18n from '@/lang';
import marked from 'marked';
import useShortcut from '@/composables/use-shortcut';
import { NavigationGuard } from 'vue-router';
import { usePermissionsStore, useUserStore } from '@/stores';
import generateJoi from '@/utils/generate-joi';
type Values = {
[field: string]: any;
@@ -210,6 +212,9 @@ export default defineComponent({
},
},
setup(props) {
const permissionsStore = usePermissionsStore();
const userStore = useUserStore();
const { collection, primaryKey } = toRefs(props);
const { breadcrumb } = useBreadcrumb();
@@ -276,6 +281,8 @@ export default defineComponent({
return next();
};
const { deleteAllowed } = usePermissions();
return {
item,
loading,
@@ -307,6 +314,7 @@ export default defineComponent({
leaveTo,
discardAndLeave,
navigationGuard,
deleteAllowed,
};
function useBreadcrumb() {
@@ -357,6 +365,31 @@ export default defineComponent({
edits.value = {};
router.push(leaveTo.value);
}
function usePermissions() {
const permissions = computed(() => {
return permissionsStore.state.permissions.filter((permission) => permission.collection === props.collection);
});
const deleteAllowed = computed(() => {
if (userStore.isAdmin.value === true) return true;
const deletePermission = permissions.value.find((permission) => permission.action === 'delete');
if (!deletePermission) return false;
const schema = generateJoi(deletePermission.permissions);
const { error } = schema.validate(item.value);
if (!error) {
return true;
}
return false;
});
return { deleteAllowed };
}
},
});
</script>

View File

@@ -1,10 +1,11 @@
import { createStore } from 'pinia';
import api from '@/api';
import { Permission } from '@/types';
export const usePermissionsStore = createStore({
id: 'permissionsStore',
state: () => ({
permissions: [],
permissions: [] as Permission[],
}),
actions: {
async hydrate() {

View File

@@ -0,0 +1,104 @@
import Joi, { AnySchema } from 'joi';
/**
* @TODO
* This is copy pasted between app and api. Make this a reusable module.
*/
export default function generateJoi(filter: Record<string, any> | null) {
filter = filter || {};
const schema: Record<string, AnySchema> = {};
for (const [key, value] of Object.entries(filter)) {
const isField = key.startsWith('_') === false;
if (isField) {
const operator = Object.keys(value)[0];
if (operator === '_eq') {
schema[key] = Joi.any().equal(Object.values(value)[0]);
}
if (operator === '_neq') {
schema[key] = Joi.any().not(Object.values(value)[0]);
}
if (operator === '_contains') {
schema[key] = Joi.string().custom((value, helpers) => {
const contains = value.includes(Object.values(value)[0]);
if (contains === false) {
return helpers.error(`"${key}" must include "${Object.values(value)[0]}"`);
}
return value;
});
}
if (operator === '_ncontains') {
schema[key] = Joi.string().custom((value, helpers) => {
const contains = value.includes(Object.values(value)[0]);
if (contains === true) {
return helpers.error(`"${key}" can't include "${Object.values(value)[0]}"`);
}
return value;
});
}
if (operator === '_in') {
schema[key] = Joi.any().equal(...(Object.values(value)[0] as (string | number)[]));
}
if (operator === '_nin') {
schema[key] = Joi.any().not(...(Object.values(value)[0] as (string | number)[]));
}
if (operator === '_gt') {
schema[key] = Joi.number().greater(Number(Object.values(value)[0]));
}
if (operator === '_gte') {
schema[key] = Joi.number().min(Number(Object.values(value)[0]));
}
if (operator === '_lt') {
schema[key] = Joi.number().less(Number(Object.values(value)[0]));
}
if (operator === '_lte') {
schema[key] = Joi.number().max(Number(Object.values(value)[0]));
}
if (operator === '_null') {
schema[key] = Joi.any().valid(null);
}
if (operator === '_nnull') {
schema[key] = Joi.any().invalid(null);
}
if (operator === '_empty') {
schema[key] = Joi.any().valid('');
}
if (operator === '_nempty') {
schema[key] = Joi.any().invalid('');
}
if (operator === '_between') {
const values = Object.values(value)[0] as number[];
schema[key] = Joi.number().greater(values[0]).less(values[1]);
}
if (operator === '_nbetween') {
const values = Object.values(value)[0] as number[];
schema[key] = Joi.number().less(values[0]).greater(values[1]);
}
}
}
return Joi.object(schema).unknown();
}