Update secret imports audit log v2

This commit is contained in:
Tuan Dang
2023-08-08 22:04:23 +07:00
parent 0cad823267
commit 7ae859e9ae
17 changed files with 335 additions and 253 deletions

View File

@@ -1,11 +1,12 @@
import { Request, Response } from "express";
import { validateMembership } from "../../helpers";
import SecretImport from "../../models/secretImports";
import { Folder, SecretImport } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { BadRequestError } from "../../utils/errors";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { ADMIN, MEMBER } from "../../variables";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { getFolderPath } from "../../services/FolderService";
export const createSecretImport = async (req: Request, res: Response) => {
const { workspaceId, environment, folderId, secretImport } = req.body;
@@ -14,6 +15,17 @@ export const createSecretImport = async (req: Request, res: Response) => {
environment,
folderId
});
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = await getFolderPath(folders, folderId);
if (!importSecDoc) {
const doc = new SecretImport({
@@ -29,11 +41,12 @@ export const createSecretImport = async (req: Request, res: Response) => {
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
environment,
secretImportId: doc._id.toString(),
folderId: doc.folderId.toString(),
importEnvironment: secretImport.environment,
importSecretPath: secretImport.secretPath
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath
}
},
{
@@ -61,11 +74,12 @@ export const createSecretImport = async (req: Request, res: Response) => {
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
environment,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importEnvironment: secretImport.environment,
importSecretPath: secretImport.secretPath
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath
}
},
{
@@ -91,17 +105,33 @@ export const updateSecretImport = async (req: Request, res: Response) => {
acceptedRoles: [ADMIN, MEMBER]
});
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = await getFolderPath(folders, importSecDoc.folderId);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
environment: importSecDoc.environment,
importToEnvironment: importSecDoc.environment,
importToSecretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
numberOfImports: secretImports.length
orderBefore,
orderAfter: secretImports
}
},
{
@@ -130,16 +160,28 @@ export const deleteSecretImport = async (req: Request, res: Response) => {
);
await importSecDoc.save();
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment,
}).lean();
if (!folders) throw ResourceNotFoundError({
message: "Failed to find folder"
});
const importToSecretPath = await getFolderPath(folders, importSecDoc.folderId);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
environment: importSecDoc.environment,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importEnvironment: secretImportEnv,
importSecretPath: secretImportPath
importFromEnvironment: secretImportEnv,
importFromSecretPath: secretImportPath,
importToEnvironment: importSecDoc.environment,
importToSecretPath
}
},
{

View File

@@ -307,32 +307,42 @@ interface GetSecretImportsEvent {
interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
importEnvironment: string;
importSecretPath: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
}
interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
numberOfImports: number;
importToEnvironment: string;
importToSecretPath: string;
orderBefore: {
environment: string;
secretPath: string;
}[],
orderAfter: {
environment: string;
secretPath: string;
}[]
}
}
interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
importEnvironment: string;
importSecretPath: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
}

View File

@@ -10,6 +10,8 @@ import Membership, { IMembership } from "./membership";
import MembershipOrg, { IMembershipOrg } from "./membershipOrg";
import Organization, { IOrganization } from "./organization";
import Secret, { ISecret } from "./secret";
import Folder, { TFolderRootSchema, TFolderSchema } from "./folder";
import SecretImport, { ISecretImports } from "./secretImports";
import SecretBlindIndexData, { ISecretBlindIndexData } from "./secretBlindIndexData";
import ServiceToken, { IServiceToken } from "./serviceToken";
import ServiceAccount, { IServiceAccount } from "./serviceAccount"; // new
@@ -51,6 +53,11 @@ export {
IOrganization,
Secret,
ISecret,
Folder,
TFolderRootSchema,
TFolderSchema,
SecretImport,
ISecretImports,
SecretBlindIndexData,
ISecretBlindIndexData,
ServiceToken,

View File

@@ -309,32 +309,42 @@ interface GetSecretImportsEvent {
interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
importEnvironment: string;
importSecretPath: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
}
interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
numberOfImports: number;
importToEnvironment: string;
importToSecretPath: string;
orderBefore: {
environment: string;
secretPath: string;
}[],
orderAfter: {
environment: string;
secretPath: string;
}[]
}
}
interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
environment: string;
secretImportId: string;
folderId: string;
importEnvironment: string;
importSecretPath: string;
importFromEnvironment: string;
importFromSecretPath: string;
importToEnvironment: string;
importToSecretPath: string;
}
}

