diff --git a/rest_server/src/controllers/item_controller.js b/rest_server/src/controllers/item_controller.js index 25cd540f..6a7cb059 100644 --- a/rest_server/src/controllers/item_controller.js +++ b/rest_server/src/controllers/item_controller.js @@ -1,20 +1,35 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. const { isNil } = require('lodash'); -const { MarketplaceItem } = require('../models'); +const { MarketplaceItem, ItemCategory } = require('../models'); const asyncHandler = require('./async_handler'); const yaml = require('js-yaml'); const protocolValidator = require('../utils/protocol'); const error = require('../models/error'); -function checkReadPermission(userInfo, item) { - if (userInfo.admin === true) { +async function checkWritePermission(tokenInfo, item, categories) { + if (tokenInfo.admin === true) { return true; } - if (userInfo.username === item.author) { + if (categories === undefined) { + categories = await MarketplaceItem.getCategories(item); + } + if ( + !categories.some( + category => category.name === ItemCategory.OFFICIAL_EXAMPLE, + ) && + tokenInfo.username === item.author + ) { return true; } - if (item.isPublic) { + return false; +} + +async function checkReadPermission(userInfo, item, categories) { + if ( + item.isPublic || + (await checkWritePermission(userInfo, item, categories)) + ) { return true; } if (!item.isPrivate && (userInfo.grouplist && item.groupList)) { @@ -27,16 +42,6 @@ function checkReadPermission(userInfo, item) { return false; } -function checkWritePermission(tokenInfo, item) { - if (tokenInfo.admin === true) { - return true; - } - if (tokenInfo.username === item.author) { - return true; - } - return false; -} - const list = asyncHandler(async (req, res, next) => { try { const result = await MarketplaceItem.list( @@ -96,7 +101,7 @@ const get = asyncHandler(async (req, res, next) => { if (isNil(result)) { return next(error.createNotFound()); } else { - if (checkReadPermission(req.userInfo, result)) { + if (await checkReadPermission(req.userInfo, result)) { res.status(200).json(result); } else { return next( @@ -114,7 +119,7 @@ const get = asyncHandler(async (req, res, next) => { const update = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkWritePermission(req.tokenInfo, result)) { + if (await checkWritePermission(req.tokenInfo, result)) { result = await MarketplaceItem.update(req.params.itemId, req.body); if (isNil(result)) { return next(error.createNotFound()); @@ -136,7 +141,7 @@ const update = asyncHandler(async (req, res, next) => { const del = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkWritePermission(req.tokenInfo, result)) { + if (await checkWritePermission(req.tokenInfo, result)) { result = await MarketplaceItem.del(req.params.itemId); if (isNil(result)) { return next(error.createNotFound()); @@ -158,7 +163,7 @@ const del = asyncHandler(async (req, res, next) => { const listTags = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkReadPermission(req.userInfo, result)) { + if (await checkReadPermission(req.userInfo, result)) { result = await MarketplaceItem.getTags(result); if (isNil(result)) { return next(error.createNotFound()); @@ -180,7 +185,7 @@ const listTags = asyncHandler(async (req, res, next) => { const addTag = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkReadPermission(req.userInfo, result)) { + if (await checkWritePermission(req.tokenInfo, result)) { result = await MarketplaceItem.addTag(result, req.params.tagId); if (isNil(result)) { return next(error.createNotFound()); @@ -202,7 +207,7 @@ const addTag = asyncHandler(async (req, res, next) => { const deleteTag = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkReadPermission(req.userInfo, result)) { + if (await checkWritePermission(req.tokenInfo, result)) { result = await MarketplaceItem.deleteTag(result, req.params.tagId); if (isNil(result)) { return next(error.createNotFound()); @@ -223,14 +228,13 @@ const deleteTag = asyncHandler(async (req, res, next) => { const listCategories = asyncHandler(async (req, res, next) => { try { - let result = await MarketplaceItem.get(req.params.itemId); - if (checkReadPermission(req.userInfo, result)) { - result = await MarketplaceItem.getCategories(result); - if (isNil(result)) { - return next(error.createNotFound()); - } else { - res.status(200).json(result); - } + const result = await MarketplaceItem.get(req.params.itemId); + const categories = await MarketplaceItem.getCategories(result); + if (isNil(result)) { + return next(error.createNotFound()); + } + if (await checkReadPermission(req.userInfo, result, categories)) { + res.status(200).json(categories); } else { return next( error.createForbidden( @@ -246,7 +250,18 @@ const listCategories = asyncHandler(async (req, res, next) => { const addCategory = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkReadPermission(req.userInfo, result)) { + if (await checkWritePermission(req.tokenInfo, result)) { + const category = await ItemCategory.get(req.params.categoryId); + if ( + category.name === ItemCategory.OFFICIAL_EXAMPLE && + req.tokenInfo.admin !== true + ) { + return next( + error.createForbidden( + `Only admin can set "${ItemCategory.OFFICIAL_EXAMPLE}" category.`, + ), + ); + } result = await MarketplaceItem.addCategory(result, req.params.categoryId); if (isNil(result)) { return next(error.createNotFound()); @@ -268,7 +283,7 @@ const addCategory = asyncHandler(async (req, res, next) => { const deleteCategory = asyncHandler(async (req, res, next) => { try { let result = await MarketplaceItem.get(req.params.itemId); - if (checkReadPermission(req.userInfo, result)) { + if (await checkWritePermission(req.tokenInfo, result)) { result = await MarketplaceItem.deleteCategory( result, req.params.categoryId, diff --git a/rest_server/src/models/item_category.js b/rest_server/src/models/item_category.js index 538c4489..820bc8d9 100644 --- a/rest_server/src/models/item_category.js +++ b/rest_server/src/models/item_category.js @@ -5,6 +5,8 @@ const modelSyncHandler = require('./model_init_handler'); class ItemCategory { constructor(sequelize, DataTypes) { + this.OFFICIAL_EXAMPLE = 'official example'; + this.orm = sequelize.define('ItemCategory', { id: { type: DataTypes.STRING, diff --git a/rest_server/src/models/marketplace_item.js b/rest_server/src/models/marketplace_item.js index 78af945f..312653c1 100644 --- a/rest_server/src/models/marketplace_item.js +++ b/rest_server/src/models/marketplace_item.js @@ -3,6 +3,7 @@ const { isNil, toLower } = require('lodash'); const { Op, fn, col, where, cast } = require('sequelize'); const modelSyncHandler = require('./model_init_handler'); +const ItemCategory = require('./item_category'); class MarketplaceItem { constructor(sequelize, DataTypes) { @@ -123,44 +124,6 @@ class MarketplaceItem { }, ]; } - if (!userInfo.admin) { - filterStatement[Op.or] = [ - { - isPublic: { - [Op.eq]: true, - }, - }, - { - [Op.and]: [ - { - isPrivate: { - [Op.eq]: true, - }, - }, - { - author: { - [Op.eq]: userInfo.username, - }, - }, - ], - }, - { - [Op.and]: [ - { - isPublic: { - [Op.eq]: false, - }, - isPrivate: { - [Op.eq]: false, - }, - groupList: { - [Op.overlap]: userInfo.groupList, - }, - }, - ], - }, - ]; - } const havings = []; const havingArrayAgg = (queryParameter, colName) => { @@ -177,6 +140,65 @@ class MarketplaceItem { havingArrayAgg(tags, 'ItemTags.name'); havingArrayAgg(categories, 'ItemCategories.name'); + if (!userInfo.admin) { + if (filterStatement[Op.and] === undefined) { + filterStatement[Op.and] = []; + } + filterStatement[Op.and].push({ + [Op.or]: [ + { + isPublic: { + [Op.eq]: true, + }, + }, + { + [Op.and]: [ + { + isPrivate: { + [Op.eq]: true, + }, + }, + { + author: { + [Op.eq]: userInfo.username, + }, + }, + ], + }, + { + [Op.and]: [ + { + isPublic: { + [Op.eq]: false, + }, + isPrivate: { + [Op.eq]: false, + }, + groupList: { + [Op.overlap]: userInfo.groupList, + }, + }, + ], + }, + ], + }); + + havings.push({ + [Op.not]: { + [Op.and]: [ + { + isPrivate: true, + }, + where( + fn('array_agg', col('ItemCategories.name')), + Op.contains, + cast([ItemCategory.OFFICIAL_EXAMPLE], 'varchar[]'), + ), + ], + }, + }); + } + const items = await this.orm.findAll({ where: filterStatement, having: havings, @@ -373,17 +395,17 @@ class MarketplaceItem { return await handler(item, this.models); } - async addCategory(item, tagId) { + async addCategory(item, categoryId) { const handler = modelSyncHandler(async item => { - return await item.addItemCategory(tagId); + return await item.addItemCategory(categoryId); }); return await handler(item, this.models); } - async deleteCategory(item, tagId) { + async deleteCategory(item, categoryId) { const handler = modelSyncHandler(async item => { - return await item.removeItemCategory(tagId); + return await item.removeItemCategory(categoryId); }); return await handler(item, this.models); diff --git a/rest_server/src/router.js b/rest_server/src/router.js index c8bc5fc8..852323c3 100644 --- a/rest_server/src/router.js +++ b/rest_server/src/router.js @@ -26,8 +26,8 @@ router router .route('/items/:itemId/tag/:tagId') - .post(token.checkAuthAndGetUserInfo, itemController.addTag) - .delete(token.checkAuthAndGetUserInfo, itemController.deleteTag); + .post(token.checkAuthAndGetTokenInfo, itemController.addTag) + .delete(token.checkAuthAndGetTokenInfo, itemController.deleteTag); router .route('/items/:itemId/category') @@ -35,8 +35,8 @@ router router .route('/items/:itemId/category/:categoryId') - .post(token.checkAuthAndGetUserInfo, itemController.addCategory) - .delete(token.checkAuthAndGetUserInfo, itemController.deleteCategory); + .post(token.checkAuthAndGetTokenInfo, itemController.addCategory) + .delete(token.checkAuthAndGetTokenInfo, itemController.deleteCategory); router .route('/tags')