From 0fbf8efd3a3ff0301ea18ec275956ebf839eb788 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Tue, 3 Jun 2025 14:36:47 -0700 Subject: [PATCH 1/9] improvement: add filter by roles to org users table --- .../OrgMembersSection/OrgMembersTable.tsx | 112 ++++++++++++++++-- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index e0350741e..1ad9cfdd1 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -1,8 +1,11 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { faArrowDown, faArrowUp, + faCheckCircle, + faChevronRight, faEllipsis, + faFilter, faMagnifyingGlass, faSearch, faUsers @@ -19,7 +22,11 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuTrigger, + DropdownSubMenu, + DropdownSubMenuContent, + DropdownSubMenuTrigger, EmptyState, IconButton, Input, @@ -75,6 +82,10 @@ enum OrgMembersOrderBy { Email = "email" } +type Filter = { + roles: string[]; +}; + export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => { const navigate = useNavigate(); const { subscription } = useSubscription(); @@ -184,17 +195,29 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro setUserTablePreference("orgMembersTable", PreferenceKey.PerPage, newPerPage); }; + const [filter, setFilter] = useState({ + roles: [] + }); + const filteredUsers = useMemo( () => members - ?.filter( - ({ user: u, inviteEmail }) => + ?.filter(({ user: u, inviteEmail, role, roleId }) => { + if ( + filter.roles.length && + !filter.roles.includes(role === "custom" ? findRoleFromId(roleId)!.slug : role) + ) { + return false; + } + + return ( u?.firstName?.toLowerCase().includes(search.toLowerCase()) || u?.lastName?.toLowerCase().includes(search.toLowerCase()) || u?.username?.toLowerCase().includes(search.toLowerCase()) || u?.email?.toLowerCase().includes(search.toLowerCase()) || inviteEmail?.toLowerCase().includes(search.toLowerCase()) - ) + ); + }) .sort((a, b) => { const [memberOne, memberTwo] = orderDirection === OrderByDirection.ASC ? [a, b] : [b, a]; @@ -217,7 +240,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro return valueOne.toLowerCase().localeCompare(valueTwo.toLowerCase()); }), - [members, search, orderDirection, orderBy] + [members, search, orderDirection, orderBy, filter] ); const handleSort = (column: OrgMembersOrderBy) => { @@ -236,14 +259,81 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Pro setPage }); + const handleRoleToggle = useCallback( + (roleSlug: string) => + setFilter((state) => { + const currentRoles = state.roles || []; + + if (currentRoles.includes(roleSlug)) { + return { ...state, roles: currentRoles.filter((role) => role !== roleSlug) }; + } + return { ...state, roles: [...currentRoles, roleSlug] }; + }), + [] + ); + + const isTableFiltered = Boolean(filter.roles.length); + return (
- setSearch(e.target.value)} - leftIcon={} - placeholder="Search members..." - /> +
+ + + + + + + + Filter By + + } + > + Roles + + + + Apply Roles to Filter Users + + {roles?.map(({ id, slug, name }) => ( + { + evt.preventDefault(); + handleRoleToggle(slug); + }} + key={id} + icon={filter.roles.includes(slug) && } + iconPos="right" + > +
+
+ {name} +
+ + ))} + + + + + setSearch(e.target.value)} + leftIcon={} + placeholder="Search members..." + /> +
From 698260cba67d4822f2900a63b46857e195e8eb9a Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 4 Jun 2025 13:27:08 -0700 Subject: [PATCH 2/9] improvement: add more aggresive rate limiting on smtp endpoints --- backend/src/server/config/rateLimiter.ts | 11 ++++++++++- backend/src/server/routes/v1/invite-org-router.ts | 8 +++++--- backend/src/server/routes/v1/org-admin-router.ts | 8 +++++--- backend/src/server/routes/v1/password-router.ts | 14 ++++++++++---- backend/src/server/routes/v2/user-router.ts | 6 ++++-- backend/src/server/routes/v3/signup-router.ts | 6 ++++-- frontend/src/components/auth/CodeInputStep.tsx | 13 ++++++++----- .../pages/auth/VerifyEmailPage/VerifyEmailPage.tsx | 8 ++++++-- .../OrgMembersSection/AddOrgMemberModal.tsx | 1 + .../MembersTab/components/AddMemberModal.tsx | 1 + 10 files changed, 54 insertions(+), 22 deletions(-) diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 7b4b9a99b..ab2406266 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -11,7 +11,7 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => { return { errorResponseBuilder: (_, context) => { throw new RateLimitError({ - message: `Rate limit exceeded. Please try again in ${context.after}` + message: `Rate limit exceeded. Please try again in ${Math.ceil(context.ttl / 1000)} seconds` }); }, timeWindow: 60 * 1000, @@ -113,3 +113,12 @@ export const requestAccessLimit: RateLimitOptions = { max: 10, keyGenerator: (req) => req.realIp }; + +export const smtpRateLimit = ({ + keyGenerator = (req) => req.realIp +}: Pick = {}): RateLimitOptions => ({ + timeWindow: 20 * 1000, + hook: "preValidation", + max: 1, + keyGenerator +}); diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 77ae0e627..117532703 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas"; -import { inviteUserRateLimit } from "@app/server/config/rateLimiter"; +import { inviteUserRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -11,7 +11,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { server.route({ url: "/signup", config: { - rateLimit: inviteUserRateLimit + rateLimit: smtpRateLimit() }, method: "POST", schema: { @@ -81,7 +81,9 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { server.route({ url: "/signup-resend", config: { - rateLimit: inviteUserRateLimit + rateLimit: smtpRateLimit({ + keyGenerator: (req) => (req.body as { membershipId: string }).membershipId + }) }, method: "POST", schema: { diff --git a/backend/src/server/routes/v1/org-admin-router.ts b/backend/src/server/routes/v1/org-admin-router.ts index cc0543d4c..d4b1ee188 100644 --- a/backend/src/server/routes/v1/org-admin-router.ts +++ b/backend/src/server/routes/v1/org-admin-router.ts @@ -2,9 +2,9 @@ import { z } from "zod"; import { ProjectMembershipsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; -import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { readLimit, smtpRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { SanitizedProjectSchema } from "../sanitizedSchemas"; @@ -47,7 +47,9 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/projects/:projectId/grant-admin-access", config: { - rateLimit: writeLimit + rateLimit: smtpRateLimit({ + keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp) + }) }, schema: { params: z.object({ diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 724468e02..32921087b 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -2,10 +2,10 @@ import { z } from "zod"; import { BackupPrivateKeySchema, UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; -import { authRateLimit } from "@app/server/config/rateLimiter"; +import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; -import { AuthMode } from "@app/services/auth/auth-type"; +import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { UserEncryption } from "@app/services/user/user-types"; export const registerPasswordRouter = async (server: FastifyZodProvider) => { @@ -80,7 +80,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/email/password-reset", config: { - rateLimit: authRateLimit + rateLimit: smtpRateLimit({ + keyGenerator: (req) => (req.body as { email: string }).email + }) }, schema: { body: z.object({ @@ -224,7 +226,9 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/email/password-setup", config: { - rateLimit: authRateLimit + rateLimit: smtpRateLimit({ + keyGenerator: (req) => (req.auth.actor === ActorType.USER ? req.auth.userId : req.realIp) + }) }, schema: { response: { @@ -233,6 +237,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { }) } }, + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { await server.services.password.sendPasswordSetupEmail(req.permission); @@ -267,6 +272,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { }) } }, + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req, res) => { await server.services.password.setupPassword(req.body, req.permission); diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 027f527fc..730d34bb9 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { AuthTokenSessionsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { ApiKeysSchema } from "@app/db/schemas/api-keys"; -import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { authRateLimit, readLimit, smtpRateLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMethod, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; import { sanitizedOrganizationSchema } from "@app/services/org/org-schema"; @@ -12,7 +12,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { method: "POST", url: "/me/emails/code", config: { - rateLimit: authRateLimit + rateLimit: smtpRateLimit({ + keyGenerator: (req) => (req.body as { username: string }).username + }) }, schema: { body: z.object({ diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 552253cde..275836b1e 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -3,7 +3,7 @@ import { z } from "zod"; import { UsersSchema } from "@app/db/schemas"; import { getConfig } from "@app/lib/config/env"; import { ForbiddenRequestError } from "@app/lib/errors"; -import { authRateLimit } from "@app/server/config/rateLimiter"; +import { authRateLimit, smtpRateLimit } from "@app/server/config/rateLimiter"; import { GenericResourceNameSchema } from "@app/server/lib/schemas"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -13,7 +13,9 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { url: "/email/signup", method: "POST", config: { - rateLimit: authRateLimit + rateLimit: smtpRateLimit({ + keyGenerator: (req) => (req.body as { email: string }).email + }) }, schema: { body: z.object({ diff --git a/frontend/src/components/auth/CodeInputStep.tsx b/frontend/src/components/auth/CodeInputStep.tsx index 09958fafd..f992c8da6 100644 --- a/frontend/src/components/auth/CodeInputStep.tsx +++ b/frontend/src/components/auth/CodeInputStep.tsx @@ -78,11 +78,14 @@ export default function CodeInputStep({ const resendVerificationEmail = async () => { setIsResendingVerificationEmail(true); setIsLoading(true); - await mutateAsync({ email }); - setTimeout(() => { - setIsLoading(false); - setIsResendingVerificationEmail(false); - }, 2000); + try { + await mutateAsync({ email }); + } finally { + setTimeout(() => { + setIsLoading(false); + setIsResendingVerificationEmail(false); + }, 1000); + } }; return ( diff --git a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx index 47012e2cb..9777f564a 100644 --- a/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx +++ b/frontend/src/pages/auth/VerifyEmailPage/VerifyEmailPage.tsx @@ -22,8 +22,12 @@ export const VerifyEmailPage = () => { */ const sendVerificationEmail = async () => { if (email) { - await mutateAsync({ email }); - setStep(2); + try { + await mutateAsync({ email }); + setStep(2); + } catch { + setLoading(false); + } } }; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx index f9c454985..ec104d391 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/AddOrgMemberModal.tsx @@ -159,6 +159,7 @@ export const AddOrgMemberModal = ({ text: "Failed to invite user to org", type: "error" }); + return; } if (serverDetails?.emailConfigured) { diff --git a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx index d50baf135..811bf57e7 100644 --- a/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/pages/project/AccessControlPage/components/MembersTab/components/AddMemberModal.tsx @@ -163,6 +163,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { text: "Failed to add user to project", type: "error" }); + return; } handlePopUpToggle("addMember", false); reset(); From 54435d0ad96a690cf389066930530d3d16da86e2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 4 Jun 2025 14:21:36 -0700 Subject: [PATCH 3/9] improvements: prevent comma separated value usage with eq and neq checks --- .../ProjectRoleModifySection.utils.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index 6cef42f12..c986be37b 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -226,6 +226,23 @@ const ConditionSchema = z : el.rhs.trim().startsWith("/") ), { message: "Invalid Secret Path. Must start with '/'" } + ) + .refine( + (val) => + val + .filter((el) => el.operator === PermissionConditionOperators.$EQ) + .every((el) => !el.rhs.includes(",")), + { message: '"Equal" checks cannot contain comma separated values. Use "IN" operator instead.' } + ) + .refine( + (val) => + val + .filter((el) => el.operator === PermissionConditionOperators.$NEQ) + .every((el) => !el.rhs.includes(",")), + { + message: + '"Not Equal" checks cannot contain comma separated values. Use "IN" operator with "Forbid" instead.' + } ); export const projectRoleFormSchema = z.object({ From 696bbcb07277e644151d3f52140c1fe7f81e58cd Mon Sep 17 00:00:00 2001 From: = Date: Thu, 5 Jun 2025 03:44:54 +0530 Subject: [PATCH 4/9] feat: updated ui for replication approval --- frontend/src/lib/fn/string.ts | 5 + .../SecretApprovalRequest.tsx | 9 +- .../SecretApprovalRequestChangeItem.tsx | 73 ++++----- .../SecretApprovalRequestChanges.tsx | 138 ++++++++++++++---- 4 files changed, 149 insertions(+), 76 deletions(-) diff --git a/frontend/src/lib/fn/string.ts b/frontend/src/lib/fn/string.ts index 6b2842701..a733308b7 100644 --- a/frontend/src/lib/fn/string.ts +++ b/frontend/src/lib/fn/string.ts @@ -8,6 +8,11 @@ export const formatReservedPaths = (secretPath: string) => { return secretPath; }; +export const parsePathFromReplicatedPath = (secretPath: string) => { + const i = secretPath.indexOf(ReservedFolders.SecretReplication); + return secretPath.slice(0, i); +}; + export const camelCaseToSpaces = (input: string) => { return input.replace(/([a-z])([A-Z])/g, "$1 $2"); }; diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx index 1de35b528..ca0e598a2 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/SecretApprovalRequest.tsx @@ -224,8 +224,7 @@ export const SecretApprovalRequest = () => { createdAt, reviewers, status, - committerUser, - isReplicated: isReplication + committerUser } = secretApproval; const isReviewed = reviewers.some( ({ status: reviewStatus, userId }) => @@ -244,13 +243,15 @@ export const SecretApprovalRequest = () => { >
- {generateCommitText(commits)} + {secretApproval.isReplicated + ? `${commits.length} secret pending import` + : generateCommitText(commits)} #{secretApproval.slug}
Opened {formatDistance(new Date(createdAt), new Date())} ago by{" "} {committerUser?.firstName || ""} {committerUser?.lastName || ""} ( - {committerUser?.email}){isReplication && " via replication"} + {committerUser?.email}) {!isReviewed && status === "open" && " - Review required"} diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index 43a729bd8..806cda25d 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -12,7 +12,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { Tag, Tooltip } from "@app/components/v2"; +import { SecretInput, Tag, Tooltip } from "@app/components/v2"; import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/hooks/api/types"; export type Props = { @@ -86,10 +86,10 @@ export const SecretApprovalRequestChangeItem = ({ {op === CommitType.UPDATE || op === CommitType.DELETE ? (
- Legacy Secret + Previous Secret
- Deprecated + Previous
@@ -104,27 +104,22 @@ export const SecretApprovalRequestChangeItem = ({ Rotated Secret value will not be affected ) : ( -
setIsOldSecretValueVisible(!isOldSecretValueVisible)} - className="relative flex max-w-[100vh] flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 px-2" - > +
+
setIsOldSecretValueVisible(!isOldSecretValueVisible)} > - {isOldSecretValueVisible - ? secretVersion?.secretValue || "EMPTY" - : secretVersion?.secretValue - ? secretVersion?.secretValue?.split("").map(() => "•") - : "EMPTY"}{" "} +
- {secretVersion?.secretValue && ( -
- -
- )}
)}
@@ -191,8 +186,7 @@ export const SecretApprovalRequestChangeItem = ({
) : (
- {" "} - Secret not existent in the previous version. + Secret did not exist in the previous version.
)} {op === CommitType.UPDATE || op === CommitType.CREATE ? ( @@ -216,27 +210,22 @@ export const SecretApprovalRequestChangeItem = ({ Rotated Secret value will not be affected ) : ( -
setIsNewSecretValueVisible(!isNewSecretValueVisible)} - className="relative flex max-w-[100vh] flex-row items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-900 px-2" - > +
+
setIsNewSecretValueVisible(!isNewSecretValueVisible)} > - {isNewSecretValueVisible - ? newVersion?.secretValue || "EMPTY" - : newVersion?.secretValue - ? newVersion?.secretValue?.split("").map(() => "•") - : "EMPTY"}{" "} +
- {newVersion?.secretValue && ( -
- -
- )}
)}
@@ -302,7 +291,7 @@ export const SecretApprovalRequestChangeItem = ({ ) : (
{" "} - Secret not existent in the new version. + Secret did not exist in the previous version.
)}
diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 218365ae9..940d11fdb 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -30,19 +30,24 @@ import { TextArea, Tooltip } from "@app/components/v2"; -import { useUser } from "@app/context"; +import { useUser, useWorkspace } from "@app/context"; import { usePopUp } from "@app/hooks"; import { useGetSecretApprovalRequestDetails, + useGetSecretImports, useUpdateSecretApprovalReviewStatus } from "@app/hooks/api"; import { ApprovalStatus, CommitType } from "@app/hooks/api/types"; -import { formatReservedPaths } from "@app/lib/fn/string"; +import { formatReservedPaths, parsePathFromReplicatedPath } from "@app/lib/fn/string"; import { SecretApprovalRequestAction } from "./SecretApprovalRequestAction"; import { SecretApprovalRequestChangeItem } from "./SecretApprovalRequestChangeItem"; -export const generateCommitText = (commits: { op: CommitType }[] = []) => { +export const generateCommitText = (commits: { op: CommitType }[] = [], isReplicated = false) => { + if (isReplicated) { + return {commits.length} secret pending import; + } + const score: Record = {}; commits.forEach(({ op }) => { score[op] = (score?.[op] || 0) + 1; @@ -74,7 +79,6 @@ export const generateCommitText = (commits: { op: CommitType }[] = []) => { deleted ); - return text; }; @@ -105,6 +109,7 @@ export const SecretApprovalRequestChanges = ({ workspaceId }: Props) => { const { user: userSession } = useUser(); + const { currentWorkspace } = useWorkspace(); const { data: secretApprovalRequestDetails, isSuccess: isSecretApprovalRequestSuccess, @@ -112,6 +117,20 @@ export const SecretApprovalRequestChanges = ({ } = useGetSecretApprovalRequestDetails({ id: approvalRequestId }); + const approvalSecretPath = parsePathFromReplicatedPath( + secretApprovalRequestDetails?.secretPath || "" + ); + const { data: secretImports } = useGetSecretImports({ + environment: secretApprovalRequestDetails?.environment || "", + projectId: currentWorkspace.id, + path: approvalSecretPath + }); + + const replicatedImport = secretApprovalRequestDetails?.isReplicated + ? secretImports?.find( + (el) => secretApprovalRequestDetails?.secretPath?.includes(el.id) && el.isReplication + ) + : undefined; const { mutateAsync: updateSecretApprovalRequestStatus, @@ -226,34 +245,16 @@ export const SecretApprovalRequestChanges = ({ : secretApprovalRequestDetails.status} -
-
- {generateCommitText(secretApprovalRequestDetails.commits)} - {secretApprovalRequestDetails.isReplicated && ( - (replication) +
+
+ {generateCommitText( + secretApprovalRequestDetails.commits, + secretApprovalRequestDetails.isReplicated )}
-
-

- {secretApprovalRequestDetails?.committerUser?.firstName || ""} - {secretApprovalRequestDetails?.committerUser?.lastName || ""} ( - {secretApprovalRequestDetails?.committerUser?.email}) wants to change{" "} - {secretApprovalRequestDetails.commits.length} secret values in -

-

- {secretApprovalRequestDetails.environment} -

-
-

- -

-

- {formatReservedPaths(secretApprovalRequestDetails.secretPath)} -

-
+
+ By {secretApprovalRequestDetails?.committerUser?.firstName} ( + {secretApprovalRequestDetails?.committerUser?.email})
{!hasMerged && @@ -367,10 +368,87 @@ export const SecretApprovalRequestChanges = ({ )}
+
+
+ {secretApprovalRequestDetails.isReplicated ? ( +
+ A secret import in +

+ {secretApprovalRequestDetails?.environment} +

+
+

+ +

+ +

+ {approvalSecretPath} +

+
+
+ has pending changes to be accepted from its source at{" "} +

+ {replicatedImport?.importEnv?.slug} +

+
+

+ +

+ +

+ {replicatedImport?.importPath} +

+
+
+ . Approving these changes will add them to that import. +
+ ) : ( +
+

Secret(s) in

+

+ {secretApprovalRequestDetails?.environment} +

+
+

+ +

+ +

+ {formatReservedPaths(secretApprovalRequestDetails.secretPath)} +

+
+
+

+ have pending changes. Approving these changes will add them to that environment + and path. +

+
+ )} +
+
{secretApprovalRequestDetails.commits.map( ({ op, secretVersion, secret, ...newVersion }, index) => ( Date: Thu, 5 Jun 2025 03:54:09 +0530 Subject: [PATCH 5/9] feat: small text changes --- .../components/SecretApprovalRequestChangeItem.tsx | 2 +- .../components/SecretApprovalRequestChanges.tsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index 806cda25d..60e916fdb 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -195,7 +195,7 @@ export const SecretApprovalRequestChangeItem = ({ New Secret
- Current + New
diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx index 940d11fdb..89b427bb3 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChanges.tsx @@ -448,7 +448,6 @@ export const SecretApprovalRequestChanges = ({ {secretApprovalRequestDetails.commits.map( ({ op, secretVersion, secret, ...newVersion }, index) => ( Date: Wed, 4 Jun 2025 19:05:22 -0700 Subject: [PATCH 6/9] improvements: address feedback --- backend/src/server/config/rateLimiter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index ab2406266..d3d3d3efd 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -117,8 +117,8 @@ export const requestAccessLimit: RateLimitOptions = { export const smtpRateLimit = ({ keyGenerator = (req) => req.realIp }: Pick = {}): RateLimitOptions => ({ - timeWindow: 20 * 1000, + timeWindow: 40 * 1000, hook: "preValidation", - max: 1, + max: 2, keyGenerator }); From 135f425fcfad227a83543dc2e2d0416f45120cf7 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 4 Jun 2025 20:00:53 -0700 Subject: [PATCH 7/9] improvement: trim and substring keys and default to realIp --- backend/src/server/routes/v1/invite-org-router.ts | 3 ++- backend/src/server/routes/v1/password-router.ts | 2 +- backend/src/server/routes/v2/user-router.ts | 2 +- backend/src/server/routes/v3/signup-router.ts | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 117532703..525d51913 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -82,7 +82,8 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { url: "/signup-resend", config: { rateLimit: smtpRateLimit({ - keyGenerator: (req) => (req.body as { membershipId: string }).membershipId + keyGenerator: (req) => + (req.body as { membershipId?: string })?.membershipId?.trim().substring(0, 100) ?? req.realIp }) }, method: "POST", diff --git a/backend/src/server/routes/v1/password-router.ts b/backend/src/server/routes/v1/password-router.ts index 32921087b..eeb730f29 100644 --- a/backend/src/server/routes/v1/password-router.ts +++ b/backend/src/server/routes/v1/password-router.ts @@ -81,7 +81,7 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => { url: "/email/password-reset", config: { rateLimit: smtpRateLimit({ - keyGenerator: (req) => (req.body as { email: string }).email + keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp }) }, schema: { diff --git a/backend/src/server/routes/v2/user-router.ts b/backend/src/server/routes/v2/user-router.ts index 730d34bb9..bbd566334 100644 --- a/backend/src/server/routes/v2/user-router.ts +++ b/backend/src/server/routes/v2/user-router.ts @@ -13,7 +13,7 @@ export const registerUserRouter = async (server: FastifyZodProvider) => { url: "/me/emails/code", config: { rateLimit: smtpRateLimit({ - keyGenerator: (req) => (req.body as { username: string }).username + keyGenerator: (req) => (req.body as { username?: string })?.username?.trim().substring(0, 100) ?? req.realIp }) }, schema: { diff --git a/backend/src/server/routes/v3/signup-router.ts b/backend/src/server/routes/v3/signup-router.ts index 275836b1e..c249e7dbe 100644 --- a/backend/src/server/routes/v3/signup-router.ts +++ b/backend/src/server/routes/v3/signup-router.ts @@ -14,7 +14,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => { method: "POST", config: { rateLimit: smtpRateLimit({ - keyGenerator: (req) => (req.body as { email: string }).email + keyGenerator: (req) => (req.body as { email?: string })?.email?.trim().substring(0, 100) ?? req.realIp }) }, schema: { From 93ba6f7b588dc66d677cb0b817a4431865f71e1b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 5 Jun 2025 01:18:21 -0400 Subject: [PATCH 8/9] add netowkring docs --- .../platform/gateways/gateway-security.mdx | 19 -- .../platform/gateways/networking.mdx | 168 ++++++++++++++++++ .../platform/gateways/overview.mdx | 2 +- docs/documentation/setup/networking.mdx | 49 ++--- docs/mint.json | 3 +- 5 files changed, 197 insertions(+), 44 deletions(-) create mode 100644 docs/documentation/platform/gateways/networking.mdx diff --git a/docs/documentation/platform/gateways/gateway-security.mdx b/docs/documentation/platform/gateways/gateway-security.mdx index 83490fd4d..93a7f662f 100644 --- a/docs/documentation/platform/gateways/gateway-security.mdx +++ b/docs/documentation/platform/gateways/gateway-security.mdx @@ -89,22 +89,3 @@ The relay system provides secure tunneling: - Gateways only accept connections to approved resources - Each connection requires explicit project authorization - Resources remain private to their assigned organization - -## Security Measures - -### Certificate Lifecycle -- Certificates have limited validity periods -- Automatic certificate rotation -- Immediate certificate revocation capabilities - -### Monitoring and Verification -1. **Continuous Verification**: - - Regular heartbeat checks - - Certificate chain validation - - Connection state monitoring - -2. **Security Controls**: - - Automatic connection termination on verification failure - - Audit logging of all access attempts - - Machine identity based authentication - diff --git a/docs/documentation/platform/gateways/networking.mdx b/docs/documentation/platform/gateways/networking.mdx new file mode 100644 index 000000000..2e512bf8b --- /dev/null +++ b/docs/documentation/platform/gateways/networking.mdx @@ -0,0 +1,168 @@ +--- +title: "Networking" +description: "Network configuration and firewall requirements for Infisical Gateway" +--- + +The Infisical Gateway requires outbound network connectivity to establish secure communication with Infisical's relay infrastructure. +This page outlines the required ports, protocols, and firewall configurations needed for optimal gateway usage. + +## Network Architecture + +The gateway uses a relay-based architecture to establish secure connections: + +1. **Gateway** connects outbound to **Relay Servers** using UDP/QUIC protocol +2. **Relay Servers** facilitate secure communication between Gateway and Infisical Cloud +3. All traffic is end-to-end encrypted using mutual TLS over QUIC + +## Required Network Connectivity + +### Outbound Connections (Required) + +The gateway requires the following outbound connectivity: + +| Protocol | Destination | Ports | Purpose | +|----------|-------------|-------|---------| +| UDP | Relay Servers | 49152-65535 | Allocated relay communication (TLS) | +| TCP | app.infisical.com / eu.infisical.com | 443 | API communication and relay allocation | + +### Relay Server IP Addresses + +Your firewall must allow outbound connectivity to the following Infisical relay servers on dynamically allocated ports. + + + + ``` + 54.235.197.91:49152-65535 + 18.215.196.229:49152-65535 + 3.222.120.233:49152-65535 + 34.196.115.157:49152-65535 + ``` + + + ``` + 3.125.237.40:49152-65535 + 52.28.157.98:49152-65535 + 3.125.176.90:49152-65535 + ``` + + + Please contact your Infisical account manager for dedicated relay server IP addresses. + + + + + These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice. + + +## Protocol Details + +### QUIC over UDP + +The gateway uses QUIC (Quick UDP Internet Connections) for primary communication: + +- **Port 5349**: STUN/TURN over TLS (secure relay communication) +- **Built-in features**: Connection migration, multiplexing, reduced latency +- **Encryption**: TLS 1.3 with certificate pinning + +## Understanding Firewall Behavior with UDP + +Unlike TCP connections, UDP is a stateless protocol, and depending on your organization's firewall configuration, you may need to adjust network rules accordingly. +When the gateway sends UDP packets to a relay server, the return responses need to be allowed back through the firewall. +Modern firewalls handle this through "connection tracking" (also called "stateful inspection"), but the behavior can vary depending on your firewall configuration. + + +### Connection Tracking + +Modern firewalls automatically track UDP connections and allow return responses. This is the preferred configuration as it: +- Automatically handles return responses +- Reduces firewall rule complexity +- Avoids the need for manual IP whitelisting + +In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicity define return traffic manually. + +## Common Network Scenarios + +### Corporate Firewalls + +For corporate environments with strict egress filtering: + +1. **Whitelist relay IP addresses** (listed above) +2. **Allow UDP port 5349** outbound +3. **Configure connection tracking** for UDP return traffic +4. **Allow ephemeral port range** 49152-65535 for return traffic if connection tracking is disabled + +### Cloud Environments (AWS/GCP/Azure) + +Configure security groups to allow: +- **Outbound UDP** to relay IPs on port 5349 +- **Outbound HTTPS** to api.infisical.com +- **Inbound UDP** on ephemeral ports (if not using stateful rules) + +## Frequently Asked Questions + + +The gateway is designed to handle network interruptions gracefully: + +- **Automatic reconnection**: The gateway will automatically attempt to reconnect to relay servers every 5 seconds if the connection is lost +- **Connection retry logic**: Built-in retry mechanisms handle temporary network outages without manual intervention +- **Multiple relay servers**: If one relay server is unavailable, the gateway can connect to alternative relay servers +- **Persistent sessions**: Existing connections are maintained where possible during brief network interruptions +- **Graceful degradation**: The gateway logs connection issues and continues attempting to restore connectivity + +No manual intervention is typically required during network interruptions. + + + +QUIC (Quick UDP Internet Connections) provides several advantages over traditional TCP for gateway communication: + +- **Faster connection establishment**: QUIC combines transport and security handshakes, reducing connection setup time +- **Built-in encryption**: TLS 1.3 is integrated into the protocol, ensuring all traffic is encrypted by default +- **Connection migration**: QUIC connections can survive IP address changes (useful for NAT rebinding) +- **Reduced head-of-line blocking**: Multiple data streams can be multiplexed without blocking each other +- **Better performance over unreliable networks**: Advanced congestion control and packet loss recovery +- **Lower latency**: Optimized for real-time communication between gateway and cloud services + +While TCP is stateful and easier for firewalls to track, QUIC's performance benefits outweigh the additional firewall configuration requirements. + + + +No inbound ports need to be opened. The gateway only makes outbound connections: + +- **Outbound UDP** to relay servers on ports 49152-65535 +- **Outbound HTTPS** to Infisical API endpoints +- **Return responses** are handled by connection tracking or explicit IP whitelisting + +This design maintains security by avoiding the need for inbound firewall rules that could expose your network to external threats. + + + +If your firewall has strict UDP restrictions: + +1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses +2. **Use explicit IP whitelisting** (Option 2) if connection tracking is disabled +3. **Consider network policy exceptions** for the gateway host +4. **Monitor firewall logs** to identify which specific rules are blocking traffic + +The gateway requires UDP connectivity to function - TCP-only configurations are not supported. + + + +The gateway connects to **one relay server at a time**: + +- **Single active connection**: Only one relay connection is established per gateway instance +- **Automatic failover**: If the current relay becomes unavailable, the gateway will connect to an alternative relay +- **Load distribution**: Different gateway instances may connect to different relay servers for load balancing +- **No manual selection**: The Infisical API automatically assigns the optimal relay server based on availability and proximity + +You should whitelist all relay IP addresses to ensure proper failover functionality. + + +No, relay servers cannot decrypt any traffic passing through them: + +- **End-to-end encryption**: All traffic between the gateway and Infisical Cloud is encrypted using mutual TLS with certificate pinning +- **Relay acts as a tunnel**: The relay server only forwards encrypted packets - it has no access to encryption keys +- **No data storage**: Relay servers do not store any traffic or network-identifiable information +- **Certificate isolation**: Each organization has its own private PKI system, ensuring complete tenant isolation + +The relay infrastructure is designed as a secure forwarding mechanism, similar to a VPN tunnel, where the relay provider cannot see the contents of the traffic flowing through it. + \ No newline at end of file diff --git a/docs/documentation/platform/gateways/overview.mdx b/docs/documentation/platform/gateways/overview.mdx index ae4a3c7ad..53df5993b 100644 --- a/docs/documentation/platform/gateways/overview.mdx +++ b/docs/documentation/platform/gateways/overview.mdx @@ -32,7 +32,7 @@ For detailed installation instructions, refer to the Infisical [CLI Installation To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway. Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable. -### Deployment process +### Get started diff --git a/docs/documentation/setup/networking.mdx b/docs/documentation/setup/networking.mdx index 4a666b73c..6de27c3c0 100644 --- a/docs/documentation/setup/networking.mdx +++ b/docs/documentation/setup/networking.mdx @@ -4,33 +4,36 @@ sidebarTitle: "Networking" description: "Network configuration details for Infisical Cloud" --- -## Overview - When integrating your infrastructure with Infisical Cloud, you may need to configure network access controls. This page provides the IP addresses that Infisical uses to communicate with your services. -## Egress IP Addresses +## Infisical IP Addresses -Infisical Cloud operates from two regions: US and EU. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the egress IPs Infisical uses when making outbound requests to your services. +Infisical Cloud operates from multiple regions. If your infrastructure has strict network policies, you may need to allow traffic from Infisical by adding the following IP addresses to your ingress rules. These are the IP addresses that Infisical uses when making outbound requests to your services. -### US Region + + + ``` + 3.213.63.16 + 54.164.68.7 + ``` + + + + ``` + 3.77.89.19 + 3.125.209.189 + ``` + + + + For dedicated Infisical deployments, please contact your account manager for the specific IP addresses used in your dedicated environment. + + -To allow connections from Infisical US, add these IP addresses to your ingress rules: + +These IP addresses are static and managed by Infisical. Any changes will be communicated with 60-day advance notice. + -- `3.213.63.16` -- `54.164.68.7` +## What These IP Addresses Are Used For -### EU Region - -To allow connections from Infisical EU, add these IP addresses to your ingress rules: - -- `3.77.89.19` -- `3.125.209.189` - -## Common Use Cases - -You may need to allow Infisical’s egress IPs if your services require inbound connections for: - -- Secret rotation - When Infisical needs to send requests to your systems to automatically rotate credentials -- Dynamic secrets - When Infisical generates and manages temporary credentials for your cloud services -- Secret integrations - When syncing secrets with third-party services like Azure Key Vault -- Native authentication with machine identities - When using methods like Kubernetes authentication +These IP addresses represent the source IPs you'll see when Infisical Cloud makes connections to your infrastructure. All outbound traffic from Infisical Cloud originates from these IP addresses, ensuring predictable source IP addresses for your firewall rules. diff --git a/docs/mint.json b/docs/mint.json index 67a771085..64b827f55 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -233,7 +233,8 @@ "group": "Gateway", "pages": [ "documentation/platform/gateways/overview", - "documentation/platform/gateways/gateway-security" + "documentation/platform/gateways/gateway-security", + "documentation/platform/gateways/networking" ] }, "documentation/platform/project-templates", From 9cc17452fa187254eeb9fc7d14c61ef31d4fc83a Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 5 Jun 2025 01:23:28 -0400 Subject: [PATCH 9/9] address greptile --- docs/documentation/platform/gateways/networking.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/documentation/platform/gateways/networking.mdx b/docs/documentation/platform/gateways/networking.mdx index 2e512bf8b..6acdc1993 100644 --- a/docs/documentation/platform/gateways/networking.mdx +++ b/docs/documentation/platform/gateways/networking.mdx @@ -78,7 +78,7 @@ Modern firewalls automatically track UDP connections and allow return responses. - Reduces firewall rule complexity - Avoids the need for manual IP whitelisting -In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicity define return traffic manually. +In the event that your firewall does not support connection tracking, you will need to whitelist the relay IPs to explicitly define return traffic manually. ## Common Network Scenarios @@ -95,7 +95,7 @@ For corporate environments with strict egress filtering: Configure security groups to allow: - **Outbound UDP** to relay IPs on port 5349 -- **Outbound HTTPS** to api.infisical.com +- **Outbound HTTPS** to app.infisical.com/eu.infisical.com on port 443 - **Inbound UDP** on ephemeral ports (if not using stateful rules) ## Frequently Asked Questions @@ -139,7 +139,7 @@ This design maintains security by avoiding the need for inbound firewall rules t If your firewall has strict UDP restrictions: 1. **Work with your network team** to allow outbound UDP to the specific relay IP addresses -2. **Use explicit IP whitelisting** (Option 2) if connection tracking is disabled +2. **Use explicit IP whitelisting** if connection tracking is disabled 3. **Consider network policy exceptions** for the gateway host 4. **Monitor firewall logs** to identify which specific rules are blocking traffic