diff --git a/api-service/src/controllers/Alerts/Alerts.ts b/api-service/src/controllers/Alerts/Alerts.ts index 278ac586..eaaef01d 100644 --- a/api-service/src/controllers/Alerts/Alerts.ts +++ b/api-service/src/controllers/Alerts/Alerts.ts @@ -13,6 +13,8 @@ const telemetryObject = { type: "alert", ver: "1.0.0" }; const createAlertHandler = async (req: Request, res: Response, next: NextFunction) => { try { const alertPayload = getAlertPayload(req.body); + const userID = (req as any)?.userID; + _.set(alertPayload, "created_by", userID); const response = await Alert.create(alertPayload); updateTelemetryAuditEvent({ request: req, object: { id: response?.dataValues?.id, ...telemetryObject } }); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { id: response.dataValues.id } }); @@ -30,6 +32,8 @@ const publishAlertHandler = async (req: Request, res: Response, next: NextFuncti const { alertId } = req.params; const rulePayload: Record | null = await getAlertRule(alertId); if (!rulePayload) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); + const userID = (req as any)?.userID; + _.set(rulePayload, "updated_by", userID); if (rulePayload.status == "live") { await deleteAlertRule(rulePayload, false); } @@ -87,6 +91,8 @@ const deleteAlertHandler = async (req: Request, res: Response, next: NextFunctio return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); } const rulePayload = ruleModel.toJSON(); + const userID = (req as any)?.userID || "SYSTEM"; + _.set(rulePayload, "updated_by", userID); await deleteAlertRule(rulePayload, hardDelete === "true"); updateTelemetryAuditEvent({ request: req, currentRecord: rulePayload, object: { id: alertId, ...telemetryObject } }); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { id: alertId } }); @@ -103,12 +109,14 @@ const updateAlertHandler = async (req: Request, res: Response, next: NextFunctio const ruleModel = await getAlertRule(alertId); if (!ruleModel) { return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }) } const rulePayload = ruleModel.toJSON(); + const userID = (req as any)?.userID; if (rulePayload.status == "live") { + _.set(rulePayload, "updated_by", userID); await deleteAlertRule(rulePayload, false); await retireAlertSilence(alertId); } const updatedPayload = getAlertPayload({ ...req.body, manager: rulePayload?.manager }); - await Alert.update({ ...updatedPayload, status: "draft" }, { where: { id: alertId } }); + await Alert.update({ ...updatedPayload, status: "draft", updated_by: userID }, { where: { id: alertId } }); updateTelemetryAuditEvent({ request: req, currentRecord: rulePayload, object: { id: alertId, ...telemetryObject } }); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { id: alertId } }); } catch (error: any) { diff --git a/api-service/src/controllers/Alerts/Silence.ts b/api-service/src/controllers/Alerts/Silence.ts index 32e2c531..15a7dfda 100644 --- a/api-service/src/controllers/Alerts/Silence.ts +++ b/api-service/src/controllers/Alerts/Silence.ts @@ -20,12 +20,14 @@ const createHandler = async (request: Request, response: Response, next: NextFun const start_date = new Date(startDate); const end_date = new Date(endDate); + const userID = (request as any)?.userID; const silenceBody = { id: grafanaResponse.silenceId, manager: grafanaResponse.manager, alert_id: alertId, start_time: start_date, end_time: end_date, + created_by : userID, } const sileneResponse = await Silence.create(silenceBody); updateTelemetryAuditEvent({ request, object: { id: sileneResponse?.dataValues?.id, ...telemetryObject } }); @@ -78,10 +80,12 @@ const updateHandler = async (request: Request, response: Response, next: NextFun await updateSilence(silenceObject, payload); const updatedStartTime = new Date(payload.startTime); const updatedEndTime = new Date(payload.endTime); + const userID = (request as any)?.userID; const updatedSilence = { ...silenceObject, start_time: updatedStartTime, - end_time: updatedEndTime + end_time: updatedEndTime, + updated_by: userID, } const silenceResponse = await Silence.update(updatedSilence, { where: { id } }) ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { silenceResponse } }) diff --git a/api-service/src/controllers/CreateQueryTemplate/CreateTemplateController.ts b/api-service/src/controllers/CreateQueryTemplate/CreateTemplateController.ts index 90808e7d..68e36a22 100644 --- a/api-service/src/controllers/CreateQueryTemplate/CreateTemplateController.ts +++ b/api-service/src/controllers/CreateQueryTemplate/CreateTemplateController.ts @@ -42,6 +42,8 @@ export const createQueryTemplate = async (req: Request, res: Response) => { } const data = transformRequest(requestBody, templateName); + const userID = (req as any)?.userID; + _.set(data, "created_by", userID); await QueryTemplate.create(data) logger.info({ apiId, msgid, resmsgid, requestBody: req?.body, message: `Query template created successfully` }) return ResponseHandler.successResponse(req, res, { status: 200, data: { template_id: templateId, template_name: templateName, message: `The query template has been saved successfully` } }); diff --git a/api-service/src/controllers/DatasetCopy/DatasetCopy.ts b/api-service/src/controllers/DatasetCopy/DatasetCopy.ts index 9308dd71..28707bd2 100644 --- a/api-service/src/controllers/DatasetCopy/DatasetCopy.ts +++ b/api-service/src/controllers/DatasetCopy/DatasetCopy.ts @@ -40,6 +40,8 @@ const datasetCopy = async (req: Request, res: Response) => { validateRequest(req); const newDatasetId = _.get(req, "body.request.destination.datasetId"); const dataset = await fetchDataset(req); + const userID = (req as any)?.userID; + _.set(dataset, "created_by", userID); updateRecords(dataset, newDatasetId) const response = await datasetService.createDraftDataset(dataset).catch(err => { if (err?.name === "SequelizeUniqueConstraintError") { diff --git a/api-service/src/controllers/DatasetCreate/DatasetCreate.ts b/api-service/src/controllers/DatasetCreate/DatasetCreate.ts index 2867457b..62b2e4b7 100644 --- a/api-service/src/controllers/DatasetCreate/DatasetCreate.ts +++ b/api-service/src/controllers/DatasetCreate/DatasetCreate.ts @@ -34,6 +34,8 @@ const datasetCreate = async (req: Request, res: Response) => { await validateRequest(req) const draftDataset = getDraftDataset(req.body.request) + const userID = (req as any)?.userID; + _.set(draftDataset, "created_by", userID); const dataset = await datasetService.createDraftDataset(draftDataset); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: dataset }); } diff --git a/api-service/src/controllers/DatasetImport/DatasetImport.ts b/api-service/src/controllers/DatasetImport/DatasetImport.ts index e390d8bd..6ef907c2 100644 --- a/api-service/src/controllers/DatasetImport/DatasetImport.ts +++ b/api-service/src/controllers/DatasetImport/DatasetImport.ts @@ -15,18 +15,21 @@ const datasetImport = async (req: Request, res: Response) => { const migratedConfigs = migrateExportedDatasetV1(requestBody) datasetPayload = migratedConfigs; } + const userID = (req as any)?.userID; + _.set(datasetPayload, "created_by", userID); const { updatedDataset, ignoredFields } = await datasetImportValidation({ ...requestBody, "request": datasetPayload }) const { successMsg, partialIgnored } = getResponseData(ignoredFields) - const dataset = await importDataset(updatedDataset, overwrite); + const dataset = await importDataset(updatedDataset, overwrite, userID); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: { message: successMsg, data: dataset, ...(!_.isEmpty(partialIgnored) && { ignoredFields: partialIgnored }) } }); } -const importDataset = async (dataset: Record, overwrite: string | any) => { +const importDataset = async (dataset: Record, overwrite: string | any, userID : string) => { const dataset_id = _.get(dataset,"dataset_id") const response = await datasetService.createDraftDataset(dataset).catch(err => { return err }) if (response?.name === "SequelizeUniqueConstraintError") { if (overwrite === "true") { + _.set(dataset, "updated_by", userID); const overwriteRes = await datasetService.updateDraftDataset(dataset).catch(()=>{ throw obsrvError(dataset_id, "DATASET_IMPORT_FAILURE", `Failed to import dataset: ${dataset_id} as overwrite failed`, "INTERNAL_SERVER_ERROR", 500); }) diff --git a/api-service/src/controllers/DatasetRead/DatasetRead.ts b/api-service/src/controllers/DatasetRead/DatasetRead.ts index e1853e57..d428b02a 100644 --- a/api-service/src/controllers/DatasetRead/DatasetRead.ts +++ b/api-service/src/controllers/DatasetRead/DatasetRead.ts @@ -30,8 +30,9 @@ const datasetRead = async (req: Request, res: Response) => { validateRequest(req); const { dataset_id } = req.params; const { fields, mode } = req.query; + const userID = (req as any)?.userID; const attributes = !fields ? defaultFields : _.split(fields, ","); - const dataset = (mode == "edit") ? await readDraftDataset(dataset_id, attributes) : await readDataset(dataset_id, attributes) + const dataset = (mode == "edit") ? await readDraftDataset(dataset_id, attributes, userID) : await readDataset(dataset_id, attributes) if (!dataset) { throw obsrvError(dataset_id, "DATASET_NOT_FOUND", `Dataset with the given dataset_id:${dataset_id} not found`, "NOT_FOUND", 404); } @@ -41,19 +42,19 @@ const datasetRead = async (req: Request, res: Response) => { ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: dataset }); } -const readDraftDataset = async (datasetId: string, attributes: string[]): Promise => { +const readDraftDataset = async (datasetId: string, attributes: string[], userID: string): Promise => { const attrs = _.union(attributes, ["dataset_config", "api_version", "type", "id"]) const draftDataset = await datasetService.getDraftDataset(datasetId, attrs); if (draftDataset) { // Contains a draft const apiVersion = _.get(draftDataset, ["api_version"]); - const dataset: any = (apiVersion === "v2") ? draftDataset : await datasetService.migrateDraftDataset(datasetId, draftDataset) + const dataset: any = (apiVersion === "v2") ? draftDataset : await datasetService.migrateDraftDataset(datasetId, draftDataset, userID) return _.pick(dataset, attributes); } const liveDataset = await datasetService.getDataset(datasetId, undefined, true); if (liveDataset) { - const dataset = await datasetService.createDraftDatasetFromLive(liveDataset) + const dataset = await datasetService.createDraftDatasetFromLive(liveDataset, userID) return _.pick(dataset, attributes); } diff --git a/api-service/src/controllers/DatasetStatusTransition/DatasetStatusTransition.ts b/api-service/src/controllers/DatasetStatusTransition/DatasetStatusTransition.ts index c97b19c5..80b4b91d 100644 --- a/api-service/src/controllers/DatasetStatusTransition/DatasetStatusTransition.ts +++ b/api-service/src/controllers/DatasetStatusTransition/DatasetStatusTransition.ts @@ -53,6 +53,7 @@ const datasetStatusTransition = async (req: Request, res: Response) => { validateRequest(req, dataset_id); const dataset: Record = (_.includes(liveDatasetActions, status)) ? await datasetService.getDataset(dataset_id, ["id", "status", "type", "api_version"], true) : await datasetService.getDraftDataset(dataset_id, ["id", "dataset_id", "status", "type", "api_version"]) + const userID = (req as any)?.userID; validateDataset(dataset, dataset_id, status); switch (status) { @@ -60,13 +61,13 @@ const datasetStatusTransition = async (req: Request, res: Response) => { await deleteDataset(dataset); break; case "ReadyToPublish": - await readyForPublish(dataset); + await readyForPublish(dataset, userID); break; case "Live": - await publishDataset(dataset); + await publishDataset(dataset, userID); break; case "Retire": - await retireDataset(dataset); + await retireDataset(dataset, userID); break; default: throw obsrvError(dataset.id, "UNKNOWN_STATUS_TRANSITION", "Unknown status transition requested", "BAD_REQUEST", 400) @@ -84,7 +85,7 @@ const deleteDataset = async (dataset: Record) => { } -const readyForPublish = async (dataset: Record) => { +const readyForPublish = async (dataset: Record, updated_by: any) => { const draftDataset: any = await datasetService.getDraftDataset(dataset.dataset_id) let defaultConfigs: any = _.cloneDeep(defaultDatasetConfig) @@ -93,7 +94,14 @@ const readyForPublish = async (dataset: Record) => { if (draftDataset?.type === "master") { defaultConfigs = _.omit(defaultConfigs, "dataset_config.keys_config.data_key"); } - _.mergeWith(draftDataset, defaultConfigs, draftDataset, (objValue, srcValue) => { + _.set(draftDataset, "updated_by", updated_by); + _.mergeWith(draftDataset, defaultConfigs, draftDataset, (objValue, srcValue ,key) => { + if (key === "created_by"|| key === "updated_by") { + if (objValue !== "SYSTEM") { + return objValue; + } + return srcValue; + } if (_.isBoolean(objValue) && _.isBoolean(srcValue)) { return objValue; } @@ -120,10 +128,12 @@ const readyForPublish = async (dataset: Record) => { * * @param dataset */ -const publishDataset = async (dataset: Record) => { +const publishDataset = async (dataset: Record, userID: any) => { const draftDataset: Record = await datasetService.getDraftDataset(dataset.dataset_id) as unknown as Record - + _.set(draftDataset, ["created_by"], userID); + _.set(draftDataset, ["updated_by"], userID); + console.log(draftDataset); await validateAndUpdateDenormConfig(draftDataset); await updateMasterDataConfig(draftDataset) await datasetService.publishDataset(draftDataset) @@ -202,10 +212,10 @@ const updateMasterDataConfig = async (draftDataset: Record) => { } } -const retireDataset = async (dataset: Record) => { +const retireDataset = async (dataset: Record, updated_by: any) => { await canRetireIfMasterDataset(dataset); - await datasetService.retireDataset(dataset); + await datasetService.retireDataset(dataset, updated_by); await restartPipeline(dataset); } diff --git a/api-service/src/controllers/DatasetUpdate/DatasetUpdate.ts b/api-service/src/controllers/DatasetUpdate/DatasetUpdate.ts index 274ad3c4..7277b1b8 100644 --- a/api-service/src/controllers/DatasetUpdate/DatasetUpdate.ts +++ b/api-service/src/controllers/DatasetUpdate/DatasetUpdate.ts @@ -59,6 +59,8 @@ const datasetUpdate = async (req: Request, res: Response) => { validateDataset(datasetModel, req) const draftDataset = mergeDraftDataset(datasetModel, datasetReq); + const userID = (req as any)?.userID; + _.set(draftDataset, "updated_by", userID ) const response = await datasetService.updateDraftDataset(draftDataset); ResponseHandler.successResponse(req, res, { status: httpStatus.OK, data: response }); } diff --git a/api-service/src/controllers/NotificationChannel/Notification.ts b/api-service/src/controllers/NotificationChannel/Notification.ts index f6db4fd4..deeee234 100644 --- a/api-service/src/controllers/NotificationChannel/Notification.ts +++ b/api-service/src/controllers/NotificationChannel/Notification.ts @@ -12,6 +12,8 @@ const telemetryObject = { type: "notificationChannel", ver: "1.0.0" }; const createHandler = async (request: Request, response: Response, next: NextFunction) => { try { const payload = request.body; + const userID = (request as any)?.userID; + _.set(payload, "created_by", userID); const notificationBody = await Notification.create(payload); updateTelemetryAuditEvent({ request, object: { id: notificationBody?.dataValues?.id, ...telemetryObject } }); ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { id: notificationBody.dataValues.id } }) @@ -32,6 +34,8 @@ const updateHandler = async (request: Request, response: Response, next: NextFun if (_.get(notificationPayload, "status") === "live") { await updateNotificationChannel(notificationPayload); } + const userID = (request as any)?.userID; + _.set(updatedPayload, "updated_by", userID); await Notification.update({ ...updatedPayload, status: "draft" }, { where: { id } }); ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { id } }); } catch (err) { @@ -74,7 +78,8 @@ const retireHandler = async (request: Request, response: Response, next: NextFun if (!notificationPayload) return next({ message: httpStatus[httpStatus.NOT_FOUND], statusCode: httpStatus.NOT_FOUND }); updateTelemetryAuditEvent({ request, object: { id, ...telemetryObject }, currentRecord: notificationPayload }); await updateNotificationChannel(notificationPayload); - await Notification.update({ status: "retired" }, { where: { id } }) + const userID = (request as any)?.userID; + await Notification.update({ status: "retired", updated_by: userID }, { where: { id } }) ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { id } }); } catch (err) { const error = createError(httpStatus.INTERNAL_SERVER_ERROR, _.get(err, "message") || httpStatus[httpStatus.INTERNAL_SERVER_ERROR]) @@ -91,7 +96,8 @@ const publishHandler = async (request: Request, response: Response, next: NextFu if (notificationPayload.status === "live") throw new Error(httpStatus[httpStatus.CONFLICT]); updateTelemetryAuditEvent({ request, object: { id, ...telemetryObject }, currentRecord: notificationPayload }); await publishNotificationChannel(notificationPayload); - Notification.update({ status: "live" }, { where: { id } }); + const userID = (request as any)?.userID; + Notification.update({ status: "live", updated_by: userID }, { where: { id } }); ResponseHandler.successResponse(request, response, { status: httpStatus.OK, data: { id, status: "published" } }); } catch (err) { const error = createError(httpStatus.INTERNAL_SERVER_ERROR, _.get(err, "message") || httpStatus[httpStatus.INTERNAL_SERVER_ERROR]) diff --git a/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts b/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts index 114d370e..350b25f7 100644 --- a/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts +++ b/api-service/src/controllers/UpdateQueryTemplate/UpdateTemplateController.ts @@ -38,7 +38,8 @@ export const updateQueryTemplate = async (req: Request, res: Response) => { logger.error({ apiId, msgid, resmsgid, templateId, requestBody: req?.body, message: `Invalid template provided, A template should consist of variables ${requiredVariables} and type of json,sql`, code: "QUERY_TEMPLATE_INVALID_INPUT" }) return ResponseHandler.errorResponse({ statusCode: 400, message: `Invalid template provided, A template should consist of variables ${requiredVariables} and type of json,sql`, errCode: "BAD_REQUEST", code: "QUERY_TEMPLATE_INVALID_INPUT" }, req, res) } - + const userID = (req as any)?.userID; + requestBody.request.updated_by = userID; await QueryTemplate.update(requestBody?.request, { where: { template_id: templateId } }) logger.info({ apiId, msgid, resmsgid, templateId, requestBody, message: `Query template updated successfully` }) ResponseHandler.successResponse(req, res, { status: 200, data: { message: "Query template updated successfully", templateId } }); diff --git a/api-service/src/middlewares/RBAC_middleware.ts b/api-service/src/middlewares/RBAC_middleware.ts index d36babf0..8b073fbe 100644 --- a/api-service/src/middlewares/RBAC_middleware.ts +++ b/api-service/src/middlewares/RBAC_middleware.ts @@ -3,121 +3,17 @@ import jwt from "jsonwebtoken"; import { ResponseHandler } from "../helpers/ResponseHandler"; import { config } from "../configs/Config"; import _ from "lodash"; - -enum roles { - Admin = "admin", - DatasetManager = "dataset_manager", - Viewer = "viewer", - DatasetCreator = "dataset_creator", - Ingestor = "ingestor", -} - -enum permissions { - DatasetCreate = "api.datasets.create", - DatasetUpdate = "api.datasets.update", - DatasetRead = "api.datasets.read", - DatasetList = "api.datasets.list", - DataIngest = "api.data.in", - DataOut = "api.data.out", - DataExhaust = "api.data.exhaust", - QueryTemplateCreate = "api.query.template.create", - QueryTemplateRead = "api.query.template.read", - QueryTemplateDelete = "api.query.template.delete", - QueryTemplateList = "api.query.template.list", - QueryTemplateUpdate = "api.query.template.update", - QueryTemplate = "api.query.template.query", - SchemaValidator = "api.schema.validator", - GenerateSignedUrl = "api.files.generate-url", - DatasetStatusTransition = "api.datasets.status-transition", - DatasetHealth = "api.dataset.health", - DatasetReset = "api.dataset.reset", - DatasetSchemaGenerator = "api.datasets.dataschema", - DatasetExport = "api.datasets.export", - DatasetCopy = "api.datasets.copy", - ConnectorList = "api.connectors.list", - ConnectorRead = "api.connectors.read", - DatasetImport = "api.datasets.import", - SQLQuery = "api.obsrv.data.sql-query", -} - +import userPermissions from "./userPermissions.json"; interface AccessControl { - [key: string]: string[]; + apiGroups : { + [key: string]: string[]; + }, + roles : { + [key: string]: string[]; + } } -const accessControl: AccessControl = { - [roles.Ingestor]: [permissions.DataIngest], - [roles.Viewer]: [ - permissions.DatasetList, - permissions.DatasetRead, - permissions.DatasetExport, - permissions.ConnectorRead, - permissions.SQLQuery, - permissions.DataOut, - permissions.DataExhaust, - ], - [roles.DatasetCreator]: [ - permissions.DatasetList, - permissions.DatasetRead, - permissions.DatasetExport, - permissions.ConnectorRead, - permissions.SQLQuery, - permissions.DataOut, - permissions.DataExhaust, - permissions.DatasetImport, - permissions.DatasetCreate, - permissions.DatasetUpdate, - permissions.DatasetCopy, - permissions.QueryTemplateCreate, - permissions.QueryTemplateRead, - permissions.QueryTemplateDelete, - permissions.GenerateSignedUrl, - permissions.SchemaValidator, - permissions.DatasetSchemaGenerator, - ], - [roles.DatasetManager]: [ - permissions.DatasetList, - permissions.DatasetRead, - permissions.DatasetExport, - permissions.ConnectorRead, - permissions.SQLQuery, - permissions.DataOut, - permissions.DataExhaust, - permissions.DatasetImport, - permissions.DatasetCreate, - permissions.DatasetUpdate, - permissions.DatasetCopy, - permissions.QueryTemplateCreate, - permissions.QueryTemplateRead, - permissions.QueryTemplateDelete, - permissions.GenerateSignedUrl, - permissions.SchemaValidator, - permissions.DatasetSchemaGenerator, - permissions.DatasetReset, - permissions.DatasetStatusTransition, - ], - [roles.Admin]: [ - permissions.DatasetCreate, - permissions.DatasetList, - permissions.DatasetRead, - permissions.DatasetExport, - permissions.ConnectorRead, - permissions.SQLQuery, - permissions.DataOut, - permissions.DataExhaust, - permissions.DatasetImport, - permissions.DatasetCreate, - permissions.DatasetUpdate, - permissions.DatasetCopy, - permissions.QueryTemplateCreate, - permissions.QueryTemplateRead, - permissions.QueryTemplateDelete, - permissions.GenerateSignedUrl, - permissions.SchemaValidator, - permissions.DatasetSchemaGenerator, - permissions.DatasetReset, - permissions.DatasetStatusTransition, - ], -}; +const accessControl: AccessControl = userPermissions; export default { name: "rbac:middleware", @@ -131,8 +27,8 @@ export default { if (!token) { return ResponseHandler.errorResponse( { - statusCode: 403, - errCode: "FORBIDDEN", + statusCode: 401, + errCode: "Unauthorized access", message: "No token provided", }, req, @@ -152,16 +48,33 @@ export default { ); } if (decoded && _.isObject(decoded)) { - const action = (req as any).id; - const hasAccess = decoded?.roles?.some( - (role: string) => - accessControl[role] && accessControl[role].includes(action) - ); - if (!hasAccess) { + if (!decoded?.id) { return ResponseHandler.errorResponse( { statusCode: 401, errCode: "Unauthorized access", + message: "User ID is missing from the decoded token.", + }, + req, + res + ); + } + (req as any).userID = decoded?.id; + const action = (req as any).id; + const hasAccess = decoded?.roles?.some((role: string) => { + const apiGroups = accessControl.roles[role]; + + if (!apiGroups) return false; + + return apiGroups.some((apiGroup: string) => + accessControl.apiGroups[apiGroup]?.includes(action) + ); + }); + if (!hasAccess) { + return ResponseHandler.errorResponse( + { + statusCode: 403, + errCode: "FORBIDDEN", message: "Access denied for the user", }, req, diff --git a/api-service/src/middlewares/userPermissions.json b/api-service/src/middlewares/userPermissions.json new file mode 100644 index 00000000..400e218f --- /dev/null +++ b/api-service/src/middlewares/userPermissions.json @@ -0,0 +1,129 @@ +{ + "apiGroups": { + "general_access": [ + "api.datasets.list", + "api.datasets.read", + "api.datasets.export", + "api.data.out", + "api.data.exhaust", + "api.alert.list", + "api.alert.getAlertDetails", + "api.metric.list", + "api.alert.silence.list", + "api.alert.silence.get", + "api.alert.notification.list", + "api.alert.notification.get" + ], + "restricted_dataset_api": [ + "api.datasets.reset", + "api.datasets.status-transition" + ], + "alert": [ + "api.alert.create", + "api.alert.publish", + "api.alert.update", + "api.alert.delete" + ], + "metric": [ + "api.metric.add", + "api.metric.update", + "api.metric.remove" + ], + "silence": [ + "api.alert.silence.create", + "api.alert.silence.edit", + "api.alert.silence.delete" + ], + "notificationChannel": [ + "api.alert.notification.create", + "api.alert.notification.publish", + "api.alert.notification.test", + "api.alert.notification.update", + "api.alert.notification.retire" + ], + "dataset": [ + "api.datasets.create", + "api.datasets.update", + "api.datasets.import", + "api.datasets.copy", + "api.dataset.health", + "api.datasets.dataschema" + ], + "data": [ + "api.data.in" + ], + "queryTemplate": [ + "api.query.template.create", + "api.query.template.read", + "api.query.template.delete", + "api.query.template.update", + "api.query.template.query", + "api.query.template.list" + ], + "schema": [ + "api.schema.validator" + ], + "file": [ + "api.files.generate-url" + ], + "connector": [ + "api.connectors.list", + "api.connectors.read" + ], + "sqlQuery": [ + "api.obsrv.data.sql-query" + ] + }, + "roles": { + "ingestor": [ + "data" + ], + "viewer": [ + "general_access", + "connector", + "sqlQuery" + ], + "dataset_creator": [ + "general_access", + "connector", + "sqlQuery", + "dataset", + "queryTemplate", + "schema", + "file", + "connector", + "sqlQuery" + ], + "dataset_manager": [ + "general_access", + "connector", + "sqlQuery", + "dataset", + "queryTemplate", + "schema", + "file", + "connector", + "sqlQuery", + "restricted_dataset_api" + ], + "admin": [ + "general_access", + "connector", + "sqlQuery", + "dataset", + "queryTemplate", + "schema", + "file", + "connector", + "sqlQuery", + "restricted_dataset_api" + ], + "operations_admin": [ + "alert", + "metric", + "silence", + "notificationChannel", + "general_access" + ] + } +} \ No newline at end of file diff --git a/api-service/src/routes/AlertsRouter.ts b/api-service/src/routes/AlertsRouter.ts index 01c843dd..6044e9fe 100644 --- a/api-service/src/routes/AlertsRouter.ts +++ b/api-service/src/routes/AlertsRouter.ts @@ -4,38 +4,39 @@ import { setDataToRequestObject } from "../middlewares/setDataToRequestObject"; import customAlertHandler from "../controllers/Alerts/Alerts"; import metricAliasHandler from "../controllers/Alerts/Metric"; import silenceHandler from "../controllers/Alerts/Silence"; +import checkRBAC from "../middlewares/RBAC_middleware"; export const alertsRouter = express.Router(); // Notifications -alertsRouter.post("/notifications/search", setDataToRequestObject("api.alert.notification.list"), notificationHandler.listHandler); -alertsRouter.post("/notifications/create", setDataToRequestObject("api.alert.notification.create"), notificationHandler.createHandler); -alertsRouter.get("/notifications/publish/:id", setDataToRequestObject("api.alert.notification.publish"), notificationHandler.publishHandler); -alertsRouter.post("/notifications/test", setDataToRequestObject("api.alert.notification.test"), notificationHandler.testNotifationChannelHandler); -alertsRouter.patch("/notifications/update/:id", setDataToRequestObject("api.alert.notification.update"), notificationHandler.updateHandler); -alertsRouter.delete("/notifications/delete/:id", setDataToRequestObject("api.alert.notification.retire"), notificationHandler.retireHandler); -alertsRouter.get("/notifications/get/:id", setDataToRequestObject("api.alert.notification.get"), notificationHandler.fetchHandler); +alertsRouter.post("/notifications/search", setDataToRequestObject("api.alert.notification.list"), checkRBAC.handler(), notificationHandler.listHandler); +alertsRouter.post("/notifications/create", setDataToRequestObject("api.alert.notification.create"), checkRBAC.handler(), notificationHandler.createHandler); +alertsRouter.get("/notifications/publish/:id", setDataToRequestObject("api.alert.notification.publish"), checkRBAC.handler(), notificationHandler.publishHandler); +alertsRouter.post("/notifications/test", setDataToRequestObject("api.alert.notification.test"), checkRBAC.handler(), notificationHandler.testNotifationChannelHandler); +alertsRouter.patch("/notifications/update/:id", setDataToRequestObject("api.alert.notification.update"), checkRBAC.handler(), notificationHandler.updateHandler); +alertsRouter.delete("/notifications/delete/:id", setDataToRequestObject("api.alert.notification.retire"), checkRBAC.handler(), notificationHandler.retireHandler); +alertsRouter.get("/notifications/get/:id", setDataToRequestObject("api.alert.notification.get"), checkRBAC.handler(), notificationHandler.fetchHandler); // alerts -alertsRouter.post("/create", setDataToRequestObject("api.alert.create"), customAlertHandler.createAlertHandler); -alertsRouter.get("/publish/:alertId", setDataToRequestObject("api.alert.publish"), customAlertHandler.publishAlertHandler); -alertsRouter.post(`/search`, setDataToRequestObject("api.alert.list"), customAlertHandler.searchAlertHandler); -alertsRouter.get("/get/:alertId", setDataToRequestObject("api.alert.getAlertDetails"), customAlertHandler.alertDetailsHandler); -alertsRouter.delete("/delete/:alertId", setDataToRequestObject("api.alert.delete"), customAlertHandler.deleteAlertHandler); -alertsRouter.delete("/delete", setDataToRequestObject("api.alert.delete"), customAlertHandler.deleteSystemAlertsHandler); -alertsRouter.patch("/update/:alertId", setDataToRequestObject("api.alert.update"), customAlertHandler.updateAlertHandler); +alertsRouter.post("/create", setDataToRequestObject("api.alert.create"), checkRBAC.handler(), customAlertHandler.createAlertHandler); +alertsRouter.get("/publish/:alertId", setDataToRequestObject("api.alert.publish"), checkRBAC.handler(), customAlertHandler.publishAlertHandler); +alertsRouter.post(`/search`, setDataToRequestObject("api.alert.list"), checkRBAC.handler(), customAlertHandler.searchAlertHandler); +alertsRouter.get("/get/:alertId", setDataToRequestObject("api.alert.getAlertDetails"), checkRBAC.handler(), customAlertHandler.alertDetailsHandler); +alertsRouter.delete("/delete/:alertId", setDataToRequestObject("api.alert.delete"), checkRBAC.handler(), customAlertHandler.deleteAlertHandler); +alertsRouter.delete("/delete", setDataToRequestObject("api.alert.delete"), checkRBAC.handler(), customAlertHandler.deleteSystemAlertsHandler); +alertsRouter.patch("/update/:alertId", setDataToRequestObject("api.alert.update"), checkRBAC.handler(), customAlertHandler.updateAlertHandler); // metrics -alertsRouter.post("/metric/alias/create",setDataToRequestObject("api.metric.add"), metricAliasHandler.createMetricHandler); -alertsRouter.post("/metric/alias/search", setDataToRequestObject("api.metric.list"), metricAliasHandler.listMetricsHandler); -alertsRouter.patch("/metric/alias/update/:id", setDataToRequestObject("api.metric.update"),metricAliasHandler.updateMetricHandler); -alertsRouter.delete("/metric/alias/delete/:id", setDataToRequestObject("api.metric.remove"),metricAliasHandler.deleteMetricHandler); -alertsRouter.delete("/metric/alias/delete", setDataToRequestObject("api.metric.remove"), metricAliasHandler.deleteMultipleMetricHandler); +alertsRouter.post("/metric/alias/create",setDataToRequestObject("api.metric.add"), checkRBAC.handler(), metricAliasHandler.createMetricHandler); +alertsRouter.post("/metric/alias/search", setDataToRequestObject("api.metric.list"), checkRBAC.handler(), metricAliasHandler.listMetricsHandler); +alertsRouter.patch("/metric/alias/update/:id", setDataToRequestObject("api.metric.update"), checkRBAC.handler(), metricAliasHandler.updateMetricHandler); +alertsRouter.delete("/metric/alias/delete/:id", setDataToRequestObject("api.metric.remove"), checkRBAC.handler(), metricAliasHandler.deleteMetricHandler); +alertsRouter.delete("/metric/alias/delete", setDataToRequestObject("api.metric.remove"), checkRBAC.handler(), metricAliasHandler.deleteMultipleMetricHandler); // silence -alertsRouter.post("/silence/create",setDataToRequestObject("api.alert.silence.create"),silenceHandler.createHandler); -alertsRouter.get("/silence/search",setDataToRequestObject("api.alert.silence.list"),silenceHandler.listHandler); -alertsRouter.get("/silence/get/:id",setDataToRequestObject("api.alert.silence.get"),silenceHandler.fetchHandler); -alertsRouter.patch("/silence/update/:id",setDataToRequestObject("api.alert.silence.edit"),silenceHandler.updateHandler); -alertsRouter.delete("/silence/delete/:id",setDataToRequestObject("api.alert.silence.delete"),silenceHandler.deleteHandler); \ No newline at end of file +alertsRouter.post("/silence/create",setDataToRequestObject("api.alert.silence.create"), checkRBAC.handler(), silenceHandler.createHandler); +alertsRouter.get("/silence/search",setDataToRequestObject("api.alert.silence.list"), checkRBAC.handler(), silenceHandler.listHandler); +alertsRouter.get("/silence/get/:id",setDataToRequestObject("api.alert.silence.get"), checkRBAC.handler(), silenceHandler.fetchHandler); +alertsRouter.patch("/silence/update/:id",setDataToRequestObject("api.alert.silence.edit"), checkRBAC.handler(), silenceHandler.updateHandler); +alertsRouter.delete("/silence/delete/:id",setDataToRequestObject("api.alert.silence.delete"), checkRBAC.handler(), silenceHandler.deleteHandler); \ No newline at end of file diff --git a/api-service/src/services/DatasetService.ts b/api-service/src/services/DatasetService.ts index 2e800212..e7bda7d9 100644 --- a/api-service/src/services/DatasetService.ts +++ b/api-service/src/services/DatasetService.ts @@ -92,9 +92,10 @@ class DatasetService { return responseData; } - migrateDraftDataset = async (datasetId: string, dataset: Record): Promise => { + migrateDraftDataset = async (datasetId: string, dataset: Record, userID: string): Promise => { const dataset_id = _.get(dataset, "id") const draftDataset = await this.migrateDatasetV1(dataset_id, dataset); + _.set(draftDataset, "updated_by", userID); const transaction = await sequelize.transaction(); try { await DatasetDraft.update(draftDataset, { where: { id: dataset_id }, transaction }); @@ -166,7 +167,7 @@ class DatasetService { } } - createDraftDatasetFromLive = async (dataset: Model) => { + createDraftDatasetFromLive = async (dataset: Model, userID: string) => { const draftDataset: any = _.omit(dataset, ["created_date", "updated_date", "published_date"]); const dataset_config: any = _.get(dataset, "dataset_config"); @@ -232,6 +233,7 @@ class DatasetService { draftDataset["version_key"] = Date.now().toString() draftDataset["version"] = _.add(_.get(dataset, ["version"]), 1); // increment the dataset version draftDataset["status"] = DatasetStatus.Draft + draftDataset["created_by"] = userID; const result = await DatasetDraft.create(draftDataset); return _.get(result, "dataValues") } @@ -256,14 +258,14 @@ class DatasetService { } } - retireDataset = async (dataset: Record) => { + retireDataset = async (dataset: Record, updatedBy: any) => { const transaction = await sequelize.transaction(); try { - await Dataset.update({ status: DatasetStatus.Retired }, { where: { id: dataset.id }, transaction }); - await DatasetSourceConfig.update({ status: DatasetStatus.Retired }, { where: { dataset_id: dataset.id }, transaction }); - await Datasource.update({ status: DatasetStatus.Retired }, { where: { dataset_id: dataset.id }, transaction }); - await DatasetTransformations.update({ status: DatasetStatus.Retired }, { where: { dataset_id: dataset.id }, transaction }); + await Dataset.update({ status: DatasetStatus.Retired, updated_by: updatedBy }, { where: { id: dataset.id }, transaction }); + await DatasetSourceConfig.update({ status: DatasetStatus.Retired, updated_by: updatedBy }, { where: { dataset_id: dataset.id }, transaction }); + await Datasource.update({ status: DatasetStatus.Retired, updated_by: updatedBy }, { where: { dataset_id: dataset.id }, transaction }); + await DatasetTransformations.update({ status: DatasetStatus.Retired, updated_by: updatedBy }, { where: { dataset_id: dataset.id }, transaction }); await transaction.commit(); await this.deleteDruidSupervisors(dataset); } catch (err: any) { @@ -321,30 +323,39 @@ class DatasetService { private createDruidDataSource = async (draftDataset: Record, transaction: Transaction) => { + const {created_by, updated_by} = draftDataset; const allFields = await tableGenerator.getAllFields(draftDataset, "druid"); const draftDatasource = this.createDraftDatasource(draftDataset, "druid"); const ingestionSpec = tableGenerator.getDruidIngestionSpec(draftDataset, allFields, draftDatasource.datasource_ref); _.set(draftDatasource, "ingestion_spec", ingestionSpec) + _.set(draftDatasource, "created_by", created_by); + _.set(draftDatasource, "updated_by", updated_by); await DatasourceDraft.create(draftDatasource, { transaction }) } private createHudiDataSource = async (draftDataset: Record, transaction: Transaction) => { + const {created_by, updated_by} = draftDataset; const allFields = await tableGenerator.getAllFields(draftDataset, "hudi"); const draftDatasource = this.createDraftDatasource(draftDataset, "hudi"); const ingestionSpec = tableGenerator.getHudiIngestionSpecForCreate(draftDataset, allFields, draftDatasource.datasource_ref); _.set(draftDatasource, "ingestion_spec", ingestionSpec) + _.set(draftDatasource, "created_by", created_by); + _.set(draftDatasource, "updated_by", updated_by); await DatasourceDraft.create(draftDatasource, { transaction }) } private updateHudiDataSource = async (draftDataset: Record, transaction: Transaction) => { + const {created_by, updated_by} = draftDataset; const allFields = await tableGenerator.getAllFields(draftDataset, "hudi"); const draftDatasource = this.createDraftDatasource(draftDataset, "hudi"); const dsId = _.join([draftDataset.dataset_id, "events", "hudi"], "_") const liveDatasource = await Datasource.findOne({ where: { id: dsId }, attributes: ["ingestion_spec"], raw: true }) as unknown as Record const ingestionSpec = tableGenerator.getHudiIngestionSpecForUpdate(draftDataset, liveDatasource?.ingestion_spec, allFields, draftDatasource?.datasource_ref); _.set(draftDatasource, "ingestion_spec", ingestionSpec) + _.set(draftDatasource, "created_by", created_by); + _.set(draftDatasource, "updated_by", updated_by); await DatasourceDraft.create(draftDatasource, { transaction }) } diff --git a/api-service/src/services/managers/index.ts b/api-service/src/services/managers/index.ts index 84b666d9..e6cf822f 100644 --- a/api-service/src/services/managers/index.ts +++ b/api-service/src/services/managers/index.ts @@ -24,16 +24,16 @@ const getService = (manager: string) => { }; export const publishAlert = async (payload: Record) => { - const { id, manager } = payload; + const { id, manager, updated_by } = payload; const service = getService(manager); const publishResponse = await service.publishAlert(payload) - await updateStatus(id, "live"); + await updateStatus(id, "live", updated_by); return publishResponse; }; -const updateStatus = (id: string, status: string) => { - return Alert.update({ status }, { where: { id } }); +const updateStatus = (id: string, status: string, updated_by: string) => { + return Alert.update({ status, updated_by }, { where: { id } }); } const deleteRule = (id: string) => { @@ -41,7 +41,7 @@ const deleteRule = (id: string) => { } export const deleteAlertRule = async (payload: Record, hardDelete: boolean) => { - const { id, manager, status } = payload; + const { id, manager, status, updated_by } = payload; if (status == "live") { try { @@ -56,7 +56,7 @@ export const deleteAlertRule = async (payload: Record, hardDelete: return deleteRule(id); } - return updateStatus(id, "retired"); + return updateStatus(id, "retired", updated_by); } diff --git a/api-service/src/services/telemetry.ts b/api-service/src/services/telemetry.ts index d4eca61b..8408dfb3 100644 --- a/api-service/src/services/telemetry.ts +++ b/api-service/src/services/telemetry.ts @@ -10,15 +10,15 @@ const telemetryTopic = _.get(appConfig, "telemetry_dataset"); export enum OperationType { CREATE = 1, UPDATE, PUBLISH, RETIRE, LIST, GET } -const getDefaults = () => { +const getDefaults = (userID:any) => { return { eid: "AUDIT", ets: Date.now(), ver: "1.0.0", mid: v4(), actor: { - id: "SYSTEM", - type: "User" + id: userID || "SYSTEM", + type: "User", }, context: { env, @@ -128,7 +128,7 @@ export const processAuditEvents = (request: Request) => { _.set(auditEvent, "edata.transition.toState", toState); _.set(auditEvent, "edata.transition.fromState", fromState); } - const telemetryEvent = getDefaults(); + const telemetryEvent = getDefaults((request as any)?.userID); _.set(telemetryEvent, "edata", edata); _.set(telemetryEvent, "object", { ...(object.id && object.type && { ...object, ver: "1.0.0" }) }); sendTelemetryEvents(telemetryEvent);