View File

@@ -483,11 +483,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?._id}/logs`} passHref>
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?._id}/logs`
router.asPath === `/project/${currentWorkspace?._id}/audit-logs`
}
icon="system-outline-168-view-headline"
>
@@ -495,11 +495,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem>
</a>
</Link>
<Link href={`/project/${currentWorkspace?._id}/audit-logs`} passHref>
<Link href={`/project/${currentWorkspace?._id}/logs`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?._id}/audit-logs`
router.asPath === `/project/${currentWorkspace?._id}/logs`
}
icon="system-outline-168-view-headline"
>

View File

@@ -1,197 +1,23 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { useRouter } from "next/router";
import Button from "@app/components/basic/buttons/Button";
import EventFilter from "@app/components/basic/EventFilter";
import { UpgradePlanModal } from "@app/components/v2";
import { useSubscription } from "@app/context";
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
import { usePopUp } from "@app/hooks/usePopUp";
import { AuditLogsPage } from "@app/views/Project/AuditLogsPage";
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
import ActivityTable from "../../../../ee/components/ActivityTable";
const Logs = () => {
const { t } = useTranslation();
interface LogData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: {
email: string;
};
serviceAccount?: {
string: string;
},
serviceTokenData?: {
name: string;
}
actions: {
_id: string;
name: string;
payload: {
secretVersions: string[];
};
}[];
}
interface PayloadProps {
_id: string;
name: string;
secretVersions: string[];
}
interface LogDataPoint {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
serviceAccount: {
name: string;
};
serviceTokenData: {
name: string;
};
payload: PayloadProps[];
}
/**
* This is the tab that includes all of the user activity logs
*/
export default function Activity() {
const router = useRouter();
const [eventChosen, setEventChosen] = useState("");
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>();
const { t } = useTranslation();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"upgradePlan"
] as const);
// this use effect updates the data in case of a new filter being added
useEffect(() => {
setCurrentOffset(0);
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({
workspaceId: String(router.query.id),
offset: 0,
limit: currentLimit,
userId: "",
actionNames: eventChosen
});
setLogsData(
tempLogsData.map((log: LogData) => ({
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log?.user?.email,
serviceAccount: log?.serviceAccount,
serviceTokenData: log?.serviceTokenData,
payload: log.actions.map((action) => ({
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}))
}))
);
setIsLoading(false);
};
getLogData();
}, [eventChosen]);
// this use effect adds more data in case 'View More' button is clicked
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({
workspaceId: String(router.query.id),
offset: currentOffset,
limit: currentLimit,
userId: "",
actionNames: eventChosen
});
setLogsData(
logsData.concat(
tempLogsData.map((log: LogData) => ({
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log?.user?.email,
serviceAccount: log?.serviceAccount,
serviceTokenData: log?.serviceTokenData,
payload: log.actions.map((action) => ({
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}))
}))
)
);
setIsLoading(false);
};
getLogData();
}, [currentLimit, currentOffset]);
const loadMoreLogs = () => {
if (subscription?.auditLogs === false) {
handlePopUpOpen("upgradePlan");
} else {
setCurrentOffset(currentOffset + currentLimit);
}
};
return (
<div className="mx-auto w-full h-full max-w-7xl">
<Head>
<title>Audit Logs</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
{currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
)}
<div className="flex flex-col justify-between items-start mx-4 mb-4 text-xl px-2">
<div className="flex flex-row justify-start items-center text-3xl mt-6">
<p className="font-semibold mr-4 text-bunker-100">{t("activity.title")}</p>
</div>
<p className="mr-4 text-base text-gray-400">{t("activity.subtitle")}</p>
</div>
<div className="px-6 h-8 mt-2">
<EventFilter selected={eventChosen} select={setEventChosen} />
</div>
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
<div className="flex justify-center w-full mb-6">
<div className="items-center w-60">
<Button
text={String(t("common.view-more"))}
textDisabled={String(t("common.end-of-history"))}
active={logsData.length % 10 === 0}
onButtonPressed={loadMoreLogs}
size="md"
color="mineshaft"
/>
</div>
</div>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
/>
)}
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<AuditLogsPage />
</div>
);
);
}
Activity.requireAuth = true;
export default Logs;
Logs.requireAuth = true;

View File

@@ -1,23 +1,197 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { useRouter } from "next/router";
import { LogsPage } from "@app/views/Project/LogsPage";
import Button from "@app/components/basic/buttons/Button";
import EventFilter from "@app/components/basic/EventFilter";
import { UpgradePlanModal } from "@app/components/v2";
import { useSubscription } from "@app/context";
import ActivitySideBar from "@app/ee/components/ActivitySideBar";
import { usePopUp } from "@app/hooks/usePopUp";
const Logs = () => {
const { t } = useTranslation();
import getProjectLogs from "../../../../ee/api/secrets/GetProjectLogs";
import ActivityTable from "../../../../ee/components/ActivityTable";
return (
<div className="h-full bg-bunker-800">
<Head>
<title>{t("common.head-title", { title: t("billing.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
<LogsPage />
</div>
);
interface LogData {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: {
email: string;
};
serviceAccount?: {
string: string;
},
serviceTokenData?: {
name: string;
}
actions: {
_id: string;
name: string;
payload: {
secretVersions: string[];
};
}[];
}
export default Logs;
interface PayloadProps {
_id: string;
name: string;
secretVersions: string[];
}
interface LogDataPoint {
_id: string;
channel: string;
createdAt: string;
ipAddress: string;
user: string;
serviceAccount: {
name: string;
};
serviceTokenData: {
name: string;
};
payload: PayloadProps[];
}
/**
* This is the tab that includes all of the user activity logs
*/
export default function Activity() {
const router = useRouter();
const [eventChosen, setEventChosen] = useState("");
const [logsData, setLogsData] = useState<LogDataPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [currentOffset, setCurrentOffset] = useState(0);
const currentLimit = 10;
const [currentSidebarAction, toggleSidebar] = useState<string>();
const { t } = useTranslation();
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
"upgradePlan"
] as const);
// this use effect updates the data in case of a new filter being added
useEffect(() => {
setCurrentOffset(0);
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({
workspaceId: String(router.query.id),
offset: 0,
limit: currentLimit,
userId: "",
actionNames: eventChosen
});
setLogsData(
tempLogsData.map((log: LogData) => ({
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log?.user?.email,
serviceAccount: log?.serviceAccount,
serviceTokenData: log?.serviceTokenData,
payload: log.actions.map((action) => ({
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}))
}))
);
setIsLoading(false);
};
getLogData();
}, [eventChosen]);
// this use effect adds more data in case 'View More' button is clicked
useEffect(() => {
const getLogData = async () => {
setIsLoading(true);
const tempLogsData = await getProjectLogs({
workspaceId: String(router.query.id),
offset: currentOffset,
limit: currentLimit,
userId: "",
actionNames: eventChosen
});
setLogsData(
logsData.concat(
tempLogsData.map((log: LogData) => ({
_id: log._id,
channel: log.channel,
createdAt: log.createdAt,
ipAddress: log.ipAddress,
user: log?.user?.email,
serviceAccount: log?.serviceAccount,
serviceTokenData: log?.serviceTokenData,
payload: log.actions.map((action) => ({
_id: action._id,
name: action.name,
secretVersions: action.payload.secretVersions
}))
}))
)
);
setIsLoading(false);
};
getLogData();
}, [currentLimit, currentOffset]);
const loadMoreLogs = () => {
if (subscription?.auditLogs === false) {
handlePopUpOpen("upgradePlan");
} else {
setCurrentOffset(currentOffset + currentLimit);
}
};
return (
<div className="mx-auto w-full h-full max-w-7xl">
<Head>
<title>Audit Logs</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
</Head>
{currentSidebarAction && (
<ActivitySideBar toggleSidebar={toggleSidebar} currentAction={currentSidebarAction} />
)}
<div className="flex flex-col justify-between items-start mx-4 mb-4 text-xl px-2">
<div className="flex flex-row justify-start items-center text-3xl mt-6">
<p className="font-semibold mr-4 text-bunker-100">{t("activity.title")}</p>
</div>
<p className="mr-4 text-base text-gray-400">{t("activity.subtitle")}</p>
</div>
<div className="px-6 h-8 mt-2">
<EventFilter selected={eventChosen} select={setEventChosen} />
</div>
<ActivityTable data={logsData} toggleSidebar={toggleSidebar} isLoading={isLoading} />
<div className="flex justify-center w-full mb-6">
<div className="items-center w-60">
<Button
text={String(t("common.view-more"))}
textDisabled={String(t("common.end-of-history"))}
active={logsData.length % 10 === 0}
onButtonPressed={loadMoreLogs}
size="md"
color="mineshaft"
/>
</div>
</div>
{subscription && (
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={() => handlePopUpClose("upgradePlan")}
text={subscription.slug === null ? "You can see more logs under an Enterprise license" : "You can see more logs if you switch to Infisical's Business/Professional Plan."}
/>
)}
</div>
);
}
Activity.requireAuth = true;
Logs.requireAuth = true;

View File

@@ -27,7 +27,7 @@ type Props = {
onEnvCompare: (secretKey: string) => void;
secretVersion?: Array<{ id: string; createdAt: string; value: string }>;
// to record the ids of deleted ones
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
onSecretDelete: (index: number, secretName: string, id?: string, overrideId?: string) => void;
onSave: () => void;
};
@@ -45,6 +45,15 @@ export const SecretDetailDrawer = ({
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
const { register, setValue, control, getValues } = useFormContext<FormData>();
const secKey = useWatch({
control,
name: `secrets.${index}.key`,
disabled: false,
exact: true
});
console.log("secKeyyy", secKey);
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
const isOverridden =

View File

@@ -2,7 +2,7 @@ import {
LogsSection
} from "./components";
export const LogsPage = () => {
export const AuditLogsPage = () => {
return (
<div className="flex justify-center bg-bunker-800 text-white w-full h-full">
<div className="max-w-7xl px-6 w-full">

View File

@@ -73,7 +73,8 @@ export const LogsFilter = ({
className="w-40 mr-4"
>
<Select
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
placeholder="Select"
// {...(field.value ? { value: field.value } : { placeholder: "Select" })}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
@@ -189,7 +190,7 @@ export const LogsFilter = ({
}}
/>
</div>
{/* <div>
<div>
<Button
isLoading={false}
colorSchema="primary"
@@ -206,7 +207,7 @@ export const LogsFilter = ({
>
Clear filters
</Button>
</div> */}
</div>
</div>
);
}

View File

@@ -2,10 +2,9 @@ import {
Td,
Tr
} from "@app/components/v2";
import { eventToNameMap, userAgentTTypeoNameMap } from "~/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "~/hooks/api/auditLogs/enums";
import { Actor, AuditLog, Event } from "~/hooks/api/auditLogs/types";
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor, AuditLog, Event } from "@app/hooks/api/auditLogs/types";
type Props = {
auditLog: AuditLog
@@ -258,27 +257,31 @@ export const LogsTableRow = ({
case EventType.CREATE_SECRET_IMPORT:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Imported path: ${event.metadata.importSecretPath}`}</p>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.UPDATE_SECRET_IMPORT:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`# Imported path: ${event.metadata.numberOfImports}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
case EventType.DELETE_SECRET_IMPORT:
return (
<Td>
<p>{`Environment: ${event.metadata.environment}`}</p>
<p>{`Imported path: ${event.metadata.importSecretPath}`}</p>
<p>{`Import from env: ${event.metadata.importFromEnvironment}`}</p>
<p>{`Import from path: ${event.metadata.importFromSecretPath}`}</p>
<p>{`Import to env: ${event.metadata.importToEnvironment}`}</p>
<p>{`Import to path: ${event.metadata.importToSecretPath}`}</p>
</Td>
);
default:
return (
<Td></Td>
<Td />
);
}
}

View File

@@ -0,0 +1 @@
export { AuditLogsPage } from "./AuditLogsPage";

View File

@@ -1 +0,0 @@
export { LogsPage } from "./LogsPage";