misc: addressed review comments

This commit is contained in:
Sheen Capadngan
2024-09-11 00:11:19 +08:00
parent ffaf145317
commit 7a36badb23
13 changed files with 212 additions and 275 deletions
@@ -1,11 +1,6 @@
import { PackRule, unpackRules } from "@casl/ability/extra";
import { UnauthorizedError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { TVerifyPermission } from "./access-approval-request-types";
@@ -56,67 +51,3 @@ export const verifyRequestedPermissions = ({ permissions }: TVerifyPermission) =
accessTypes: requestedPermissions.filter(filterUnique)
};
};
export const triggerAccessRequestSlackNotif = async ({
projectId,
projectName,
requesterFullName,
isTemporary,
requesterEmail,
secretPath,
environment,
permissions,
approvalUrl,
projectDAL,
kmsService,
projectSlackConfigDAL
}: {
projectId: string;
projectName: string;
requesterFullName: string;
isTemporary: boolean;
requesterEmail: string;
secretPath: string;
environment: string;
permissions: string[];
approvalUrl: string;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectWithOrg">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
}) => {
const messageBody = `${requesterFullName} (${requesterEmail}) has requested ${
isTemporary ? "temporary" : "permanent"
} access to ${secretPath} in the ${environment} environment of ${projectName}.
The following permissions are requested: ${permissions.join(", ")}
View the request and approve or deny it <${approvalUrl}|here>.`;
const payloadBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "New access approval request pending for review",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
}
}
];
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
payloadMessage: messageBody,
projectSlackConfigDAL,
payloadBlocks,
feature: SlackTriggerFeature.ACCESS_REQUEST
});
};
@@ -10,6 +10,8 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -20,7 +22,7 @@ import { TPermissionServiceFactory } from "../permission/permission-service";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "../project-user-additional-privilege/project-user-additional-privilege-types";
import { TAccessApprovalRequestDALFactory } from "./access-approval-request-dal";
import { triggerAccessRequestSlackNotif, verifyRequestedPermissions } from "./access-approval-request-fns";
import { verifyRequestedPermissions } from "./access-approval-request-fns";
import { TAccessApprovalRequestReviewerDALFactory } from "./access-approval-request-reviewer-dal";
import {
ApprovalStatus,
@@ -178,19 +180,24 @@ export const accessApprovalRequestServiceFactory = ({
const requesterFullName = `${requestedByUser.firstName} ${requestedByUser.lastName}`;
const approvalUrl = `${cfg.SITE_URL}/project/${project.id}/approval`;
await triggerAccessRequestSlackNotif({
await triggerSlackNotification({
projectId: project.id,
projectName: project.name,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl,
projectSlackConfigDAL,
projectDAL,
kmsService,
projectSlackConfigDAL
notification: {
type: SlackTriggerFeature.ACCESS_REQUEST,
payload: {
projectName: project.name,
requesterFullName,
isTemporary,
requesterEmail: requestedByUser.email as string,
secretPath,
environment: envSlug,
permissions: accessTypes,
approvalUrl
}
}
});
await smtpService.sendMail({
@@ -1,12 +1,7 @@
import { TSecretApprovalRequests } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
@@ -18,68 +13,6 @@ type TSendApprovalEmails = {
secretApprovalRequest: TSecretApprovalRequests;
};
type TTriggerSecretApprovalSlackNotif = {
environment: string;
projectId: string;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectWithOrg">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
secretApprovalRequest: TSecretApprovalRequests;
secretPath: string;
userDAL: Pick<TUserDALFactory, "findById">;
};
export const triggerSecretApprovalSlackNotif = async ({
projectId,
projectDAL,
kmsService,
secretApprovalRequest,
projectSlackConfigDAL,
userDAL,
environment,
secretPath
}: TTriggerSecretApprovalSlackNotif) => {
const appCfg = getConfig();
const project = await projectDAL.findProjectWithOrg(projectId);
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
const messageBody = `A secret approval request has been opened by ${user.email}.
*Environment*: ${environment}
*Secret path*: ${secretPath || "/"}
View the complete details <${appCfg.SITE_URL}/project/${project.id}/approval?requestId=${
secretApprovalRequest.id
}|here>.`;
const payloadBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "Secret approval request",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
}
}
];
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
payloadMessage: messageBody,
projectSlackConfigDAL,
payloadBlocks,
feature: SlackTriggerFeature.SECRET_APPROVAL
});
};
export const sendApprovalEmailsFn = async ({
secretApprovalPolicyDAL,
projectDAL,
@@ -48,6 +48,8 @@ import {
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TProjectSlackConfigDALFactory } from "@app/services/slack/project-slack-config-dal";
import { triggerSlackNotification } from "@app/services/slack/slack-fns";
import { SlackTriggerFeature } from "@app/services/slack/slack-types";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -57,7 +59,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/pr
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
import { sendApprovalEmailsFn, triggerSecretApprovalSlackNotif } from "./secret-approval-request-fns";
import { sendApprovalEmailsFn } from "./secret-approval-request-fns";
import { TSecretApprovalRequestReviewerDALFactory } from "./secret-approval-request-reviewer-dal";
import { TSecretApprovalRequestSecretDALFactory } from "./secret-approval-request-secret-dal";
import {
@@ -1073,15 +1075,22 @@ export const secretApprovalRequestServiceFactory = ({
});
const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerSecretApprovalSlackNotif({
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
await triggerSlackNotification({
projectId,
secretPath: policy.secretPath as string,
environment: env.name,
projectDAL,
kmsService,
secretApprovalRequest,
userDAL,
projectSlackConfigDAL
projectSlackConfigDAL,
notification: {
type: SlackTriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id
}
}
});
await sendApprovalEmailsFn({
@@ -1346,16 +1355,23 @@ export const secretApprovalRequestServiceFactory = ({
return { ...doc, commits: approvalCommits };
});
const user = await userDAL.findById(secretApprovalRequest.committerUserId);
const env = await projectEnvDAL.findOne({ id: policy.envId });
await triggerSecretApprovalSlackNotif({
secretPath: policy.secretPath as string,
environment: env.name,
await triggerSlackNotification({
projectId,
projectDAL,
kmsService,
secretApprovalRequest,
userDAL,
projectSlackConfigDAL
projectSlackConfigDAL,
notification: {
type: SlackTriggerFeature.SECRET_APPROVAL,
payload: {
userEmail: user.email as string,
environment: env.name,
secretPath,
projectId,
requestId: secretApprovalRequest.id
}
}
});
await sendApprovalEmailsFn({
@@ -130,29 +130,6 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "GET",
url: "/integrations/slack/bot-creation-url",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.string()
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const url = await server.services.superAdmin.getCustomSlackBotCreationUrl();
return url;
}
});
server.route({
method: "GET",
url: "/integrations/slack/config",
+88 -52
View File
@@ -1,53 +1,14 @@
import { Block, WebClient } from "@slack/web-api";
import { WebClient } from "@slack/web-api";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectSlackConfigDALFactory } from "./project-slack-config-dal";
import { SlackTriggerFeature } from "./slack-types";
export const getCustomSlackBotManifest = () => {
const appCfg = getConfig();
return {
display_information: {
name: "Infisical",
description: "Get real-time Infisical updates in Slack",
background_color: "#c2d62b",
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
},
features: {
app_home: {
home_tab_enabled: false,
messages_tab_enabled: false,
messages_tab_read_only_enabled: true
},
bot_user: {
display_name: "Infisical",
always_online: true
}
},
oauth_config: {
redirect_urls: [`${appCfg.SITE_URL}/api/v1/workflow-integrations/slack/oauth_redirect`],
scopes: {
bot: ["chat:write.public", "chat:write", "channels:read", "groups:read", "im:read", "mpim:read"]
}
},
settings: {
org_deploy_enabled: false,
socket_mode_enabled: false,
token_rotation_enabled: false
}
};
};
import { SlackTriggerFeature, TSlackNotification } from "./slack-types";
export const fetchSlackChannels = async (botKey: string) => {
const slackChannels: {
@@ -80,23 +41,98 @@ export const fetchSlackChannels = async (botKey: string) => {
return slackChannels;
};
const buildSlackPayload = (notification: TSlackNotification) => {
const appCfg = getConfig();
switch (notification.type) {
case SlackTriggerFeature.SECRET_APPROVAL: {
const { payload } = notification;
const messageBody = `A secret approval request has been opened by ${payload.userEmail}.
*Environment*: ${payload.environment}
*Secret path*: ${payload.secretPath || "/"}
View the complete details <${appCfg.SITE_URL}/project/${payload.projectId}/approval?requestId=${
payload.requestId
}|here>.`;
const payloadBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "Secret approval request",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
}
}
];
return {
payloadMessage: messageBody,
payloadBlocks
};
}
case SlackTriggerFeature.ACCESS_REQUEST: {
const { payload } = notification;
const messageBody = `${payload.requesterFullName} (${payload.requesterEmail}) has requested ${
payload.isTemporary ? "temporary" : "permanent"
} access to ${payload.secretPath} in the ${payload.environment} environment of ${payload.projectName}.
The following permissions are requested: ${payload.permissions.join(", ")}
View the request and approve or deny it <${payload.approvalUrl}|here>.`;
const payloadBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "New access approval request pending for review",
emoji: true
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: messageBody
}
}
];
return {
payloadMessage: messageBody,
payloadBlocks
};
}
default: {
throw new BadRequestError({
message: "Slack notification type not supported."
});
}
}
};
export const triggerSlackNotification = async ({
projectId,
payloadBlocks,
payloadMessage,
notification,
projectSlackConfigDAL,
projectDAL,
kmsService,
feature
kmsService
}: {
projectId: string;
payloadBlocks: Block[];
payloadMessage: string;
notification: TSlackNotification;
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "getIntegrationDetailsByProject">;
projectDAL: Pick<TProjectDALFactory, "findById">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
feature: SlackTriggerFeature;
}) => {
const { payloadMessage, payloadBlocks } = buildSlackPayload(notification);
const project = await projectDAL.findById(projectId);
const slackIntegration = await projectSlackConfigDAL.getIntegrationDetailsByProject(project.id);
@@ -105,12 +141,12 @@ export const triggerSlackNotification = async ({
}
let targetChannelIds: string[] = [];
if (feature === SlackTriggerFeature.ACCESS_REQUEST) {
if (notification.type === SlackTriggerFeature.ACCESS_REQUEST) {
targetChannelIds = slackIntegration.accessRequestChannels?.split(", ") || [];
if (!targetChannelIds.length || !slackIntegration.isAccessRequestNotificationEnabled) {
return;
}
} else if (feature === SlackTriggerFeature.SECRET_APPROVAL) {
} else if (notification.type === SlackTriggerFeature.SECRET_APPROVAL) {
targetChannelIds = slackIntegration.secretRequestChannels?.split(", ") || [];
if (!targetChannelIds.length || !slackIntegration.isSecretRequestNotificationEnabled) {
return;
@@ -136,6 +172,6 @@ export const triggerSlackNotification = async ({
text: payloadMessage,
blocks: payloadBlocks
})
.catch((err) => void logger.error(err));
.catch((err) => logger.error(err));
}
};
+1 -1
View File
@@ -158,7 +158,7 @@ export const slackServiceFactory = ({
message: `Invalid Slack configuration. ${
appCfg.isCloud
? "Please contact the Infisical team."
: "Contact your instance admin to setup Slack integration in the Admin settings."
: "Contact your instance admin to setup Slack integration in the Admin settings. Your configuration is missing Slack client ID and secret."
}`
});
}
+25
View File
@@ -52,3 +52,28 @@ export enum SlackTriggerFeature {
SECRET_APPROVAL = "secret-approval",
ACCESS_REQUEST = "access-request"
}
export type TSlackNotification =
| {
type: SlackTriggerFeature.SECRET_APPROVAL;
payload: {
userEmail: string;
environment: string;
secretPath: string;
requestId: string;
projectId: string;
};
}
| {
type: SlackTriggerFeature.ACCESS_REQUEST;
payload: {
requesterFullName: string;
requesterEmail: string;
isTemporary: boolean;
secretPath: string;
environment: string;
projectName: string;
permissions: string[];
approvalUrl: string;
};
};
@@ -12,7 +12,6 @@ import { TAuthLoginFactory } from "../auth/auth-login-service";
import { AuthMethod } from "../auth/auth-type";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TOrgServiceFactory } from "../org/org-service";
import { getCustomSlackBotManifest } from "../slack/slack-fns";
import { TUserDALFactory } from "../user/user-dal";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import { LoginMethod, TAdminGetUsersDTO, TAdminSignUpDTO } from "./super-admin-types";
@@ -90,9 +89,7 @@ export const superAdminServiceFactory = ({
data: TSuperAdminUpdate & { slackClientId?: string; slackClientSecret?: string },
userId: string
) => {
const updatedData = {
...data
};
const updatedData = data;
if (data.enabledLoginMethods) {
const superAdminUser = await userDAL.findById(userId);
@@ -263,12 +260,6 @@ export const superAdminServiceFactory = ({
return user;
};
const getCustomSlackBotCreationUrl = async () => {
return `https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
JSON.stringify(getCustomSlackBotManifest())
)}`;
};
const getAdminSlackConfig = async () => {
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
@@ -301,7 +292,6 @@ export const superAdminServiceFactory = ({
adminSignUp,
getUsers,
deleteUser,
getCustomSlackBotCreationUrl,
getAdminSlackConfig
};
};
+1 -6
View File
@@ -4,9 +4,4 @@ export {
useUpdateAdminSlackConfig,
useUpdateServerConfig
} from "./mutation";
export {
useAdminGetUsers,
useGetAdminSlackConfig,
useGetCustomSlackAppCreationUrl,
useGetServerConfig
} from "./queries";
export { useAdminGetUsers, useGetAdminSlackConfig, useGetServerConfig } from "./queries";
-13
View File
@@ -12,7 +12,6 @@ export const adminStandaloneKeys = {
export const adminQueryKeys = {
serverConfig: () => ["server-config"] as const,
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getCustomSlackAppCreationUrl: () => ["custom-slack-app-creation-url"] as const,
getAdminSlackConfig: () => ["admin-slack-config"] as const
};
@@ -62,18 +61,6 @@ export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
});
};
export const useGetCustomSlackAppCreationUrl = () =>
useQuery({
queryKey: adminQueryKeys.getCustomSlackAppCreationUrl(),
queryFn: async () => {
const { data } = await apiRequest.get<string>(
"/api/v1/admin/integrations/slack/bot-creation-url"
);
return data;
}
});
export const useGetAdminSlackConfig = () =>
useQuery({
queryKey: adminQueryKeys.getAdminSlackConfig(),
@@ -121,7 +121,11 @@ export const SlackIntegrationForm = ({ id, onClose }: Props) => {
</FormControl>
)}
<div className="mt-6 flex items-center space-x-4">
<Button type="submit" isLoading={isSubmitting || isConnectLoading} isDisabled={!isDirty}>
<Button
type="submit"
isLoading={isSubmitting || isConnectLoading}
isDisabled={!isDirty || isConnectLoading || isSubmitting}
>
{slackIntegration ? "Save" : "Connect Slack"}
</Button>
<Button variant="outline_bg" onClick={onClose}>
@@ -5,11 +5,7 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import {
useGetAdminSlackConfig,
useGetCustomSlackAppCreationUrl,
useUpdateServerConfig
} from "@app/hooks/api";
import { useGetAdminSlackConfig, useUpdateServerConfig } from "@app/hooks/api";
const slackFormSchema = z.object({
clientId: z.string(),
@@ -18,6 +14,45 @@ const slackFormSchema = z.object({
type TSlackForm = z.infer<typeof slackFormSchema>;
const getCustomSlackAppCreationUrl = () =>
`https://api.slack.com/apps?new_app=1&manifest_json=${encodeURIComponent(
JSON.stringify({
display_information: {
name: "Infisical",
description: "Get real-time Infisical updates in Slack",
background_color: "#c2d62b",
long_description: `This Slack application is designed specifically for use with your self-hosted Infisical instance, allowing seamless integration between your Infisical projects and your Slack workspace. With this integration, your team can stay up-to-date with the latest events, changes, and notifications directly inside Slack.
- Notifications: Receive real-time updates and alerts about critical events in your Infisical projects. Whether it's a new project being created, updates to secrets, or changes to your team's configuration, you will be promptly notified within the designated Slack channels of your choice.
- Customization: Tailor the notifications to your team's specific needs by configuring which types of events trigger alerts and in which channels they are sent.
- Collaboration: Keep your entire team in the loop with notifications that help facilitate more efficient collaboration by ensuring that everyone is aware of important developments in your Infisical projects.
By integrating Infisical with Slack, you can enhance your workflow by combining the power of secure secrets management with the communication capabilities of Slack.`
},
features: {
app_home: {
home_tab_enabled: false,
messages_tab_enabled: false,
messages_tab_read_only_enabled: true
},
bot_user: {
display_name: "Infisical",
always_online: true
}
},
oauth_config: {
redirect_urls: [`${window.origin}/api/v1/workflow-integrations/slack/oauth_redirect`],
scopes: {
bot: ["chat:write.public", "chat:write"]
}
},
settings: {
org_deploy_enabled: false,
socket_mode_enabled: false,
token_rotation_enabled: false
}
})
)}`;
export const IntegrationPanel = () => {
const {
control,
@@ -28,9 +63,7 @@ export const IntegrationPanel = () => {
resolver: zodResolver(slackFormSchema)
});
const { data: customSlackAppCreationUrl } = useGetCustomSlackAppCreationUrl();
const { data: adminSlackConfig } = useGetAdminSlackConfig();
const { mutateAsync: updateAdminServerConfig } = useUpdateServerConfig();
useEffect(() => {
@@ -63,7 +96,10 @@ export const IntegrationPanel = () => {
Step 1: Create your Infisical Slack App
</div>
<div className="mb-6">
<Button colorSchema="secondary" onClick={() => window.open(customSlackAppCreationUrl)}>
<Button
colorSchema="secondary"
onClick={() => window.open(getCustomSlackAppCreationUrl())}
>
Create Slack App
</Button>
</div>