feat(secret-approval): added new audit log and subscription on policy creation

This commit is contained in:
Akhil Mohan
2023-10-05 20:44:16 +05:30
parent 7caac2e64c
commit faacb75034
12 changed files with 374 additions and 259 deletions
@@ -9,6 +9,8 @@ import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy"; import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService"; import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
export const getSecretApprovalRequests = async (req: Request, res: Response) => { export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const { const {
@@ -31,6 +33,7 @@ export const getSecretApprovalRequests = async (req: Request, res: Response) =>
{ {
$match: query $match: query
}, },
{ $sort: { createdAt: -1 } },
{ {
$lookup: { $lookup: {
from: SecretApprovalPolicy.collection.name, from: SecretApprovalPolicy.collection.name,
@@ -234,5 +237,37 @@ export const updateSecretApprovalRequestStatus = async (req: Request, res: Respo
{ status, statusChangeBy: membership._id }, { status, statusChangeBy: membership._id },
{ new: true } { new: true }
); );
if (status === "close") {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_CLOSED,
metadata: {
closedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
} else {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_REOPENED,
metadata: {
reopenedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
}
return res.send({ approval: updatedRequest }); return res.send({ approval: updatedRequest });
}; };
@@ -595,14 +595,17 @@ export const createSecret = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Create secretAction: ProjectPermissionActions.Create
}); });
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership && type !== "personal") { if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({ const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
policy: secretApprovalPolicy, policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(), commiterMembershipId: membership._id.toString(),
authData:req.authData,
data: { data: {
[CommitType.CREATE]: [ [CommitType.CREATE]: [
{ {
@@ -623,6 +626,7 @@ export const createSecret = async (req: Request, res: Response) => {
}); });
return res.send({ approval: secretApprovalRequest }); return res.send({ approval: secretApprovalRequest });
} }
}
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
secretName, secretName,
@@ -700,14 +704,16 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Edit secretAction: ProjectPermissionActions.Edit
}); });
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership && type !== "personal") { if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({ const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
policy: secretApprovalPolicy, policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(), commiterMembershipId: membership._id.toString(),
authData: req.authData,
data: { data: {
[CommitType.UPDATE]: [ [CommitType.UPDATE]: [
{ {
@@ -730,6 +736,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
}); });
return res.send({ approval: secretApprovalRequest }); return res.send({ approval: secretApprovalRequest });
} }
}
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
secretName, secretName,
@@ -784,12 +791,14 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Delete secretAction: ProjectPermissionActions.Delete
}); });
if (membership && type !== "personal") {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership && type !== "personal") { if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({ const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
authData: req.authData,
policy: secretApprovalPolicy, policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(), commiterMembershipId: membership._id.toString(),
data: { data: {
@@ -802,6 +811,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
}); });
return res.send({ approval: secretApprovalRequest }); return res.send({ approval: secretApprovalRequest });
} }
}
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -838,12 +848,14 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Create secretAction: ProjectPermissionActions.Create
}); });
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) { if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({ const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
authData: req.authData,
policy: secretApprovalPolicy, policy: secretApprovalPolicy,
commiterMembershipId: membership._id.toString(), commiterMembershipId: membership._id.toString(),
data: { data: {
@@ -852,6 +864,7 @@ export const createSecretByNameBatch = async (req: Request, res: Response) => {
}); });
return res.send({ approval: secretApprovalRequest }); return res.send({ approval: secretApprovalRequest });
} }
}
const createdSecrets = await SecretService.createSecretBatch({ const createdSecrets = await SecretService.createSecretBatch({
secretPath, secretPath,
@@ -879,8 +892,9 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Edit secretAction: ProjectPermissionActions.Edit
}); });
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) { if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({ const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId, workspaceId,
environment, environment,
@@ -889,10 +903,12 @@ export const updateSecretByNameBatch = async (req: Request, res: Response) => {
commiterMembershipId: membership._id.toString(), commiterMembershipId: membership._id.toString(),
data: { data: {
[CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared") [CommitType.UPDATE]: secrets.filter(({ type }) => type === "shared")
} },
authData: req.authData
}); });
return res.send({ approval: secretApprovalRequest }); return res.send({ approval: secretApprovalRequest });
} }
}
const updatedSecrets = await SecretService.updateSecretBatch({ const updatedSecrets = await SecretService.updateSecretBatch({
secretPath, secretPath,
@@ -920,8 +936,10 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
secretAction: ProjectPermissionActions.Delete secretAction: ProjectPermissionActions.Delete
}); });
if (membership) {
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath); const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
if (secretApprovalPolicy && membership) { if (secretApprovalPolicy) {
const secretApprovalRequest = await generateSecretApprovalRequest({ const secretApprovalRequest = await generateSecretApprovalRequest({
workspaceId, workspaceId,
environment, environment,
@@ -930,10 +948,12 @@ export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
commiterMembershipId: membership._id.toString(), commiterMembershipId: membership._id.toString(),
data: { data: {
[CommitType.DELETE]: secrets.filter(({ type }) => type === "shared") [CommitType.DELETE]: secrets.filter(({ type }) => type === "shared")
} },
authData: req.authData
}); });
return res.send({ approval: secretApprovalRequest }); return res.send({ approval: secretApprovalRequest });
} }
}
const deletedSecrets = await SecretService.deleteSecretBatch({ const deletedSecrets = await SecretService.deleteSecretBatch({
secretPath, secretPath,
+5 -1
View File
@@ -50,5 +50,9 @@ export enum EventType {
UPDATE_SECRET_IMPORT = "update-secret-import", UPDATE_SECRET_IMPORT = "update-secret-import",
DELETE_SECRET_IMPORT = "delete-secret-import", DELETE_SECRET_IMPORT = "delete-secret-import",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions" UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
SECRET_APPROVAL_MERGED = "secret-approval-merged",
SECRET_APPROVAL_REQUEST = "secret-approval-request",
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
} }
+40 -1
View File
@@ -437,6 +437,41 @@ interface UpdateUserDeniedPermissions {
}[] }[]
} }
} }
interface SecretApprovalMerge {
type: EventType.SECRET_APPROVAL_MERGED;
metadata: {
mergedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalClosed {
type: EventType.SECRET_APPROVAL_CLOSED;
metadata: {
closedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalReopened {
type: EventType.SECRET_APPROVAL_REOPENED;
metadata: {
reopenedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
interface SecretApprovalRequest {
type: EventType.SECRET_APPROVAL_REQUEST;
metadata: {
committedBy: string;
secretApprovalRequestSlug: string;
secretApprovalRequestId: string;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
@@ -476,4 +511,8 @@ export type Event =
| UpdateSecretImportEvent | UpdateSecretImportEvent
| DeleteSecretImportEvent | DeleteSecretImportEvent
| UpdateUserRole | UpdateUserRole
| UpdateUserDeniedPermissions; | UpdateUserDeniedPermissions
| SecretApprovalMerge
| SecretApprovalClosed
| SecretApprovalRequest
| SecretApprovalReopened;
@@ -1,4 +1,5 @@
import { Schema, Types, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { customAlphabet } from "nanoid";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables";
export enum ApprovalStatus { export enum ApprovalStatus {
@@ -13,6 +14,9 @@ export enum CommitType {
CREATE = "create" CREATE = "create"
} }
const SLUG_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const nanoId = customAlphabet(SLUG_ALPHABETS, 10);
export interface ISecretApprovalSecChange { export interface ISecretApprovalSecChange {
_id: Types.ObjectId; _id: Types.ObjectId;
version: number; version: number;
@@ -54,6 +58,7 @@ export type ISecretCommits<T = Types.ObjectId, J = Types.ObjectId> = Array<
export interface ISecretApprovalRequest { export interface ISecretApprovalRequest {
_id: Types.ObjectId; _id: Types.ObjectId;
committer: Types.ObjectId; committer: Types.ObjectId;
slug: string;
statusChangeBy: Types.ObjectId; statusChangeBy: Types.ObjectId;
reviewers: { reviewers: {
member: Types.ObjectId; member: Types.ObjectId;
@@ -143,6 +148,10 @@ const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
required: true, required: true,
default: "root" default: "root"
}, },
slug: {
type: String,
default: () => nanoId()
},
reviewers: { reviewers: {
type: [ type: [
{ {
+35 -59
View File
@@ -96,6 +96,7 @@ type TGenerateSecretApprovalRequestArg = {
[CommitType.DELETE]?: { secretName: string }[]; [CommitType.DELETE]?: { secretName: string }[];
}; };
commiterMembershipId: string; commiterMembershipId: string;
authData: AuthData;
}; };
export const generateSecretApprovalRequest = async ({ export const generateSecretApprovalRequest = async ({
@@ -104,7 +105,8 @@ export const generateSecretApprovalRequest = async ({
secretPath, secretPath,
policy, policy,
data, data,
commiterMembershipId commiterMembershipId,
authData
}: TGenerateSecretApprovalRequestArg) => { }: TGenerateSecretApprovalRequestArg) => {
// calculate folder id from secret path // calculate folder id from secret path
let folderId = "root"; let folderId = "root";
@@ -313,6 +315,22 @@ export const generateSecretApprovalRequest = async ({
committer: commiterMembershipId committer: commiterMembershipId
}); });
await secretApprovalRequest.save(); await secretApprovalRequest.save();
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: commiterMembershipId,
secretApprovalRequestId: secretApprovalRequest._id.toString(),
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
return secretApprovalRequest; return secretApprovalRequest;
}; };
@@ -436,26 +454,6 @@ export const performSecretApprovalRequestMerge = async (
}) })
) )
}); });
// question to team where to keep secretKey
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.CREATE_SECRETS,
metadata: {
environment,
secretPath: "/",
secrets: newlyCreatedSecrets.map(({ version, _id }) => ({
secretId: _id.toString(),
secretKey: "",
secretVersion: version
}))
}
},
{
workspaceId
}
);
} }
const secretUpdationCommits = secretApprovalRequest.commits.filter( const secretUpdationCommits = secretApprovalRequest.commits.filter(
@@ -570,25 +568,6 @@ export const performSecretApprovalRequestMerge = async (
}); });
}) })
}); });
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.UPDATE_SECRETS,
metadata: {
environment,
secretPath: "/",
secrets: nonConflictSecrets.map(({ secret }) => ({
secretId: secret._id.toString(),
secretKey: "",
secretVersion: secret.version + 1
}))
}
},
{
workspaceId
}
);
} }
const secretDeletionCommits = secretApprovalRequest.commits.filter( const secretDeletionCommits = secretApprovalRequest.commits.filter(
@@ -614,25 +593,6 @@ export const performSecretApprovalRequestMerge = async (
await EESecretService.markDeletedSecretVersions({ await EESecretService.markDeletedSecretVersions({
secretIds: secretDeletionCommits.map(({ secret }) => secret._id) secretIds: secretDeletionCommits.map(({ secret }) => secret._id)
}); });
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.DELETE_SECRETS,
metadata: {
environment,
secretPath: "/",
secrets: secretDeletionCommits.map(({ secret: { _id, version } }) => ({
secretId: _id.toString(),
secretKey: "",
secretVersion: version
}))
}
},
{
workspaceId
}
);
} }
const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate( const updatedSecretApproval = await SecretApprovalRequest.findByIdAndUpdate(
@@ -671,5 +631,21 @@ export const performSecretApprovalRequestMerge = async (
folderId folderId
}); });
// question to team where to keep secretKey
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.SECRET_APPROVAL_MERGED,
metadata: {
mergedBy: userMembershipId,
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId
}
);
return updatedSecretApproval; return updatedSecretApproval;
}; };
@@ -47,6 +47,7 @@ export type TSecretApprovalRequest<
J extends unknown = EncryptedSecret J extends unknown = EncryptedSecret
> = { > = {
_id: string; _id: string;
slug: string;
createdAt: string; createdAt: string;
committer: string; committer: string;
reviewers: { reviewers: {
+10 -1
View File
@@ -11,12 +11,21 @@ export type SubscriptionPlan = {
rbac: boolean; rbac: boolean;
secretVersioning: boolean; secretVersioning: boolean;
slug: string; slug: string;
secretApproval: string;
tier: number; tier: number;
workspaceLimit: number; workspaceLimit: number;
workspacesUsed: number; workspacesUsed: number;
environmentLimit: number; environmentLimit: number;
samlSSO: boolean; samlSSO: boolean;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null; status:
| "incomplete"
| "incomplete_expired"
| "trialing"
| "active"
| "past_due"
| "canceled"
| "unpaid"
| null;
trial_end: number | null; trial_end: number | null;
has_used_trial: boolean; has_used_trial: boolean;
}; };
@@ -14,9 +14,15 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr Tr,
UpgradePlanModal
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useSubscription
} from "@app/context";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { import {
useDeleteSecretApprovalPolicy, useDeleteSecretApprovalPolicy,
@@ -35,9 +41,11 @@ type Props = {
export const SecretApprovalPolicyList = ({ workspaceId }: Props) => { export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
"secretPolicyForm", "secretPolicyForm",
"deletePolicy" "deletePolicy",
"upgradePlan"
] as const); ] as const);
const permission = useProjectPermission(); const permission = useProjectPermission();
const { subscription } = useSubscription();
const { createNotification } = useNotificationContext(); const { createNotification } = useNotificationContext();
const { data: members } = useGetWorkspaceUsers(workspaceId); const { data: members } = useGetWorkspaceUsers(workspaceId);
@@ -87,7 +95,13 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
> >
{(isAllowed) => ( {(isAllowed) => (
<Button <Button
onClick={() => handlePopUpOpen("secretPolicyForm")} onClick={() => {
if (subscription && !subscription?.secretApproval) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("secretPolicyForm");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />} leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed} isDisabled={!isAllowed}
> >
@@ -147,6 +161,11 @@ export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)} onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
onDeleteApproved={handleDeletePolicy} onDeleteApproved={handleDeletePolicy}
/> />
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add secret approval policy if you switch to Infisical's Team plan."
/>
</div> </div>
); );
}; };
@@ -213,6 +213,7 @@ export const SecretApprovalRequest = () => {
<div className="mb-1"> <div className="mb-1">
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" /> <FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
{generateCommitText(commits)} {generateCommitText(commits)}
<span className="text-xs text-bunker-300"> #{secretApproval.slug}</span>
</div> </div>
<span className="text-xs text-gray-500"> <span className="text-xs text-gray-500">
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "}
@@ -139,9 +139,11 @@ export const SecretApprovalRequestChanges = ({
}; };
if (isSecretApprovalRequestLoading) { if (isSecretApprovalRequestLoading) {
return (
<div> <div>
<ContentLoader /> <ContentLoader />
</div>; </div>
);
} }
if (!isSecretApprovalRequestSuccess) if (!isSecretApprovalRequestSuccess)
@@ -245,7 +245,7 @@ export const SecretListView = ({
}); });
} }
}, },
[environment, secretPath] [environment, secretPath, isProtectedBranch]
); );
const handleSecretDelete = useCallback(async () => { const handleSecretDelete = useCallback(async () => {