diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 1df50b18b..4fe040122 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -41,10 +41,11 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { let logs try { const { workspaceId } = req.params; - const { userId, actionNames } = req.query; const offset: number = parseInt(req.query.offset as string); const limit: number = parseInt(req.query.limit as string); + const userId: string = req.query.userId as string; + const actionNames: string = req.query.actionNames as string; logs = await Log.find({ workspace: workspaceId, @@ -53,11 +54,12 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { actionNames ? { actionNames: { - $in: actionNames + $in: actionNames.split(',') } } : {} ) }) + .sort({ createdAt: -1 }) .skip(offset) .limit(limit) .populate('actions') diff --git a/frontend/components/basic/EventFilter.tsx b/frontend/components/basic/EventFilter.tsx index d4fe36193..c9b31fd43 100644 --- a/frontend/components/basic/EventFilter.tsx +++ b/frontend/components/basic/EventFilter.tsx @@ -6,6 +6,7 @@ import { faEye, faPlus, faShuffle, + faTrash, faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -28,6 +29,10 @@ const eventOptions = [ { name: 'updateSecrets', icon: faShuffle + }, + { + name: 'deleteSecrets', + icon: faTrash } ]; @@ -48,7 +53,7 @@ export default function EventFilter({
{selected != '' ? ( -

{selected}

+

{t("activity:event." + selected)}

) : (

Select an event

)} @@ -76,7 +81,7 @@ export default function EventFilter({ className={`px-4 h-10 flex items-center text-sm cursor-pointer hover:bg-mineshaft-700 text-bunker-200 rounded-md ${ selected == t("activity:event." + event.name) && 'bg-mineshaft-700' }`} - value={t("activity:event." + event.name)} + value={event.name} > {({ selected }) => ( <> @@ -90,7 +95,6 @@ export default function EventFilter({ )} - {/* {event.name} */} ); })} diff --git a/frontend/ee/api/secrets/GetActionData.ts b/frontend/ee/api/secrets/GetActionData.ts new file mode 100644 index 000000000..122870d69 --- /dev/null +++ b/frontend/ee/api/secrets/GetActionData.ts @@ -0,0 +1,32 @@ +import SecurityClient from '~/utilities/SecurityClient'; + + +interface workspaceProps { + actionId: string; +} + +/** + * This function fetches the data for a certain action performed by a user + * @param {object} obj + * @param {string} obj.actionId - id of an action for which we are trying to get data + * @returns + */ +const getActionData = async ({ actionId }: workspaceProps) => { + return SecurityClient.fetchCall( + '/api/v1/action/' + actionId, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + } + ).then(async (res) => { + console.log(188, res) + if (res && res.status == 200) { + return (await res.json()).action; + } else { + console.log('Failed to get the info about an action'); + } + }); +}; + +export default getActionData; diff --git a/frontend/ee/api/secrets/GetProjectLogs.ts b/frontend/ee/api/secrets/GetProjectLogs.ts index d5324355c..277205f2f 100644 --- a/frontend/ee/api/secrets/GetProjectLogs.ts +++ b/frontend/ee/api/secrets/GetProjectLogs.ts @@ -5,7 +5,8 @@ interface workspaceProps { workspaceId: string; offset: number; limit: number; - filters: object; + userId: string; + actionNames: string; } /** @@ -14,17 +15,41 @@ interface workspaceProps { * @param {string} obj.workspaceId - workspace id for which we are trying to get project log * @param {object} obj.offset - teh starting point of logs that we want to pull * @param {object} obj.limit - how many logs will we output - * @param {object} obj.filters + * @param {object} obj.userId - optional userId filter - will only query logs for that user + * @param {string} obj.actionNames - optional actionNames filter - will only query logs for those actions * @returns */ -const getProjectLogs = async ({ workspaceId, offset, limit, filters }: workspaceProps) => { +const getProjectLogs = async ({ workspaceId, offset, limit, userId, actionNames }: workspaceProps) => { + let payload; + if (userId != "" && actionNames != '') { + payload = { + offset: String(offset), + limit: String(limit), + userId: JSON.stringify(userId), + actionNames: actionNames + } + } else if (userId != "") { + payload = { + offset: String(offset), + limit: String(limit), + userId: JSON.stringify(userId) + } + } else if (actionNames != "") { + payload = { + offset: String(offset), + limit: String(limit), + actionNames: actionNames + } + } else { + payload = { + offset: String(offset), + limit: String(limit) + } + } + return SecurityClient.fetchCall( '/api/v1/workspace/' + workspaceId + '/logs?' + - new URLSearchParams({ - offset: String(offset), - limit: String(limit), - filters: JSON.stringify(filters) - }), + new URLSearchParams(payload), { method: 'GET', headers: { diff --git a/frontend/ee/components/ActivitySideBar.tsx b/frontend/ee/components/ActivitySideBar.tsx index d96a3f6ab..e0a634d45 100644 --- a/frontend/ee/components/ActivitySideBar.tsx +++ b/frontend/ee/components/ActivitySideBar.tsx @@ -1,79 +1,184 @@ +import { useEffect, useState } from "react"; +import Image from "next/image"; +import { useRouter } from "next/router"; import { useTranslation } from "next-i18next"; import { faX } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import getActionData from "ee/api/secrets/GetActionData"; import patienceDiff from 'ee/utilities/findTextDifferences'; +import getLatestFileKey from "~/pages/api/workspace/getLatestFileKey"; + import DashboardInputField from '../../components/dashboard/DashboardInputField'; -const secretChanges = [{ - "oldSecret": "secret1", - "newSecret": "ecret2" -}, { - "oldSecret": "secret1", - "newSecret": "sercet2" -}, { - "oldSecret": "localhosta:8080", - "newSecret": "aaaalocalhoats:3000" -}] +const { + decryptAssymmetric, + decryptSymmetric +} = require('../../components/utilities/cryptography/crypto'); +const nacl = require('tweetnacl'); +nacl.util = require('tweetnacl-util'); interface SideBarProps { - toggleSidebar: (value: string[]) => void; - sidebarData: string[]; - currentEvent: string; + toggleSidebar: (value: string) => void; + currentAction: string; +} + +interface SecretProps { + secret: string; + secretKeyCiphertext: string; + secretKeyHash: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueHash: string; + secretValueIV: string; + secretValueTag: string; +} + +interface DecryptedSecretProps { + newSecretVersion: { + key: string; + value: string; + } + oldSecretVersion: { + key: string; + value: string; + } +} + +interface ActionProps { + name: string; } /** * @param {object} obj * @param {function} obj.toggleSidebar - function that opens or closes the sidebar - * @param {string[]} obj.secretIds - data of payload - * @param {string} obj.currentEvent - the event name for which a sidebar is being displayed + * @param {string} obj.currentAction - the action id for which a sidebar is being displayed * @returns the sidebar with the payload of user activity logs */ const ActivitySideBar = ({ toggleSidebar, - sidebarData, - currentEvent + currentAction }: SideBarProps) => { const { t } = useTranslation(); + const router = useRouter(); + const [actionData, setActionData] = useState(); + const [actionMetaData, setActionMetaData] = useState(); + const [isLoading, setIsLoading] = useState(false); - return
-
-
-

{t("activity:event." + currentEvent)}

-
toggleSidebar([])}> - + useEffect(() => { + const getLogData = async () => { + setIsLoading(true); + const tempActionData = await getActionData({ actionId: currentAction }); + const latestKey = await getLatestFileKey({ workspaceId: String(router.query.id) }) + const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY'); + + // #TODO: make this a separate function and reuse across the app + let decryptedLatestKey: string; + if (latestKey) { + // assymmetrically decrypt symmetric key with local private key + decryptedLatestKey = decryptAssymmetric({ + ciphertext: latestKey.latestKey.encryptedKey, + nonce: latestKey.latestKey.nonce, + publicKey: latestKey.latestKey.sender.publicKey, + privateKey: String(PRIVATE_KEY) + }); + } + + const decryptedSecretVersions = tempActionData.payload.secretVersions.map((encryptedSecretVersion: { + newSecretVersion?: SecretProps; + oldSecretVersion?: SecretProps; + }) => { + return { + newSecretVersion: { + key: decryptSymmetric({ + ciphertext: encryptedSecretVersion.newSecretVersion!.secretKeyCiphertext, + iv: encryptedSecretVersion.newSecretVersion!.secretKeyIV, + tag: encryptedSecretVersion.newSecretVersion!.secretKeyTag, + key: decryptedLatestKey + }), + value: decryptSymmetric({ + ciphertext: encryptedSecretVersion.newSecretVersion!.secretValueCiphertext, + iv: encryptedSecretVersion.newSecretVersion!.secretValueIV, + tag: encryptedSecretVersion.newSecretVersion!.secretValueTag, + key: decryptedLatestKey + }) + }, + oldSecretVersion: { + key: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext + ? decryptSymmetric({ + ciphertext: encryptedSecretVersion.oldSecretVersion?.secretKeyCiphertext, + iv: encryptedSecretVersion.oldSecretVersion?.secretKeyIV, + tag: encryptedSecretVersion.oldSecretVersion?.secretKeyTag, + key: decryptedLatestKey + }): undefined, + value: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext + ? decryptSymmetric({ + ciphertext: encryptedSecretVersion.oldSecretVersion?.secretValueCiphertext, + iv: encryptedSecretVersion.oldSecretVersion?.secretValueIV, + tag: encryptedSecretVersion.oldSecretVersion?.secretValueTag, + key: decryptedLatestKey + }): undefined + } + } + }) + + setActionData(decryptedSecretVersions); + setActionMetaData({name: tempActionData.name}); + setIsLoading(false); + } + getLogData(); + }, [currentAction]); + + return
+ {isLoading ? ( +
+ infisical loading indicator +
+ ) : ( +
+
+

{t("activity:event." + actionMetaData?.name)}

+
toggleSidebar("")}> + +
+
+
+ {(actionMetaData?.name == 'readSecrets' + || actionMetaData?.name == 'addSecrets' + || actionMetaData?.name == 'deleteSecrets') && actionData?.map((item, id) => +
+
{item.newSecretVersion.key}
+ {}} + type="value" + position={1} + value={item.newSecretVersion.value} + isDuplicate={false} + blurred={false} + /> +
+ )} + {actionMetaData?.name == 'updateSecrets' && actionData?.map((item, id) => + <> +
{item.newSecretVersion.key}
+
+
- {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.bIndex != -1 && {character.line})}
+
+ {patienceDiff(item.oldSecretVersion.value.split(''), item.newSecretVersion.value.split(''), false).lines.map((character, id) => character.aIndex != -1 && {character.line})}
+
+ + )}
-
- {currentEvent == 'readSecrets' && sidebarData.map((item, id) => - <> -
Key {id}
- {}} - type="varName" - position={1} - value={"a" + item} - isDuplicate={false} - blurred={false} - /> - - )} - {currentEvent == 'updateSecrets' && sidebarData.map((item, id) => - secretChanges.map(secretChange => - <> -
Secret Name {id}
-
-
- {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split(''), false).lines.map((character, id) => character.aIndex != -1 && {character.line})}
-
+ {patienceDiff(secretChange.oldSecret.split(''), secretChange.newSecret.split('')).lines.map((character, id) => character.bIndex != -1 && {character.line})}
-
- - ))} -
-
- + )}
}; diff --git a/frontend/ee/components/ActivityTable.tsx b/frontend/ee/components/ActivityTable.tsx index a25920202..607b25c5d 100644 --- a/frontend/ee/components/ActivityTable.tsx +++ b/frontend/ee/components/ActivityTable.tsx @@ -4,8 +4,7 @@ import { useTranslation } from "next-i18next"; import { faAngleDown, faAngleRight, - faUpRightFromSquare, - faX + faUpRightFromSquare } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import timeSince from 'ee/utilities/timeSince'; @@ -14,6 +13,7 @@ import guidGenerator from '../../components/utilities/randomId'; interface PayloadProps { + _id: string; name: string; secretVersions: string[]; } @@ -29,25 +29,26 @@ interface logData { /** - * + * This is a single row of the activity table * @param obj - * @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened + * @param {logData} obj.row - data for a certain event + * @param {function} obj.toggleSidebar - open and close sidebar that displays data for a specific event * @returns */ -const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData, toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => { +const ActivityLogsRow = ({ row, toggleSidebar }: { row: logData, toggleSidebar: (value: string) => void; }) => { const [payloadOpened, setPayloadOpened] = useState(false); const { t } = useTranslation(); return ( <> - + setPayloadOpened(!payloadOpened)} className="border-mineshaft-700 border-t text-gray-300 flex items-center cursor-pointer" > @@ -66,26 +67,23 @@ const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData {payloadOpened && - + Timestamp {row.createdAt} } {payloadOpened && row.payload?.map((action, index) => - + {t("activity:event." + action.name)} - { - toggleSidebar(action.secretVersions); - setCurrentEvent(action.name); - }}> + toggleSidebar(action._id)}> {action.secretVersions.length + (action.secretVersions.length != 1 ? " secrets" : " secret")} )} {payloadOpened && - + IP Address {row.ipAddress} @@ -99,28 +97,27 @@ const ActivityLogsRow = ({ row, toggleSidebar, setCurrentEvent }: { row: logData * @param {object} obj * @param {logData} obj.data - data for user activity logs * @param {function} obj.toggleSidebar - function that opens or closes the sidebar - * @param {function} obj.setCurrentEvent - specify the name of the event for which the sidebar is being opened * @returns */ -const ActivityTable = ({ data, toggleSidebar, setCurrentEvent }: { data: logData[], toggleSidebar: (value: string[]) => void; setCurrentEvent: (value: string) => void; }) => { +const ActivityTable = ({ data, toggleSidebar }: { data: logData[], toggleSidebar: (value: string) => void; }) => { return (
-
+
- + - - - - + + + + {data?.map((row, index) => { - return ; + return ; })}
EventUserSourceTimeEVENTUSERSOURCETIME
diff --git a/frontend/pages/activity/[id].tsx b/frontend/pages/activity/[id].tsx index f384ae69f..974607d0a 100644 --- a/frontend/pages/activity/[id].tsx +++ b/frontend/pages/activity/[id].tsx @@ -21,6 +21,7 @@ interface logData { email: string; }; actions: { + _id: string; name: string; payload: { secretVersions: string[]; @@ -29,6 +30,7 @@ interface logData { } interface PayloadProps { + _id: string; name: string; secretVersions: string[]; } @@ -51,13 +53,38 @@ export default function Activity() { const [logsData, setLogsData] = useState([]); const [currentOffset, setCurrentOffset] = useState(0); const currentLimit = 10; - const [sidebarData, toggleSidebar] = useState([]) - const [currentEvent, setCurrentEvent] = useState(""); + const [currentSidebarAction, toggleSidebar] = useState() const { t } = useTranslation(); + // this use effect updates the data in case of a new filter being added + useEffect(() => { + setCurrentOffset(0); + const getLogData = async () => { + const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: 0, limit: currentLimit, userId: "", actionNames: eventChosen }) + setLogsData(tempLogsData.map((log: logData) => { + return { + _id: log._id, + channel: log.channel, + createdAt: log.createdAt, + ipAddress: log.ipAddress, + user: log.user.email, + payload: log.actions.map(action => { + return { + _id: action._id, + name: action.name, + secretVersions: action.payload.secretVersions + } + }) + } + })) + } + getLogData(); + }, [eventChosen]); + + // this use effect adds more data in case 'View More' button is clicked useEffect(() => { const getLogData = async () => { - const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, filters: {} }) + const tempLogsData = await getProjectLogs({ workspaceId: String(router.query.id), offset: currentOffset, limit: currentLimit, userId: "", actionNames: eventChosen }) setLogsData(logsData.concat(tempLogsData.map((log: logData) => { return { _id: log._id, @@ -67,6 +94,7 @@ export default function Activity() { user: log.user.email, payload: log.actions.map(action => { return { + _id: action._id, name: action.name, secretVersions: action.payload.secretVersions } @@ -84,31 +112,24 @@ export default function Activity() { return (
- {sidebarData.length > 0 && } + {currentSidebarAction && }

Activity Logs

- Event history limited to the last 12 months. + Event history for this Infisical project.

b.createdAt.localeCompare(a.createdAt)) - .filter((log) => - eventChosen != '' ? log.payload?.map(action => t("activity:event." + action.name)).includes(eventChosen) : true - ) - } + data={logsData} toggleSidebar={toggleSidebar} - setCurrentEvent={setCurrentEvent} />