app/src/routes/api/v1/layer.router.js
const Router = require('koa-router');
const logger = require('logger');
const { RWAPIMicroservice } = require('rw-api-microservice-node');
const LayerModel = require('models/layer.model');
const LayerService = require('services/layer.service');
const DatasetService = require('services/dataset.service');
const RelationshipsService = require('services/relationships.service');
const LayerValidator = require('validators/layer.validator');
const LayerSerializer = require('serializers/layer.serializer');
const LayerDuplicated = require('errors/layerDuplicated.error');
const LayerNotFound = require('errors/layerNotFound.error');
const LayerProtected = require('errors/layerProtected.error');
const LayerNotValid = require('errors/layerNotValid.error');
const { USER_ROLES } = require('app.constants');
const UserService = require('../../../services/user.service');
const router = new Router({});
const serializeObjToQuery = (obj) => Object.keys(obj).reduce((a, k) => {
a.push(`${k}=${encodeURIComponent(obj[k])}`);
return a;
}, []).join('&');
const getHostForPaginationLink = (ctx) => {
if ('x-rw-domain' in ctx.request.header) {
return ctx.request.header['x-rw-domain'];
}
if ('referer' in ctx.request.header) {
const url = new URL(ctx.request.header.referer);
return url.host;
}
return ctx.request.host;
};
class LayerRouter {
static getUser(ctx) {
let user = { ...(ctx.request.query.loggedUser ? JSON.parse(ctx.request.query.loggedUser) : {}), ...ctx.request.body.loggedUser };
if (ctx.request.body.fields) {
user = Object.assign(user, JSON.parse(ctx.request.body.fields.loggedUser));
}
return user;
}
static async get(ctx) {
const id = ctx.params.layer;
logger.info(`[LayerRouter] Getting layer with id: ${id}`);
const includes = ctx.query.includes ? ctx.query.includes.split(',').map((elem) => elem.trim()) : [];
const { query } = ctx;
const user = ctx.query.loggedUser && ctx.query.loggedUser !== 'null' ? JSON.parse(ctx.query.loggedUser) : null;
delete query.loggedUser;
try {
const layer = await LayerService.get(id, ctx.request.headers['x-api-key'], includes, user);
ctx.body = LayerSerializer.serialize(layer);
const cache = [id, layer.slug];
if (includes) {
includes.forEach((inc) => {
cache.push(`${id}-${inc}`);
cache.push(`${layer.slug}-${inc}`);
});
}
ctx.set('cache', cache.join(' '));
} catch (err) {
if (err instanceof LayerNotFound) {
ctx.throw(404, err.message);
return;
}
throw err;
}
}
static async create(ctx) {
logger.info(`[LayerRouter] Creating layer with name: ${ctx.request.body.name}`);
try {
const { dataset } = ctx.params;
const user = LayerRouter.getUser(ctx);
const layer = await LayerService.create(ctx.request.body, dataset, user, ctx.request.headers['x-api-key']);
ctx.set('cache-control', 'flush');
ctx.body = LayerSerializer.serialize(layer);
ctx.set('uncache', ['layer', `${ctx.state.dataset.id}-layer`, `${ctx.state.dataset.attributes.slug}-layer`, `${ctx.state.dataset.id}-layer-all`]);
} catch (err) {
if (err instanceof LayerDuplicated) {
ctx.throw(400, err.message);
return;
}
throw err;
}
}
static async update(ctx) {
const id = ctx.params.layer;
logger.info(`[LayerRouter] Updating layer with id: ${id}`);
try {
const layer = await LayerService.update(id, ctx.request.body, ctx.request.headers['x-api-key']);
ctx.set('cache-control', 'flush');
ctx.body = LayerSerializer.serialize(layer);
ctx.set('uncache', ['layer', id, layer.slug, `${layer.dataset}-layer`, `${ctx.state.dataset.attributes.slug}-layer`, `${ctx.state.dataset.id}-layer-all`]);
} catch (err) {
if (err instanceof LayerNotFound) {
ctx.throw(404, err.message);
return;
}
if (err instanceof LayerDuplicated) {
ctx.throw(400, err.message);
return;
}
throw err;
}
}
static async delete(ctx) {
const id = ctx.params.layer;
logger.info(`[LayerRouter] Deleting layer with id: ${id}`);
try {
const layer = await LayerService.get(id, ctx.request.headers['x-api-key']);
await LayerService.delete(layer, ctx.request.headers['x-api-key']);
ctx.set('cache-control', 'flush');
ctx.body = LayerSerializer.serialize(layer);
ctx.set('uncache', ['layer', id, layer.slug, `${layer.dataset}-layer`, `${ctx.state.dataset.attributes.slug}-layer`, `${ctx.state.dataset.id}-layer-all`]);
} catch (err) {
if (err instanceof LayerNotFound) {
ctx.throw(404, err.message);
return;
}
if (err instanceof LayerProtected) {
ctx.throw(400, err.message);
return;
}
throw err;
}
}
static async deleteByDataset(ctx) {
const id = ctx.params.dataset;
logger.info(`[LayerRouter] Deleting layers of dataset with id: ${id}`);
try {
const layers = await LayerService.deleteByDataset(id, ctx.request.headers['x-api-key']);
ctx.set('cache-control', 'flush');
ctx.body = LayerSerializer.serialize(layers);
const uncache = ['layer', `${ctx.state.dataset.id}-layer`, `${ctx.state.dataset.attributes.slug}-layer`, `${ctx.state.dataset.id}-layer-all`];
if (layers) {
layers.forEach((layer) => {
uncache.push(layer._id);
uncache.push(layer.slug);
});
}
ctx.set('uncache', uncache.join(' '));
} catch (err) {
if (err instanceof LayerNotFound) {
ctx.throw(404, err.message);
return;
}
throw err;
}
}
static async deleteByUserId(ctx) {
const userIdToDelete = ctx.params.userId;
try {
await UserService.getUserById(userIdToDelete, ctx.request.headers['x-api-key']);
} catch (error) {
ctx.throw(404, `User ${userIdToDelete} does not exist`);
}
logger.info(`[LayerRouter] Deleting all layer for user with id: ${userIdToDelete}`);
try {
const layers = await LayerService.deleteByUserId(userIdToDelete, ctx.request.headers['x-api-key']);
ctx.body = {
deletedLayers: LayerSerializer.serialize(layers.deletedLayers, null, true).data
};
if (layers.protectedLayers) {
ctx.body.protectedLayers = LayerSerializer.serialize(layers.protectedLayers, null, true).data;
}
} catch (err) {
logger.error(`Error deleting layers from user ${userIdToDelete}`, err);
ctx.throw(500, `Error deleting layers from user ${userIdToDelete}`);
}
}
static async expireCache(ctx) {
const layerId = ctx.params.layer;
logger.info(`[LayerRouter - expireCache] Expiring cache for layer with id: ${layerId}`);
try {
const layer = await LayerService.get(layerId, ctx.request.headers['x-api-key']);
if (!['gee', 'loca', 'nexgddp'].includes(layer.provider)) {
ctx.throw(400, 'Layer provider does not support cache expiration');
}
const response = await RWAPIMicroservice.requestToMicroservice({
uri: `/v1/layer/${layer.provider}/${layerId}/expire-cache`,
method: 'DELETE',
headers: {
'x-api-key': ctx.request.headers['x-api-key']
}
});
ctx.body = response;
ctx.status = 200;
} catch (err) {
if (err instanceof LayerNotFound) {
ctx.throw(404, err.message);
return;
}
ctx.throw(err.statusCode, JSON.stringify(err.error));
}
}
static async getAll(ctx) {
logger.info(`[LayerRouter] Getting all layers`);
const { query } = ctx;
const dataset = ctx.params.dataset || null;
const user = ctx.query.loggedUser && ctx.query.loggedUser !== 'null' ? JSON.parse(ctx.query.loggedUser) : null;
const userId = user ? user.id : null;
const isAdmin = ['ADMIN', 'SUPERADMIN'].includes(user && user.role);
delete query.loggedUser;
if (ctx.query.sort && (ctx.query.sort.includes('user.role') || ctx.query.sort.includes('user.name'))) {
logger.debug('Detected sorting by user role or name');
if (!user || !isAdmin) {
ctx.throw(403, 'Sorting by user name or role not authorized.');
return;
}
// Reset all layers' sorting columns
await LayerModel.updateMany({}, { userRole: '', userName: '' });
// Fetch info to sort again
const ids = await LayerService.getAllLayersUserIds();
const users = await RelationshipsService.getUsersInfoByIds(ids, ctx.request.headers['x-api-key']);
await Promise.all(users.map((u) => LayerModel.updateMany(
{ userId: u._id },
{
userRole: u.role ? u.role.toLowerCase() : '',
userName: u.name ? u.name.toLowerCase() : ''
},
)));
}
/**
* We'll want to limit the maximum page size in the future
* However, as this will cause a production BC break, we can't enforce it just now
*/
// if (query['page[size]'] && query['page[size]'] > 100) {
// ctx.throw(400, 'Invalid page size');
// }
if (Object.keys(query).find((el) => el.indexOf('collection') >= 0)) {
if (!userId) {
ctx.throw(403, 'Collection filter not authorized');
return;
}
logger.debug('Obtaining collections', userId);
ctx.query.ids = await RelationshipsService.getCollections(ctx.query.collection, userId, ctx.request.headers['x-api-key']);
ctx.query.ids = ctx.query.ids.length > 0 ? ctx.query.ids.join(',') : '';
logger.debug('Ids from collections', ctx.query.ids);
}
if (Object.keys(query).find((el) => el.indexOf('user.role') >= 0) && isAdmin) {
logger.debug('Obtaining users with role');
ctx.query.usersRole = await RelationshipsService.getUsersWithRole(ctx.query['user.role'], ctx.request.headers['x-api-key']);
logger.debug('Ids from users with role', ctx.query.usersRole);
}
if (Object.keys(query).find((el) => el.indexOf('favourite') >= 0)) {
if (!userId) {
ctx.throw(403, 'Fav filter not authorized');
return;
}
const app = ctx.query.app || ctx.query.application || 'rw';
ctx.query.ids = await RelationshipsService.getFavorites(app, userId, ctx.request.headers['x-api-key']);
ctx.query.ids = ctx.query.ids.length > 0 ? ctx.query.ids.join(',') : '';
logger.debug('Ids from collections', ctx.query.ids);
}
// Links creation
const clonedQuery = { ...query };
delete clonedQuery['page[size]'];
delete clonedQuery['page[number]'];
delete clonedQuery.ids;
delete clonedQuery.usersRole;
const serializedQuery = serializeObjToQuery(clonedQuery) ? `?${serializeObjToQuery(clonedQuery)}&` : '?';
const apiVersion = ctx.mountPath.split('/')[ctx.mountPath.split('/').length - 1];
const link = `${ctx.request.protocol}://${getHostForPaginationLink(ctx)}/${apiVersion}${ctx.request.path}${serializedQuery}`;
const layers = await LayerService.getAll(query, dataset, user, ctx.request.headers['x-api-key']);
ctx.body = LayerSerializer.serialize(layers, link);
const includes = ctx.query.includes ? ctx.query.includes.split(',').map((elem) => elem.trim()) : [];
const cache = ['layer'];
if (ctx.params.dataset) {
cache.push(`${ctx.params.dataset}-layer-all`);
}
if (includes) {
includes.forEach((inc) => {
cache.push(`layer-${inc}`);
if (ctx.params.dataset) {
cache.push(`${ctx.params.dataset}-layer-all-${inc}`);
}
});
}
ctx.set('cache', cache.join(' '));
}
static async findByIds(ctx) {
const { body } = ctx.request;
if (body.layer) {
body.ids = body.layer.ids;
}
logger.info(`[LayerRouter] Getting layers for datasets with id: ${body.ids}`);
const resource = {
ids: body.ids,
app: body.app,
env: body.env
};
if (typeof resource.ids === 'string') {
resource.ids = resource.ids.split(',').map((elem) => elem.trim());
}
if (typeof resource.env === 'string') {
resource.env = resource.env.split(',').map((elem) => elem.trim());
}
const result = await LayerService.getByDataset(resource);
ctx.body = LayerSerializer.serialize(result, null, true);
}
static async updateEnvironment(ctx) {
logger.info('Updating environment of all layers with dataset ', ctx.params.dataset, ' to environment', ctx.params.env);
const layers = await LayerService.updateEnvironment(ctx.params.dataset, ctx.params.env);
const uncache = ['layer', `${ctx.params.dataset}-layer`, `${ctx.state.dataset.attributes.slug}-layer`, 'dataset-layer'];
if (layers) {
layers.forEach((layer) => {
uncache.push(layer._id);
uncache.push(layer.slug);
});
}
ctx.set('uncache', uncache.join(' '));
ctx.body = '';
}
}
const validationMiddleware = async (ctx, next) => {
logger.debug(`[LayerRouter] Validating`);
if (ctx.request.body.layer) {
ctx.request.body = Object.assign(ctx.request.body, ctx.request.body.layer);
delete ctx.request.body.layer;
}
try {
const newLayerCreation = ctx.request.method === 'POST';
if (newLayerCreation) {
await LayerValidator.validateCreation(ctx);
} else {
await LayerValidator.validateUpdate(ctx);
}
} catch (err) {
if (err instanceof LayerNotValid) {
ctx.throw(400, err.getMessages());
return;
}
throw err;
}
await next();
};
const datasetValidationMiddleware = async (ctx, next) => {
logger.debug(`[LayerRouter] Validating dataset presence`);
// supports the deprecated "layer" root object on the request
if (ctx.request.body && ctx.request.body.layer) {
ctx.request.body = Object.assign(ctx.request.body, ctx.request.body.layer);
delete ctx.request.body.layer;
}
// END REMOVE
try {
ctx.state.dataset = await DatasetService.checkDataset(ctx);
} catch (err) {
ctx.throw(404, 'Dataset not found');
}
await next();
};
const findByIdValidationMiddleware = async (ctx, next) => {
logger.debug(`[LayerRouter] Validating find by id`);
try {
await LayerValidator.validateFindById(ctx);
} catch (err) {
if (err instanceof LayerNotValid) {
ctx.throw(400, err.getMessages());
return;
}
throw err;
}
await next();
};
const isMicroservice = async (ctx, next) => {
logger.debug('Checking if is a microservice');
const user = LayerRouter.getUser(ctx);
if (!user || !user.id) {
ctx.throw(401, 'Not authorized');
return;
}
if (user.id !== 'microservice') {
ctx.throw(403, 'Forbidden');
return;
}
await next();
};
const isMicroserviceOrAdmin = async (ctx, next) => {
logger.debug('Checking if is a microservice or admin');
const user = LayerRouter.getUser(ctx);
if (!user || !user.id) {
ctx.throw(401, 'Not authorized');
return;
}
if (user.id !== 'microservice' && user.role !== 'ADMIN') {
ctx.throw(403, 'Forbidden');
return;
}
await next();
};
const authorizationMiddleware = async (ctx, next) => {
logger.debug(`[LayerRouter] Checking authorization`);
// Get user from query (delete) or body (post-patch)
const newLayerCreation = ctx.request.method === 'POST';
const user = LayerRouter.getUser(ctx);
if (user.id === 'microservice') {
await next();
return;
}
if (!user || USER_ROLES.indexOf(user.role) === -1) {
ctx.throw(401, 'Unauthorized'); // if not logged or invalid ROLE -> out
return;
}
if (user.role === 'USER') {
if (!newLayerCreation) {
ctx.throw(403, 'Forbidden'); // if user is USER -> out
return;
}
}
const application = ctx.request.query.application ? ctx.request.query.application : ctx.request.body.application;
if (application) {
const appPermission = application.find((app) => user.extraUserData.apps.find((userApp) => userApp === app));
if (!appPermission) {
ctx.throw(403, 'Forbidden'); // if manager or admin but no application -> out
return;
}
}
const allowedOperations = newLayerCreation;
if ((user.role === 'MANAGER' || user.role === 'ADMIN') && !allowedOperations) {
const permission = await LayerService.hasPermission(ctx.params.layer, user, ctx.request.headers['x-api-key']);
if (!permission) {
ctx.throw(403, 'Forbidden');
return;
}
}
await next(); // SUPERADMIN is included here
};
const isAuthenticatedMiddleware = async (ctx, next) => {
logger.debug(`Verifying if user is authenticated`);
const { query, body } = ctx.request;
const user = { ...(query.loggedUser ? JSON.parse(query.loggedUser) : {}), ...body.loggedUser };
if (!user || !user.id) {
ctx.throw(401, 'Unauthorized');
return;
}
await next();
};
const deleteResourceAuthorizationMiddleware = async (ctx, next) => {
logger.debug(`[LayerRouter] Checking authorization`);
const user = LayerRouter.getUser(ctx);
const userFromParam = ctx.params.userId;
if (user.id === 'microservice' || user.role === 'ADMIN') {
await next();
return;
}
if (userFromParam === user.id) {
await next();
return;
}
ctx.throw(403, 'Forbidden');
};
router.get('/layer', LayerRouter.getAll);
router.get('/layer/:layer', LayerRouter.get);
router.get('/dataset/:dataset/layer', datasetValidationMiddleware, LayerRouter.getAll);
router.post('/dataset/:dataset/layer', isAuthenticatedMiddleware, datasetValidationMiddleware, validationMiddleware, authorizationMiddleware, LayerRouter.create);
router.get('/dataset/:dataset/layer/:layer', datasetValidationMiddleware, LayerRouter.get);
router.patch('/dataset/:dataset/layer/:layer', isAuthenticatedMiddleware, datasetValidationMiddleware, validationMiddleware, authorizationMiddleware, LayerRouter.update);
router.delete('/dataset/:dataset/layer/:layer', isAuthenticatedMiddleware, datasetValidationMiddleware, authorizationMiddleware, LayerRouter.delete);
router.delete('/dataset/:dataset/layer', isAuthenticatedMiddleware, datasetValidationMiddleware, isMicroservice, LayerRouter.deleteByDataset);
router.delete('/layer/by-user/:userId', isAuthenticatedMiddleware, deleteResourceAuthorizationMiddleware, LayerRouter.deleteByUserId);
router.delete('/layer/:layer/expire-cache', isAuthenticatedMiddleware, isMicroserviceOrAdmin, LayerRouter.expireCache);
router.post('/layer/find-by-ids', isAuthenticatedMiddleware, findByIdValidationMiddleware, LayerRouter.findByIds);
router.patch('/layer/change-environment/:dataset/:env', isAuthenticatedMiddleware, datasetValidationMiddleware, isMicroservice, LayerRouter.updateEnvironment);
module.exports = router;