diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 43d223ad6..f4c25d54b 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -3,7 +3,9 @@ import { Types } from "mongoose"; import { standardRequest } from "../../config/request"; import { getApps, getTeams, revokeAccess } from "../../integrations"; import { Bot, IntegrationAuth } from "../../models"; +import { EventType } from "../../ee/models"; import { IntegrationService } from "../../services"; +import { EEAuditLogService } from "../../ee/services"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, @@ -62,6 +64,19 @@ export const oAuthExchange = async (req: Request, res: Response) => { environment: environments[0].slug }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.AUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + }, + { + workspaceId: integrationAuth.workspace + } + ); + return res.status(200).send({ integrationAuth }); @@ -129,6 +144,19 @@ export const saveIntegrationAccessToken = async (req: Request, res: Response) => }); if (!integrationAuth) throw new Error("Failed to save integration access token"); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.AUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + }, + { + workspaceId: integrationAuth.workspace + } + ); return res.status(200).send({ integrationAuth @@ -530,6 +558,23 @@ export const deleteIntegrationAuth = async (req: Request, res: Response) => { integrationAuth: req.integrationAuth, accessToken: req.accessToken }); + + if (!integrationAuth) return res.status(400).send({ + message: "Failed to find integration authorization" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UNAUTHORIZE_INTEGRATION, + metadata: { + integration: integrationAuth.integration + } + }, + { + workspaceId: integrationAuth.workspace + } + ); return res.status(200).send({ integrationAuth diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 91b32c805..e647cfb00 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -6,6 +6,8 @@ import { eventStartIntegration } from "../../events"; import Folder from "../../models/folder"; import { getFolderByPath } from "../../services/FolderService"; import { BadRequestError } from "../../utils/errors"; +import { EEAuditLogService } from "../../ee/services"; +import { EventType } from "../../ee/models"; /** * Create/initialize an (empty) integration for integration authorization @@ -74,6 +76,25 @@ export const createIntegration = async (req: Request, res: Response) => { }) }); } + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_INTEGRATION, + metadata: { + integrationId: integration._id.toString(), + integration: integration.integration, + environment: integration.environment, + secretPath, + app: integration.app, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId + } + }, + { + workspaceId: integration.workspace + } + ); return res.status(200).send({ integration @@ -148,8 +169,7 @@ export const updateIntegration = async (req: Request, res: Response) => { }; /** - * Delete integration with id [integrationId] and deactivate bot if there are - * no integrations left + * Delete integration with id [integrationId] * @param req * @param res * @returns @@ -163,6 +183,25 @@ export const deleteIntegration = async (req: Request, res: Response) => { if (!integration) throw new Error("Failed to find integration"); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_INTEGRATION, + metadata: { + integrationId: integration._id.toString(), + integration: integration.integration, + environment: integration.environment, + secretPath: integration.secretPath, + app: integration.app, + targetEnvironment: integration.targetEnvironment, + targetEnvironmentId: integration.targetEnvironmentId + } + }, + { + workspaceId: integration.workspace + } + ); + return res.status(200).send({ integration }); diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index d795bcb60..a473f0c4a 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,9 +1,12 @@ import { Request, Response } from "express"; -import { Key, Membership, MembershipOrg, User } from "../../models"; +import { Types } from "mongoose"; +import { Key, Membership, MembershipOrg, User, IUser } from "../../models"; +import { EventType } from "../../ee/models"; import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; import { sendMail } from "../../helpers/nodemailer"; import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; import { getSiteURL } from "../../config"; +import { EEAuditLogService } from "../../ee/services"; /** * Check that user is a member of workspace with id [workspaceId] @@ -36,11 +39,11 @@ export const validateMembership = async (req: Request, res: Response) => { */ export const deleteMembership = async (req: Request, res: Response) => { const { membershipId } = req.params; - + // check if membership to delete exists const membershipToDelete = await Membership.findOne({ _id: membershipId - }).populate("user"); + }).populate<{ user: IUser }>("user"); if (!membershipToDelete) { throw new Error("Failed to delete workspace membership that doesn't exist"); @@ -66,6 +69,20 @@ export const deleteMembership = async (req: Request, res: Response) => { const deletedMembership = await deleteMember({ membershipId: membershipToDelete._id.toString() }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.REMOVE_WORKSPACE_MEMBER, + metadata: { + userId: membershipToDelete.user._id.toString(), + email: membershipToDelete.user.email + } + }, + { + workspaceId: membership.workspace + } + ); return res.status(200).send({ deletedMembership @@ -140,7 +157,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { const inviteeMembership = await Membership.findOne({ user: invitee._id, workspace: workspaceId - }); + }).populate<{ user: IUser }>("user"); if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); @@ -181,6 +198,20 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { } }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_WORKSPACE_MEMBER, + metadata: { + userId: invitee._id.toString(), + email: invitee.email + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.status(200).send({ invitee, latestKey diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 6b31fd111..742852343 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -1,5 +1,6 @@ import { Request, Response } from "express"; import { Secret } from "../../models"; +import { Types } from "mongoose"; import Folder from "../../models/folder"; import { BadRequestError } from "../../utils/errors"; import { @@ -8,6 +9,7 @@ import { generateFolderId, getAllFolderIds, getFolderByPath, + getFolderPath, getParentFromFolderId, searchByFolderId, searchByFolderIdWithDir, @@ -15,10 +17,9 @@ import { } from "../../services/FolderService"; import { ADMIN, MEMBER } from "../../variables"; import { validateMembership } from "../../helpers/membership"; -import { FolderVersion } from "../../ee/models"; -import { EESecretService } from "../../ee/services"; +import { FolderVersion, EventType } from "../../ee/models"; +import { EESecretService, EEAuditLogService } from "../../ee/services"; -// TODO // verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { const { workspaceId, environment, folderName, parentFolderId } = req.body; @@ -33,6 +34,7 @@ export const createFolder = async (req: Request, res: Response) => { environment, }).lean(); // space has no folders initialized + if (!folders) { const id = generateFolderId(); const folder = new Folder({ @@ -56,13 +58,32 @@ export const createFolder = async (req: Request, res: Response) => { workspaceId, environment, }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_FOLDER, + metadata: { + environment, + folderId: id, + folderName, + folderPath: `root/${folderName}` + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.json({ folder: { id, name: folderName } }); } const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); + await Folder.findByIdAndUpdate(folders._id, folders); const parentFolder = searchByFolderId(folders.nodes, parentFolderId); + const folderVersion = new FolderVersion({ workspace: workspaceId, environment, @@ -75,6 +96,24 @@ export const createFolder = async (req: Request, res: Response) => { environment, folderId: parentFolderId, }); + + const folderPath = await getFolderPath(folders, folder.id); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_FOLDER, + metadata: { + environment, + folderId: folder.id, + folderName, + folderPath + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); return res.json({ folder }); }; @@ -87,7 +126,7 @@ export const updateFolderById = async (req: Request, res: Response) => { if (!folders) { throw BadRequestError({ message: "The folder doesn't exist" }); } - + // check that user is a member of the workspace await validateMembership({ userId: req.user._id.toString(), @@ -100,10 +139,12 @@ export const updateFolderById = async (req: Request, res: Response) => { throw BadRequestError({ message: "The folder doesn't exist" }); } const folder = parentFolder.children.find(({ id }) => id === folderId); + if (!folder) { throw BadRequestError({ message: "The folder doesn't exist" }); } + const oldFolderName = folder.name; parentFolder.version += 1; folder.name = name; @@ -121,6 +162,25 @@ export const updateFolderById = async (req: Request, res: Response) => { folderId: parentFolder.id, }); + const folderPath = await getFolderPath(folders, folder.id); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_FOLDER, + metadata: { + environment, + folderId: folder.id, + oldFolderName, + newFolderName: name, + folderPath + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + return res.json({ message: "Successfully updated folder", folder: { name: folder.name, id: folder.id }, @@ -143,6 +203,8 @@ export const deleteFolder = async (req: Request, res: Response) => { acceptedRoles: [ADMIN, MEMBER], }); + const folderPath = await getFolderPath(folders, folderId); + const delOp = deleteFolderById(folders.nodes, folderId); if (!delOp) { throw BadRequestError({ message: "The folder doesn't exist" }); @@ -173,6 +235,22 @@ export const deleteFolder = async (req: Request, res: Response) => { folderId: parentFolder.id, }); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_FOLDER , + metadata: { + environment, + folderId, + folderName: delFolder.name, + folderPath + } + }, + { + workspaceId: new Types.ObjectId(workspaceId) + } + ); + res.send({ message: "successfully deleted folders", folders: delFolderIds }); }; diff --git a/backend/src/controllers/v1/webhookController.ts b/backend/src/controllers/v1/webhookController.ts index afaf82e81..a79b794cf 100644 --- a/backend/src/controllers/v1/webhookController.ts +++ b/backend/src/controllers/v1/webhookController.ts @@ -4,7 +4,9 @@ import { client, getRootEncryptionKey } from "../../config"; import { validateMembership } from "../../helpers"; import Webhook from "../../models/webhooks"; import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService"; -import { BadRequestError } from "../../utils/errors"; +import { BadRequestError, ResourceNotFoundError } from "../../utils/errors"; +import { EEAuditLogService } from "../../ee/services"; +import { EventType } from "../../ee/models"; import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables"; export const createWebhook = async (req: Request, res: Response) => { @@ -27,6 +29,23 @@ export const createWebhook = async (req: Request, res: Response) => { } await webhook.save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_WEBHOOK, + metadata: { + webhookId: webhook._id.toString(), + environment, + secretPath, + webhookUrl, + isDisabled: false + } + }, + { + workspaceId + } + ); return res.status(200).send({ webhook, @@ -54,6 +73,23 @@ export const updateWebhook = async (req: Request, res: Response) => { } await webhook.save(); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + webhookId: webhook._id.toString(), + environment: webhook.environment, + secretPath: webhook.secretPath, + webhookUrl: webhook.url, + isDisabled + } + }, + { + workspaceId: webhook.workspace + } + ); + return res.status(200).send({ webhook, message: "successfully updated webhook" @@ -62,9 +98,10 @@ export const updateWebhook = async (req: Request, res: Response) => { export const deleteWebhook = async (req: Request, res: Response) => { const { webhookId } = req.params; - const webhook = await Webhook.findById(webhookId); + let webhook = await Webhook.findById(webhookId); + if (!webhook) { - throw BadRequestError({ message: "Webhook not found!!" }); + throw ResourceNotFoundError({ message: "Webhook not found!!" }); } await validateMembership({ @@ -72,8 +109,29 @@ export const deleteWebhook = async (req: Request, res: Response) => { workspaceId: webhook.workspace, acceptedRoles: [ADMIN, MEMBER] }); + + webhook = await Webhook.findByIdAndDelete(webhookId); - await webhook.deleteOne(); + if (!webhook) { + throw ResourceNotFoundError({ message: "Webhook not found!!" }); + } + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_WEBHOOK, + metadata: { + webhookId: webhook._id.toString(), + environment: webhook.environment, + secretPath: webhook.secretPath, + webhookUrl: webhook.url, + isDisabled: webhook.isDisabled + } + }, + { + workspaceId: webhook.workspace + } + ); return res.status(200).send({ message: "successfully removed webhook" diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index e7de3a8cf..20680f1c6 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -8,8 +8,8 @@ import { ServiceTokenData, Workspace, } from "../../models"; -import { SecretVersion } from "../../ee/models"; -import { EELicenseService } from "../../ee/services"; +import { SecretVersion, EventType } from "../../ee/models"; +import { EELicenseService, EEAuditLogService } from "../../ee/services"; import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import _ from "lodash"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; @@ -61,6 +61,20 @@ export const createWorkspaceEnvironment = async ( await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId)); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_ENVIRONMENT, + metadata: { + name: environmentName, + slug: environmentSlug + } + }, + { + workspaceId: workspace._id + } + ); + return res.status(200).send({ message: "Successfully created new environment", workspace: workspaceId, @@ -110,6 +124,8 @@ export const renameWorkspaceEnvironment = async ( if (envIndex === -1) { throw new Error("Invalid environment given"); } + + const oldEnvironment = workspace.environments[envIndex]; workspace.environments[envIndex].name = environmentName; workspace.environments[envIndex].slug = environmentSlug.toLowerCase(); @@ -142,8 +158,23 @@ export const renameWorkspaceEnvironment = async ( }, { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } - ) - + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_ENVIRONMENT, + metadata: { + oldName: oldEnvironment.name, + newName: environmentName, + oldSlug: oldEnvironment.slug, + newSlug: environmentSlug.toLowerCase() + } + }, + { + workspaceId: workspace._id + } + ); return res.status(200).send({ message: "Successfully update environment", @@ -179,6 +210,8 @@ export const deleteWorkspaceEnvironment = async ( if (envIndex === -1) { throw new Error("Invalid environment given"); } + + const oldEnvironment = workspace.environments[envIndex]; workspace.environments.splice(envIndex, 1); await workspace.save(); @@ -218,6 +251,20 @@ export const deleteWorkspaceEnvironment = async ( await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId)); + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_ENVIRONMENT, + metadata: { + name: oldEnvironment.name, + slug: oldEnvironment.slug + } + }, + { + workspaceId: workspace._id + } + ); + return res.status(200).send({ message: "Successfully deleted environment", workspace: workspaceId, diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 10fb50ef1..c918e6953 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -4,7 +4,8 @@ import bcrypt from "bcrypt"; import { ServiceTokenData } from "../../models"; import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; -import { ActorType } from "../../ee/models"; +import { ActorType, EventType } from "../../ee/models"; +import { EEAuditLogService } from "../../ee/services"; /** * Return service token data associated with service token on request @@ -99,6 +100,20 @@ export const createServiceTokenData = async (req: Request, res: Response) => { if (!serviceTokenData) throw new Error("Failed to find service token data"); const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_SERVICE_TOKEN, + metadata: { + name, + scopes + } + }, + { + workspaceId + } + ); return res.status(200).send({ serviceToken, @@ -116,6 +131,24 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { const { serviceTokenDataId } = req.params; const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); + + if (!serviceTokenData) return res.status(200).send({ + message: "Failed to delete service token" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_SERVICE_TOKEN, + metadata: { + name: serviceTokenData.name, + scopes: serviceTokenData?.scopes + } + }, + { + workspaceId: serviceTokenData.workspace + } + ); return res.status(200).send({ serviceTokenData diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index a77cdca71..4db8c4855 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -13,13 +13,14 @@ import { ServiceActor, TFolderRootVersionSchema, TrustedIP, - UserActor + UserActor, + EventType } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import Folder, { TFolderSchema } from "../../../models/folder"; import { searchByFolderId } from "../../../services/FolderService"; -import { EELicenseService } from "../../services"; +import { EELicenseService, EEAuditLogService } from "../../services"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; /** @@ -730,6 +731,21 @@ export const addWorkspaceTrustedIp = async (req: Request, res: Response) => { isActive, comment, }).save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_TRUSTED_IP, + metadata: { + trustedIpId: trustedIp._id.toString(), + ipAddress: trustedIp.ipAddress, + prefix: trustedIp.prefix + } + }, + { + workspaceId: trustedIp.workspace + } + ); return res.status(200).send({ trustedIp @@ -793,6 +809,25 @@ export const updateWorkspaceTrustedIp = async (req: Request, res: Response) => { } ); + if (!trustedIp) return res.status(400).send({ + message: "Failed to update trusted IP" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_TRUSTED_IP, + metadata: { + trustedIpId: trustedIp._id.toString(), + ipAddress: trustedIp.ipAddress, + prefix: trustedIp.prefix + } + }, + { + workspaceId: trustedIp.workspace + } + ); + return res.status(200).send({ trustedIp }); @@ -816,6 +851,25 @@ export const deleteWorkspaceTrustedIp = async (req: Request, res: Response) => { _id: new Types.ObjectId(trustedIpId), workspace: new Types.ObjectId(workspaceId) }); + + if (!trustedIp) return res.status(400).send({ + message: "Failed to delete trusted IP" + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.DELETE_TRUSTED_IP, + metadata: { + trustedIpId: trustedIp._id.toString(), + ipAddress: trustedIp.ipAddress, + prefix: trustedIp.prefix + } + }, + { + workspaceId: trustedIp.workspace + } + ); return res.status(200).send({ trustedIp diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index ce415e8c9..73dcd39bc 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -16,5 +16,25 @@ export enum EventType { REVEAL_SECRET = "reveal-secret", CREATE_SECRET = "create-secret", UPDATE_SECRET = "update-secret", - DELETE_SECRET = "delete-secret" + DELETE_SECRET = "delete-secret", + AUTHORIZE_INTEGRATION = "authorize-integration", + UNAUTHORIZE_INTEGRATION = "unauthorize-integration", + CREATE_INTEGRATION = "create-integration", + DELETE_INTEGRATION = "delete-integration", + ADD_TRUSTED_IP = "add-trusted-ip", + UPDATE_TRUSTED_IP = "update-trusted-ip", + DELETE_TRUSTED_IP = "delete-trusted-ip", + CREATE_SERVICE_TOKEN = "create-service-token", + DELETE_SERVICE_TOKEN = "delete-service-token", + CREATE_ENVIRONMENT = "create-environment", + UPDATE_ENVIRONMENT = "update-environment", + DELETE_ENVIRONMENT = "delete-environment", + ADD_WORKSPACE_MEMBER = "add-workspace-member", + REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + CREATE_FOLDER = "create-folder", + UPDATE_FOLDER = "update-folder", + DELETE_FOLDER = "delete-folder", + CREATE_WEBHOOK = "create-webhook", + UPDATE_WEBHOOK_STATUS = "update-webhook-status", + DELETE_WEBHOOK = "delete-webhook" } \ No newline at end of file diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 848fb88db..04ff317e6 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -80,9 +80,224 @@ interface DeleteSecretEvent { } } +interface AuthorizeIntegrationEvent { + type: EventType.AUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface UnauthorizeIntegrationEvent { + type: EventType.UNAUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface CreateIntegrationEvent { + type: EventType.CREATE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; // TODO: consider adding other vars + } +} + +interface DeleteIntegrationEvent { + type: EventType.DELETE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + } +} + +interface AddTrustedIPEvent { + type: EventType.ADD_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface UpdateTrustedIPEvent { + type: EventType.UPDATE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface DeleteTrustedIPEvent { + type: EventType.DELETE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface CreateServiceTokenEvent { + type: EventType.CREATE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface DeleteServiceTokenEvent { + type: EventType.DELETE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface CreateEnvironmentEvent { + type: EventType.CREATE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface UpdateEnvironmentEvent { + type: EventType.UPDATE_ENVIRONMENT; + metadata: { + oldName: string; + newName: string; + oldSlug: string; + newSlug: string; + } +} + +interface DeleteEnvironmentEvent { + type: EventType.DELETE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface AddWorkspaceMemberEvent { + type: EventType.ADD_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface RemoveWorkspaceMemberEvent { + type: EventType.REMOVE_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface CreateFolderEvent { + type: EventType.CREATE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface UpdateFolderEvent { + type: EventType.UPDATE_FOLDER; + metadata: { + environment: string; + folderId: string; + oldFolderName: string; + newFolderName: string; + folderPath: string; + } +} + +interface DeleteFolderEvent { + type: EventType.DELETE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface CreateWebhookEvent { + type: EventType.CREATE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface UpdateWebhookStatusEvent { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface DeleteWebhookEvent { + type: EventType.DELETE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + export type Event = | GetSecretsEvent | GetSecretEvent | CreateSecretEvent | UpdateSecretEvent - | DeleteSecretEvent; \ No newline at end of file + | DeleteSecretEvent + | AuthorizeIntegrationEvent + | UnauthorizeIntegrationEvent + | CreateIntegrationEvent + | DeleteIntegrationEvent + | AddTrustedIPEvent + | UpdateTrustedIPEvent + | DeleteTrustedIPEvent + | CreateServiceTokenEvent + | DeleteServiceTokenEvent + | CreateEnvironmentEvent + | UpdateEnvironmentEvent + | DeleteEnvironmentEvent + | AddWorkspaceMemberEvent + | RemoveWorkspaceMemberEvent + | CreateFolderEvent + | UpdateFolderEvent + | DeleteFolderEvent + | CreateWebhookEvent + | UpdateWebhookStatusEvent + | DeleteWebhookEvent; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 90eea76e6..2143f0781 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -97,8 +97,8 @@ router.get( query("eventType").isString().isIn(Object.values(EventType)).optional({ nullable: true }), query("userAgentType").isString().isIn(Object.values(UserAgentType)).optional({ nullable: true }), query("actor").isString().optional({ nullable: true }), - query("offset").isString().default("0"), - query("limit").isString().default("20"), + query("offset").default("0"), + query("limit").default("20"), validateRequest, workspaceController.getWorkspaceAuditLogs ); diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index de8e4369d..569f2483f 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -63,12 +63,12 @@ class EELicenseService { environmentsUsed: 0, secretVersioning: true, pitRecovery: false, - ipAllowlisting: false, + ipAllowlisting: true, rbac: true, customRateLimits: true, customAlerts: true, - auditLogs: false, - auditLogsRetentionDays: 0, + auditLogs: true, + auditLogsRetentionDays: 30, samlSSO: false, status: null, trial_end: null, diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index f2dfb3050..7d3e4bd59 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -1,6 +1,7 @@ import { nanoid } from "nanoid"; import { Types } from "mongoose"; -import Folder, { TFolderSchema } from "../models/folder"; +import Folder, { TFolderSchema, TFolderRootSchema } from "../models/folder"; +import { ResourceNotFoundError } from "../utils/errors"; type TAppendFolderDTO = { folderName: string; @@ -172,6 +173,20 @@ export const searchByFolderIdWithDir = ( return; }; +export const getFolderPath = ( + folders: TFolderRootSchema, + folderId: string + ) => { + const folderBySearch = searchByFolderIdWithDir(folders.nodes, folderId); + + if (!folderBySearch) throw ResourceNotFoundError({ + message: "Failed to find folder" + }); + + const folderPath = folderBySearch.dir.map((folder) => folder.name).join("/"); + return folderPath; + } + // to get folder of a path given // Like /frontend/folder#1 export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index f54b20b1a..3b15efa91 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -1,11 +1,31 @@ import { EventType, UserAgentType } from "./enums"; export const eventToNameMap: { [K in EventType]: string } = { - [EventType.GET_SECRETS]: "Get Secrets", - [EventType.GET_SECRET]: "Get Secret", - [EventType.CREATE_SECRET]: "Create Secret", - [EventType.UPDATE_SECRET]: "Update Secret", - [EventType.DELETE_SECRET]: "Delete Secret", + [EventType.GET_SECRETS]: "Get secrets", + [EventType.GET_SECRET]: "Get secret", + [EventType.CREATE_SECRET]: "Create secret", + [EventType.UPDATE_SECRET]: "Update secret", + [EventType.DELETE_SECRET]: "Delete secret", + [EventType.AUTHORIZE_INTEGRATION]: "Authorize integration", + [EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration", + [EventType.CREATE_INTEGRATION]: "Create integration", + [EventType.DELETE_INTEGRATION]: "Delete integration", + [EventType.ADD_TRUSTED_IP]: "Add trusted IP", + [EventType.UPDATE_TRUSTED_IP]: "Update trusted IP", + [EventType.DELETE_TRUSTED_IP]: "Delete trusted IP", + [EventType.CREATE_SERVICE_TOKEN]: "Create service token", + [EventType.DELETE_SERVICE_TOKEN]: "Delete service token", + [EventType.CREATE_ENVIRONMENT]: "Create environment", + [EventType.UPDATE_ENVIRONMENT]: "Update environment", + [EventType.DELETE_ENVIRONMENT]: "Delete environment", + [EventType.ADD_WORKSPACE_MEMBER]: "Add member", + [EventType.REMOVE_WORKSPACE_MEMBER]: "Remove member", + [EventType.CREATE_FOLDER]: "Create folder", + [EventType.UPDATE_FOLDER]: "Update folder", + [EventType.DELETE_FOLDER]: "Delete folder", + [EventType.CREATE_WEBHOOK]: "Create webhook", + [EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status", + [EventType.DELETE_WEBHOOK]: "Delete webhook", }; export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index 4c75f6395..3e159552d 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -11,9 +11,29 @@ export enum UserAgentType { } export enum EventType { - GET_SECRETS = "get-secrets", - GET_SECRET = "get-secret", - CREATE_SECRET = "create-secret", - UPDATE_SECRET = "update-secret", - DELETE_SECRET = "delete-secret" + GET_SECRETS = "get-secrets", + GET_SECRET = "get-secret", + CREATE_SECRET = "create-secret", + UPDATE_SECRET = "update-secret", + DELETE_SECRET = "delete-secret", + AUTHORIZE_INTEGRATION = "authorize-integration", + UNAUTHORIZE_INTEGRATION = "unauthorize-integration", + CREATE_INTEGRATION = "create-integration", + DELETE_INTEGRATION = "delete-integration", + ADD_TRUSTED_IP = "add-trusted-ip", + UPDATE_TRUSTED_IP = "update-trusted-ip", + DELETE_TRUSTED_IP = "delete-trusted-ip", + CREATE_SERVICE_TOKEN = "create-service-token", + DELETE_SERVICE_TOKEN = "delete-service-token", + CREATE_ENVIRONMENT = "create-environment", + UPDATE_ENVIRONMENT = "update-environment", + DELETE_ENVIRONMENT = "delete-environment", + ADD_WORKSPACE_MEMBER = "add-workspace-member", + REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", + CREATE_FOLDER = "create-folder", + UPDATE_FOLDER = "update-folder", + DELETE_FOLDER = "delete-folder", + CREATE_WEBHOOK = "create-webhook", + UPDATE_WEBHOOK_STATUS = "update-webhook-status", + DELETE_WEBHOOK = "delete-webhook" } \ No newline at end of file diff --git a/frontend/src/hooks/api/auditLogs/queries.tsx b/frontend/src/hooks/api/auditLogs/queries.tsx index 05148c941..930de7f0a 100644 --- a/frontend/src/hooks/api/auditLogs/queries.tsx +++ b/frontend/src/hooks/api/auditLogs/queries.tsx @@ -2,28 +2,22 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { EventType, UserAgentType } from "./enums"; import { Actor, - AuditLog} from "./types"; + AuditLog, + AuditLogFilters +} from "./types"; export const workspaceKeys = { - getAuditLogs: (workspaceId: string, filters: { - eventType?: EventType; - userAgentType?: UserAgentType; - actor?: string; - }) => [{ workspaceId, filters }, "audit-logs"] as const, + getAuditLogs: (workspaceId: string, filters: AuditLogFilters) => [{ workspaceId, filters }, "audit-logs"] as const, getAuditLogActorFilterOpts: (workspaceId: string) => [{ workspaceId }, "audit-log-actor-filters"] as const } -export const useGetAuditLogs = (workspaceId: string, filters: { - eventType?: EventType; - userAgentType?: UserAgentType; - actor?: string; -}) => { +export const useGetAuditLogs = (workspaceId: string, filters: AuditLogFilters) => { return useQuery({ queryKey: workspaceKeys.getAuditLogs(workspaceId, filters), queryFn: async () => { + const params = new URLSearchParams(); if (filters.eventType) { params.append("eventType", filters.eventType); @@ -37,6 +31,13 @@ export const useGetAuditLogs = (workspaceId: string, filters: { params.append("actor", filters.actor); } + if (filters.actor) { + params.append("actor", filters.actor); + } + + params.append("offset ", String(filters.offset)); + params.append("limit ", String(filters.limit)); + const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(`/api/v1/workspace/${workspaceId}/audit-logs`, { params }); return data.auditLogs; } diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index e836521ec..b7c3cc47a 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -82,12 +82,227 @@ interface DeleteSecretEvent { } } +interface AuthorizeIntegrationEvent { + type: EventType.AUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface UnauthorizeIntegrationEvent { + type: EventType.UNAUTHORIZE_INTEGRATION; + metadata: { + integration: string; // TODO: fix type + } +} + +interface CreateIntegrationEvent { + type: EventType.CREATE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; // TODO: consider adding other vars + } +} + +interface DeleteIntegrationEvent { + type: EventType.DELETE_INTEGRATION; + metadata: { + integrationId: string; + integration: string; // TODO: fix type + environment: string; + secretPath: string; + app?: string; + targetEnvironment?: string; + targetEnvironmentId?: string; + } +} + +interface AddTrustedIPEvent { + type: EventType.ADD_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface UpdateTrustedIPEvent { + type: EventType.UPDATE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface DeleteTrustedIPEvent { + type: EventType.DELETE_TRUSTED_IP; + metadata: { + trustedIpId: string; + ipAddress: string; + prefix?: number; + } +} + +interface CreateServiceTokenEvent { + type: EventType.CREATE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface DeleteServiceTokenEvent { + type: EventType.DELETE_SERVICE_TOKEN; + metadata: { + name: string; + scopes: Array<{ + environment: string; + secretPath: string; + }>; + } +} + +interface CreateEnvironmentEvent { + type: EventType.CREATE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface UpdateEnvironmentEvent { + type: EventType.UPDATE_ENVIRONMENT; + metadata: { + oldName: string; + newName: string; + oldSlug: string; + newSlug: string; + } +} + +interface DeleteEnvironmentEvent { + type: EventType.DELETE_ENVIRONMENT; + metadata: { + name: string; + slug: string; + } +} + +interface AddWorkspaceMemberEvent { + type: EventType.ADD_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface RemoveWorkspaceMemberEvent { + type: EventType.REMOVE_WORKSPACE_MEMBER; + metadata: { + userId: string; + email: string; + } +} + +interface CreateFolderEvent { + type: EventType.CREATE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface UpdateFolderEvent { + type: EventType.UPDATE_FOLDER; + metadata: { + environment: string; + folderId: string; + oldFolderName: string; + newFolderName: string; + folderPath: string; + } +} + +interface DeleteFolderEvent { + type: EventType.DELETE_FOLDER; + metadata: { + environment: string; + folderId: string; + folderName: string; + folderPath: string; + } +} + +interface CreateWebhookEvent { + type: EventType.CREATE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface UpdateWebhookStatusEvent { + type: EventType.UPDATE_WEBHOOK_STATUS, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + +interface DeleteWebhookEvent { + type: EventType.DELETE_WEBHOOK, + metadata: { + webhookId: string; + environment: string; + secretPath: string; + webhookUrl: string; + isDisabled: boolean; + } +} + export type Event = | GetSecretsEvent | GetSecretEvent | CreateSecretEvent | UpdateSecretEvent - | DeleteSecretEvent; + | DeleteSecretEvent + | AuthorizeIntegrationEvent + | UnauthorizeIntegrationEvent + | CreateIntegrationEvent + | DeleteIntegrationEvent + | AddTrustedIPEvent + | UpdateTrustedIPEvent + | DeleteTrustedIPEvent + | CreateServiceTokenEvent + | DeleteServiceTokenEvent + | CreateEnvironmentEvent + | UpdateEnvironmentEvent + | DeleteEnvironmentEvent + | AddWorkspaceMemberEvent + | RemoveWorkspaceMemberEvent + | CreateFolderEvent + | UpdateFolderEvent + | DeleteFolderEvent + | CreateWebhookEvent + | UpdateWebhookStatusEvent + | DeleteWebhookEvent; export type AuditLog = { _id: string; @@ -101,3 +316,11 @@ export type AuditLog = { createdAt: string; updatedAt: string; } + +export type AuditLogFilters = { + eventType?: EventType; + userAgentType?: UserAgentType; + actor?: string; + offset: number; + limit: number; +} \ No newline at end of file diff --git a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx index 823e3f56d..0262a640d 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsTable.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsTable.tsx @@ -33,7 +33,9 @@ export const LogsTable = ({ const { data, isLoading } = useGetAuditLogs(currentWorkspace?._id ?? "", { eventType, userAgentType, - actor + actor, + offset: 0, // TODO: update with pagination + limit: 20 // TODO: update with pagination }); return ( diff --git a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx index 05dfec9b0..7c4ecf90d 100644 --- a/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx +++ b/frontend/src/views/Project/LogsPage/components/LogsTableRow.tsx @@ -79,6 +79,169 @@ export const LogsTableRow = ({
{`Secret: ${event.metadata.secretKey}`}
); + case EventType.AUTHORIZE_INTEGRATION: + return ( +{`Integration: ${event.metadata.integration}`}
+{`Integration: ${event.metadata.integration}`}
+{`Integration: ${event.metadata.integration}`}
+{`Environment: ${event.metadata.environment}`}
+{`Path: ${event.metadata.secretPath}`}
+ {event.metadata.app && ( +{`Target app: ${event.metadata.app}`}
+ )} + {event.metadata.targetEnvironment && ( +{`Target environment: ${event.metadata.targetEnvironment}`}
+ )} + {event.metadata.targetEnvironmentId && ( +{`Target environment ID: ${event.metadata.targetEnvironmentId}`}
+ )} +{`Integration: ${event.metadata.integration}`}
+{`Environment: ${event.metadata.environment}`}
+{`Path: ${event.metadata.secretPath}`}
+ {event.metadata.app && ( +{`Target App: ${event.metadata.app}`}
+ )} + {event.metadata.targetEnvironment && ( +{`Target environment: ${event.metadata.targetEnvironment}`}
+ )} + {event.metadata.targetEnvironmentId && ( +{`Target environment ID: ${event.metadata.targetEnvironmentId}`}
+ )} +{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}
+{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}
+{`IP: ${event.metadata.ipAddress}${event.metadata.prefix !== undefined ? `/${event.metadata.prefix}` : ""}`}
+{`Name: ${event.metadata.name}`}
+{`Name: ${event.metadata.name}`}
+{`Name: ${event.metadata.name}`}
+{`Slug: ${event.metadata.slug}`}
+{`Old name: ${event.metadata.oldName}`}
+{`New name: ${event.metadata.newName}`}
+{`Old slug: ${event.metadata.oldSlug}`}
+{`New slug: ${event.metadata.newSlug}`}
+{`Name: ${event.metadata.name}`}
+{`Slug: ${event.metadata.slug}`}
+{`Email: ${event.metadata.email}`}
+{`Email: ${event.metadata.email}`}
+{`Environment: ${event.metadata.environment}`}
+{`Path: ${event.metadata.folderPath}`}
+{`Folder: ${event.metadata.folderName}`}
+{`Environment: ${event.metadata.environment}`}
+{`Path: ${event.metadata.folderPath}`}
+{`Old folder: ${event.metadata.oldFolderName}`}
+{`New folder: ${event.metadata.newFolderName}`}
+{`Environment: ${event.metadata.environment}`}
+{`Path: ${event.metadata.folderPath}`}
+{`Folder: ${event.metadata.folderName}`}
+{`Environment: ${event.metadata.environment}`}
+{`Secret Path: ${event.metadata.secretPath}`}
+{`Webhook URL: ${event.metadata.webhookUrl}`}
+{`Disabled: ${event.metadata.isDisabled}`}
+{`Environment: ${event.metadata.environment}`}
+{`Secret Path: ${event.metadata.secretPath}`}
+{`Webhook URL: ${event.metadata.webhookUrl}`}
+{`Disabled: ${event.metadata.isDisabled}`}
+{`Environment: ${event.metadata.environment}`}
+{`Secret Path: ${event.metadata.secretPath}`}
+{`Webhook URL: ${event.metadata.webhookUrl}`}
+{`Disabled: ${event.metadata.isDisabled}`}
+