diff --git a/controllers/_common/utlis_fct.js b/controllers/_common/utlis_fct.js index a365273..a4a96fe 100644 --- a/controllers/_common/utlis_fct.js +++ b/controllers/_common/utlis_fct.js @@ -205,8 +205,9 @@ module.exports = { .bulkCreate( really_new_tags.map(tag => { return { - // no matter of the kind of user, creating tags like that should be reviewed - state: tagState.PENDING, + // by default, creating tags like that should be reviewed + // (except if it is created by an admin) + state: (tag.hasOwnProperty("state")) ? tag.state : tagState.PENDING, text: tag.text, category_id: tag.category_id, // some timestamps must be inserted @@ -329,8 +330,9 @@ function store_single_exercise(user, exercise_data, existent_tags, really_new_ta .bulkCreate( really_new_tags.map(tag => { return { - // no matter of the kind of user, creating tags like that should be reviewed - state: tagState.PENDING, + // by default, creating tags like that should be reviewed + // (except if it is created by an admin) + state: (tag.hasOwnProperty("state")) ? tag.state : tagState.PENDING, text: tag.text, category_id: tag.category_id, // some timestamps must be inserted diff --git a/controllers/exercises/updateExercise.js b/controllers/exercises/updateExercise.js index e049b03..f73cda6 100644 --- a/controllers/exercises/updateExercise.js +++ b/controllers/exercises/updateExercise.js @@ -132,8 +132,9 @@ function insert_new_tags_if_there_is_at_least_one(tags_to_be_inserted, t) { return models .Tag .bulkCreate(tags_to_be_inserted.map(tag => ({ - // no matter of the kind of user, creating tags like that should be reviewed - state: tagState.NOT_VALIDATED, + // by default, creating tags like that should be reviewed + // (except if it is created by an admin) + state: (tag.hasOwnProperty("state")) ? tag.state : tagState.PENDING, text: tag.text, category_id: tag.category_id, // some timestamps must be inserted diff --git a/middlewares/rules/bulk.js b/middlewares/rules/bulk.js index 4f4d3b7..21ea23a 100644 --- a/middlewares/rules/bulk.js +++ b/middlewares/rules/bulk.js @@ -1,7 +1,7 @@ const chain = require('connect-chain-if'); const {check_credentials_on_exercises} = require("../../controllers/_common/utlis_fct"); -const {pass_middleware, check_exercise_state} = require("./common_sub_middlewares"); +const {pass_middleware, check_exercise_state, check_tags_state} = require("./common_sub_middlewares"); const check_user_role = require("../check_user_role"); const {USERS} = require("../../controllers/_common/constants"); @@ -46,6 +46,27 @@ module.exports = (operation) => (req, res, next) => { ), pass_middleware ), + // If endpoint === createMultipleExercises , we should check the state given in exercise tags + chain.if( + operation["x-operation"] === "createMultipleExercises", + // as this endpoint use 2 different schema, extraction is a little different + check_tags_state( + Array + .from( + (req.is("json")) + ? req.body + : req.body.exercisesData, + // Only takes tags objects + ex => ex.tags + ) + // if no tags were given + .filter(s => s !== undefined) + // Only takes tags objects + .filter(tags => tags.filter(tag => isNaN(tag))) + // reduce result to an array of dimension 1 (needed for middelware) + .reduce((acc, val) => acc.concat(val), []) + ) + ), // If endpoint === createMultipleTags , we should check if the user is authorized to include "state" property chain.if( operation["x-operation"] === "createMultipleTags", diff --git a/middlewares/rules/common_sub_middlewares/index.js b/middlewares/rules/common_sub_middlewares/index.js index 081c278..71b9ee1 100644 --- a/middlewares/rules/common_sub_middlewares/index.js +++ b/middlewares/rules/common_sub_middlewares/index.js @@ -3,22 +3,39 @@ const {USERS, TAGS} = require("../../../controllers/_common/constants"); const not_allowed_for_user = [TAGS.VALIDATED, TAGS.NOT_VALIDATED]; const authorizedUsers = [USERS.ADMIN, USERS.SUPER_ADMIN]; +// Create an Forbidden message +function create_forbidden_error() { + let error = new Error("FORBIDDEN"); + error.message = "It seems you tried to set a state reserved for admin : " + + "This incident will be reported"; + error.status = 403; + return error; +} + module.exports = { // check if states given are allowed in this case check_exercise_state: function (states) { return (_req, _res, _next) => { /* istanbul ignore next */ if (!authorizedUsers.includes(_req.user.role) && states.some(state => not_allowed_for_user.includes(state))) { - let error = new Error("FORBIDDEN"); - error.message = "It seems you tried to set a state reserved for admin : " + - "This incident will be reported"; - error.status = 403; + let error = create_forbidden_error(); _next(error); } else { _next() } }; }, + // check if given tag(s) are allowed in this case + check_tags_state: function(tags) { + return (_req, _res, _next) => { + if (!authorizedUsers.includes(_req.user.role) && tags.some(tag => tag.hasOwnProperty("state")) ) { + let error = create_forbidden_error(); + _next(error); + } else { + _next() + } + } + }, // Nothing to do pass_middleware: function (req, res, next) { next(); diff --git a/middlewares/rules/exercises.js b/middlewares/rules/exercises.js index 80206e3..0c135d9 100644 --- a/middlewares/rules/exercises.js +++ b/middlewares/rules/exercises.js @@ -1,6 +1,6 @@ const chain = require('connect-chain-if'); const {check_credentials_on_exercises, validated_tag_count} = require("../../controllers/_common/utlis_fct"); -const {pass_middleware, check_exercise_state} = require("./common_sub_middlewares"); +const {pass_middleware, check_exercise_state, check_tags_state} = require("./common_sub_middlewares"); // Arrays for check const check_credentials_endpoints = ["UpdateExercise", "createSingleExercise"]; @@ -27,7 +27,14 @@ module.exports = (operation) => (req, res, next) => { check_exercise_state([req.body.state].filter(s => s !== undefined)), pass_middleware ), - // Third check that user have add at least 3 validated tags + // Third check that user is allowed to use stats for tag(s) + chain.if( + check_credentials_endpoints.includes(operation["x-operation"]), + // to deal with the fact this security middelware deal with other endpoint that might not have this property + check_tags_state( (req.body.tags || []).filter(tag => isNaN(tag))), + pass_middleware + ), + // Fourth check that user have add at least 3 validated tags chain.if( check_credentials_endpoints.includes(operation["x-operation"]), (_req, _res, _next) => { diff --git a/openapi/definitions.yaml b/openapi/definitions.yaml index ee515a7..469cd00 100644 --- a/openapi/definitions.yaml +++ b/openapi/definitions.yaml @@ -294,6 +294,14 @@ components: required: - text - category_id +# TagProposal with state (only for admin) + TagProposalWithState: + allOf: + - $ref: "#/components/schemas/TagProposal" + - type: object + properties: + state: + $ref: "#/components/schemas/TagState" # Tag updated / validated TagFull: allOf: @@ -329,8 +337,8 @@ components: - type: integer minimum: 0 description: "A Tag ID ( already existent in database )" - - $ref: "#/components/schemas/TagProposal" - description: "A not-existent Tag we want to add" + - $ref: "#/components/schemas/TagProposalWithState" + description: "A not-existent Tag with state we want to add" description: "Mixed array that contains existent tag(s) or not" uniqueItems: true minItems: 3 # We must always put at least N tag(s) in the database diff --git a/tests/endpoints.test.js b/tests/endpoints.test.js index d0c9261..c2eadaf 100644 --- a/tests/endpoints.test.js +++ b/tests/endpoints.test.js @@ -453,7 +453,8 @@ describe("Complex scenarios", () => { tags: some_tags_ids.concat( ["SOME_TAG1", "SOME_TAG2", "SOME_TAG3", "some_Tag3"].map(tag => ({ text: tag, - category_id: tag_categories_ids[0] + category_id: tag_categories_ids[0], + state: "PENDING" })) ), "state": "DRAFT" @@ -479,6 +480,7 @@ describe("Complex scenarios", () => { let data = response.data[0]; expect(data.version).toBe(0); expect(data.state).toBe("DRAFT"); + expect(data.tags.some(tag => tag.state === "PENDING")).toBe(true); // A simple user should not be able to delete exercises await request @@ -1050,6 +1052,7 @@ describe("Using multipart/form-data (instead of JSON)", () => { .field("tags[1][category_id]", 1) .field("tags[2][text]", "MULTI PART exercise 3") .field("tags[2][category_id]", 1) + .field("tags[2][state]", "DEPRECATED") .field("tags[3]", 1) .field("tags[4]", 2) .field("tags[5]", 3); @@ -1058,6 +1061,10 @@ describe("Using multipart/form-data (instead of JSON)", () => { const exercise = await search_exercise(1, search_criteria); expect(exercise.data[0].file).not.toBe(null); expect(exercise.data[0].url).not.toBe(null); + // See if we can find the deprecated tag + expect(exercise.data[0].tags + .some(tag => tag["tag_text"] === "MULTI PART exercise 3" && tag.state === "DEPRECATED") + ).toBe(true); responseTmp = await request .put("/api/exercises/" + exercise.data[0].id) @@ -1072,11 +1079,16 @@ describe("Using multipart/form-data (instead of JSON)", () => { .field("tags[0][category_id]", 1) .field("tags[1][text]", "MULTI PART exercise 2") .field("tags[1][category_id]", 1) + // This time, let's see if the system had recognized the similar tags (including the one with no state set) .field("tags[2][text]", "MULTI PART exercise 3") .field("tags[2][category_id]", 1) .field("tags[3]", 1) .field("tags[4]", 2) - .field("tags[5]", 3); + .field("tags[5]", 3) + // Add a new tag with state ( to trigger validation & testing ) + .field("tags[6][text]", "MULTI PART exercise 42") + .field("tags[6][category_id]", 1) + .field("tags[6][state]", "NOT_VALIDATED"); expect(responseTmp.status).toBe(200); const exercise2 = await search_exercise(1, search_criteria); @@ -1084,8 +1096,15 @@ describe("Using multipart/form-data (instead of JSON)", () => { expect(exercise2.data[0].url).toBe(exercise2.data[0].url); expect(exercise2.data[0].title).toBe(title); expect(exercise2.data[0].description).toBe("Something changes ..."); - expect(exercise2.data[0].tags).toHaveLength(exercise.data[0].tags.length); - + expect(exercise2.data[0].tags).toHaveLength(exercise.data[0].tags.length + 1); + // This time, let's see if the system had recognized the similar tags (including the one with no state set) + expect(exercise2.data[0].tags + .some(tag => tag["tag_text"] === "MULTI PART exercise 3" && tag.state === "DEPRECATED") + ).toBe(true); + // And also find the NOT_VALIDATED tag + expect(exercise2.data[0].tags + .some(tag => tag["tag_text"] === "MULTI PART exercise 42" && tag.state === "NOT_VALIDATED") + ).toBe(true); }); it("Should be able to upload multiple exercises with their linked files then delete one of them", async () => { @@ -1284,6 +1303,25 @@ describe("Validations testing", () => { expect(responseTemp.status).toBe(403); }); + it("POST /api/create_exercise : An simple user cannot insert a exercise with tags state", async () => { + // creates one exercise + const some_exercise_data = { + "title": "HELLO WORLD", + "description": "Some verrrrrrrrrry long description here", + tags: ["SOME_TAG1", "SOME_TAG2", "SOME_TAG3"].map(text => ({ + text: text, + category_id: 42, + state: "VALIDATED" + })) + }; + let responseTemp = await request + .post("/api/create_exercise") + .set('Authorization', 'bearer ' + JWT_TOKEN_2) + .set('Content-Type', 'application/json') + .send(some_exercise_data); + expect(responseTemp.status).toBe(403); + }); + it("POST /api/ : Cannot create/update an exercise with not expected validated tag count", async () => { // creates one exercise with 2 const title = "HELLO WORLD";