From 0ea9f9b60d43e55a695ba520b3fa2d3cc53eb0d0 Mon Sep 17 00:00:00 2001
From: x032205
Date: Fri, 16 May 2025 18:36:02 -0400
Subject: [PATCH 01/22] feat(org): Shared Secret limits for org
---
...516192508_secret-sharing-limits-for-org.ts | 35 +++
backend/src/db/schemas/organizations.ts | 4 +-
.../server/routes/v1/organization-router.ts | 4 +-
backend/src/services/org/org-schema.ts | 4 +-
backend/src/services/org/org-service.ts | 8 +-
backend/src/services/org/org-types.ts | 2 +
.../secret-sharing/secret-sharing-service.ts | 13 +
.../src/hooks/api/organization/queries.tsx | 8 +-
frontend/src/hooks/api/organization/types.ts | 4 +
.../ShareSecret/AddShareSecretModal.tsx | 2 +
.../OrgSecretShareLimitSection.tsx | 280 ++++++++++++++++++
.../OrgSecurityTab/OrgSecurityTab.tsx | 2 +
.../OrgUserAccessTokenLimitSection.tsx | 106 ++++---
.../components/ShareSecretForm.tsx | 26 +-
14 files changed, 432 insertions(+), 66 deletions(-)
create mode 100644 backend/src/db/migrations/20250516192508_secret-sharing-limits-for-org.ts
create mode 100644 frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
diff --git a/backend/src/db/migrations/20250516192508_secret-sharing-limits-for-org.ts b/backend/src/db/migrations/20250516192508_secret-sharing-limits-for-org.ts
new file mode 100644
index 000000000..f68c1c29b
--- /dev/null
+++ b/backend/src/db/migrations/20250516192508_secret-sharing-limits-for-org.ts
@@ -0,0 +1,35 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ const hasLifetimeColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretLifetime");
+ const hasViewLimitColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretViewLimit");
+
+ if (!hasLifetimeColumn || !hasViewLimitColumn) {
+ await knex.schema.alterTable(TableName.Organization, (t) => {
+ if (!hasLifetimeColumn) {
+ t.integer("maxSharedSecretLifetime").nullable().defaultTo(2592000); // 30 days in seconds
+ }
+ if (!hasViewLimitColumn) {
+ t.integer("maxSharedSecretViewLimit").nullable();
+ }
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ const hasLifetimeColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretLifetime");
+ const hasViewLimitColumn = await knex.schema.hasColumn(TableName.Organization, "maxSharedSecretViewLimit");
+
+ if (hasLifetimeColumn || hasViewLimitColumn) {
+ await knex.schema.alterTable(TableName.Organization, (t) => {
+ if (hasLifetimeColumn) {
+ t.dropColumn("maxSharedSecretLifetime");
+ }
+ if (hasViewLimitColumn) {
+ t.dropColumn("maxSharedSecretViewLimit");
+ }
+ });
+ }
+}
diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts
index 6779d5407..fb0728707 100644
--- a/backend/src/db/schemas/organizations.ts
+++ b/backend/src/db/schemas/organizations.ts
@@ -34,7 +34,9 @@ export const OrganizationsSchema = z.object({
kmsProductEnabled: z.boolean().default(true).nullable().optional(),
sshProductEnabled: z.boolean().default(true).nullable().optional(),
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
- shareSecretsProductEnabled: z.boolean().default(true).nullable().optional()
+ shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
+ maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
+ maxSharedSecretViewLimit: z.number().nullable().optional()
});
export type TOrganizations = z.infer;
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index e14dacebb..604b5a355 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -281,7 +281,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
kmsProductEnabled: z.boolean().optional(),
sshProductEnabled: z.boolean().optional(),
scannerProductEnabled: z.boolean().optional(),
- shareSecretsProductEnabled: z.boolean().optional()
+ shareSecretsProductEnabled: z.boolean().optional(),
+ maxSharedSecretLifetime: z.number().optional(),
+ maxSharedSecretViewLimit: z.number().nullable().optional()
}),
response: {
200: z.object({
diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts
index 39a1680a9..ae82cd1bc 100644
--- a/backend/src/services/org/org-schema.ts
+++ b/backend/src/services/org/org-schema.ts
@@ -24,5 +24,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
kmsProductEnabled: true,
sshProductEnabled: true,
scannerProductEnabled: true,
- shareSecretsProductEnabled: true
+ shareSecretsProductEnabled: true,
+ maxSharedSecretLifetime: true,
+ maxSharedSecretViewLimit: true
});
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index bcbd9e0e5..c966d5ef9 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -361,7 +361,9 @@ export const orgServiceFactory = ({
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
- shareSecretsProductEnabled
+ shareSecretsProductEnabled,
+ maxSharedSecretLifetime,
+ maxSharedSecretViewLimit
}
}: TUpdateOrgDTO) => {
const appCfg = getConfig();
@@ -469,7 +471,9 @@ export const orgServiceFactory = ({
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
- shareSecretsProductEnabled
+ shareSecretsProductEnabled,
+ maxSharedSecretLifetime,
+ maxSharedSecretViewLimit
});
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org;
diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts
index 9625934fb..8b2485ac4 100644
--- a/backend/src/services/org/org-types.ts
+++ b/backend/src/services/org/org-types.ts
@@ -81,6 +81,8 @@ export type TUpdateOrgDTO = {
sshProductEnabled: boolean;
scannerProductEnabled: boolean;
shareSecretsProductEnabled: boolean;
+ maxSharedSecretLifetime: number;
+ maxSharedSecretViewLimit: number | null;
}>;
} & TOrgPermission;
diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts
index 9649be722..ed2ad41df 100644
--- a/backend/src/services/secret-sharing/secret-sharing-service.ts
+++ b/backend/src/services/secret-sharing/secret-sharing-service.ts
@@ -93,6 +93,19 @@ export const secretSharingServiceFactory = ({
throw new BadRequestError({ message: "Shared secret value too long" });
}
+ // Check lifetime is within org allowance
+ const expiresAtTimestamp = new Date(expiresAt).getTime();
+ const lifetime = expiresAtTimestamp - new Date().getTime();
+
+ if (org.maxSharedSecretLifetime && lifetime / 1000 > org.maxSharedSecretLifetime) {
+ throw new BadRequestError({ message: "Secret lifetime exceeds organization limit" });
+ }
+
+ // Check max view count is within org allowance
+ if (org.maxSharedSecretViewLimit && (!expiresAfterViews || expiresAfterViews > org.maxSharedSecretViewLimit)) {
+ throw new BadRequestError({ message: "Secret max views parameter exceeds organization limit" });
+ }
+
const encryptWithRoot = kmsService.encryptWithRootKey();
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx
index 947353162..cd620bb64 100644
--- a/frontend/src/hooks/api/organization/queries.tsx
+++ b/frontend/src/hooks/api/organization/queries.tsx
@@ -118,7 +118,9 @@ export const useUpdateOrg = () => {
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
- shareSecretsProductEnabled
+ shareSecretsProductEnabled,
+ maxSharedSecretLifetime,
+ maxSharedSecretViewLimit
}) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
@@ -136,7 +138,9 @@ export const useUpdateOrg = () => {
kmsProductEnabled,
sshProductEnabled,
scannerProductEnabled,
- shareSecretsProductEnabled
+ shareSecretsProductEnabled,
+ maxSharedSecretLifetime,
+ maxSharedSecretViewLimit
});
},
onSuccess: () => {
diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts
index ab015f890..068cfad6d 100644
--- a/frontend/src/hooks/api/organization/types.ts
+++ b/frontend/src/hooks/api/organization/types.ts
@@ -26,6 +26,8 @@ export type Organization = {
sshProductEnabled: boolean;
scannerProductEnabled: boolean;
shareSecretsProductEnabled: boolean;
+ maxSharedSecretLifetime: number;
+ maxSharedSecretViewLimit: number | null;
};
export type UpdateOrgDTO = {
@@ -46,6 +48,8 @@ export type UpdateOrgDTO = {
sshProductEnabled?: boolean;
scannerProductEnabled?: boolean;
shareSecretsProductEnabled?: boolean;
+ maxSharedSecretViewLimit?: number | null;
+ maxSharedSecretLifetime?: number;
};
export type BillingDetails = {
diff --git a/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal.tsx b/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal.tsx
index 45b974755..b176ff653 100644
--- a/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal.tsx
+++ b/frontend/src/pages/organization/SecretSharingPage/components/ShareSecret/AddShareSecretModal.tsx
@@ -30,6 +30,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
allowSecretSharingOutsideOrganization={
currentOrg?.allowSecretSharingOutsideOrganization ?? true
}
+ maxSharedSecretLifetime={currentOrg.maxSharedSecretLifetime}
+ maxSharedSecretViewLimit={currentOrg.maxSharedSecretViewLimit}
/>
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
new file mode 100644
index 000000000..cb5c99daa
--- /dev/null
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
@@ -0,0 +1,280 @@
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useEffect } from "react";
+
+import { createNotification } from "@app/components/notifications";
+import { OrgPermissionCan } from "@app/components/permissions";
+import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
+import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
+import { useUpdateOrg } from "@app/hooks/api";
+
+const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
+
+// Helper function to convert duration to seconds
+const durationToSeconds = (value: number, unit: "m" | "h" | "d"): number => {
+ switch (unit) {
+ case "m":
+ return value * 60;
+ case "h":
+ return value * 60 * 60;
+ case "d":
+ return value * 60 * 60 * 24;
+ default:
+ return 0;
+ }
+};
+
+// Helper function to convert seconds to form lifetime value and unit
+const getFormLifetimeFromSeconds = (
+ totalSeconds: number | null | undefined
+): { maxLifetimeValue: number; maxLifetimeUnit: "m" | "h" | "d" } => {
+ const DEFAULT_LIFETIME_VALUE = 30;
+ const DEFAULT_LIFETIME_UNIT = "d" as "m" | "h" | "d";
+
+ if (totalSeconds == null || totalSeconds <= 0) {
+ return {
+ maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
+ maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
+ };
+ }
+
+ const secondsInDay = 24 * 60 * 60;
+ const secondsInHour = 60 * 60;
+ const secondsInMinute = 60;
+
+ if (totalSeconds % secondsInDay === 0) {
+ const value = totalSeconds / secondsInDay;
+ if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "d" };
+ }
+
+ if (totalSeconds % secondsInHour === 0) {
+ const value = totalSeconds / secondsInHour;
+ if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "h" };
+ }
+
+ if (totalSeconds % secondsInMinute === 0) {
+ const value = totalSeconds / secondsInMinute;
+ if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "m" };
+ }
+
+ return {
+ maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
+ maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
+ };
+};
+
+const formSchema = z
+ .object({
+ maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
+ maxLifetimeUnit: z.enum(["m", "h", "d"], {
+ invalid_type_error: "Please select a valid time unit"
+ }),
+ maxViewLimit: z.string()
+ })
+ .superRefine((data, ctx) => {
+ const { maxLifetimeValue, maxLifetimeUnit } = data;
+
+ const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit);
+
+ if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
+ let message = "Duration exceeds maximum allowed limit";
+
+ if (maxLifetimeUnit === "m") {
+ message = `Maximum allowed minutes is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / 60} (30 days)`;
+ } else if (maxLifetimeUnit === "h") {
+ message = `Maximum allowed hours is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (60 * 60)} (30 days)`;
+ } else if (maxLifetimeUnit === "d") {
+ message = `Maximum allowed days is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (24 * 60 * 60)}`;
+ }
+
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message,
+ path: ["maxLifetimeValue"]
+ });
+ }
+ });
+
+type TForm = z.infer;
+
+const viewLimitOptions = [
+ { label: "1", value: 1 },
+ { label: "Unlimited", value: -1 }
+];
+
+export const OrgSecretShareLimitSection = () => {
+ const { mutateAsync } = useUpdateOrg();
+ const { currentOrg } = useOrganization();
+
+ const getDefaultFormValues = () => {
+ const initialLifetime = getFormLifetimeFromSeconds(currentOrg?.maxSharedSecretLifetime);
+ return {
+ maxLifetimeValue: initialLifetime.maxLifetimeValue,
+ maxLifetimeUnit: initialLifetime.maxLifetimeUnit,
+ maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "-1"
+ };
+ };
+
+ const {
+ control,
+ formState: { isSubmitting, isDirty },
+ handleSubmit,
+ reset
+ } = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: getDefaultFormValues()
+ });
+
+ useEffect(() => {
+ if (currentOrg) {
+ reset(getDefaultFormValues());
+ }
+ }, [currentOrg, reset]);
+
+ const handleFormSubmit = async (formData: TForm) => {
+ try {
+ const maxSharedSecretLifetimeSeconds = durationToSeconds(
+ formData.maxLifetimeValue,
+ formData.maxLifetimeUnit
+ );
+
+ await mutateAsync({
+ orgId: currentOrg.id,
+ maxSharedSecretViewLimit:
+ formData.maxViewLimit === "-1" ? null : Number(formData.maxViewLimit),
+ maxSharedSecretLifetime: maxSharedSecretLifetimeSeconds
+ });
+
+ createNotification({
+ text: "Successfully updated secret share limits",
+ type: "success"
+ });
+
+ reset(formData);
+ } catch {
+ createNotification({
+ text: "Failed to update secret share limits",
+ type: "error"
+ });
+ }
+ };
+
+ // Units for the dropdown with readable labels
+ const timeUnits = [
+ { value: "m", label: "Minutes" },
+ { value: "h", label: "Hours" },
+ { value: "d", label: "Days" }
+ ];
+
+ return (
+
+
+
+ These settings establish the maximum limits for all Shared Secret parameters within this
+ organization. Shared secrets cannot be created with values exceeding these limits.
+
+
+ {(isAllowed) => (
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx
index 981681b7d..2d402ab21 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx
@@ -6,6 +6,7 @@ import { withPermission } from "@app/hoc";
import { OrgGenericAuthSection } from "./OrgGenericAuthSection";
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
+import { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
export const OrgSecurityTab = withPermission(
() => {
@@ -28,6 +29,7 @@ export const OrgSecurityTab = withPermission(
+
>
);
},
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx
index bc020d3a1..58a91e90e 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgUserAccessTokenLimitSection.tsx
@@ -96,61 +96,59 @@ export const OrgUserAccessTokenLimitSection = () => {
{(isAllowed) => (
diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
index a2228e46b..35536831e 100644
--- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
+++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
@@ -6,7 +6,19 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
-import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+ Button,
+ FormControl,
+ IconButton,
+ Input,
+ Select,
+ SelectItem,
+ Switch
+} from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api";
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing";
@@ -33,7 +45,19 @@ const schema = z.object({
secret: z.string().min(1),
expiresIn: z.string(),
viewLimit: z.string(),
- accessType: z.nativeEnum(SecretSharingAccessType).optional()
+ accessType: z.nativeEnum(SecretSharingAccessType).optional(),
+ emails: z
+ .string()
+ .optional()
+ .refine(
+ (val) => {
+ if (!val) return true;
+ return val.split(",").every((email) => z.string().email().safeParse(email.trim()).success);
+ },
+ {
+ message: "Must be a comma-separated list of valid emails or empty."
+ }
+ )
});
export type FormData = z.infer;
@@ -49,7 +73,7 @@ export const ShareSecretForm = ({
value,
allowSecretSharingOutsideOrganization = true
}: Props) => {
- const [secretLink, setSecretLink] = useState("");
+ const [secretLink, setSecretLink] = useState(null);
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset({
initialState: "Copy to clipboard"
});
@@ -66,7 +90,9 @@ export const ShareSecretForm = ({
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
- secret: value || ""
+ secret: value || "",
+ viewLimit: "-1",
+ expiresIn: "3600000"
}
});
@@ -76,32 +102,45 @@ export const ShareSecretForm = ({
secret,
expiresIn,
viewLimit,
- accessType
+ accessType,
+ emails
}: FormData) => {
try {
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
+ const processedEmails = emails ? emails.split(",").map((e) => e.trim()) : undefined;
+
const { id } = await createSharedSecret.mutateAsync({
name,
password,
secretValue: secret,
expiresAt,
expiresAfterViews: viewLimit === "-1" ? undefined : Number(viewLimit),
- accessType
+ accessType,
+ emails: processedEmails
});
- const link = `${window.location.origin}/shared/secret/${id}`;
+ if (processedEmails && processedEmails.length > 0) {
+ setSecretLink("");
+ createNotification({
+ text: `Shared secret link emailed to ${processedEmails.length} user(s).`,
+ type: "success"
+ });
+ } else {
+ const link = `${window.location.origin}/shared/secret/${id}`;
+
+ setSecretLink(link);
+
+ navigator.clipboard.writeText(link);
+ setCopyTextSecret("secret");
+
+ createNotification({
+ text: "Shared secret link copied to clipboard.",
+ type: "success"
+ });
+ }
- setSecretLink(link);
reset();
-
- navigator.clipboard.writeText(link);
- setCopyTextSecret("secret");
-
- createNotification({
- text: "Shared secret link copied to clipboard.",
- type: "success"
- });
} catch (error) {
console.error(error);
createNotification({
@@ -111,152 +150,230 @@ export const ShareSecretForm = ({
}
};
- const hasSecretLink = Boolean(secretLink);
-
- return !hasSecretLink ? (
-
- ) : (
+
+ {!isPublic && (
+ (
+ Feature enforced by organization
+ )
+ }
+ errorText={error?.message}
+ isError={Boolean(error)}
+ >
+
+ onChange(
+ v ? SecretSharingAccessType.Organization : SecretSharingAccessType.Anyone
+ )
+ }
+ id="delete-secrets"
+ >
+ Limit access to people within organization
+
+
+ )}
+ />
+ )}
+
+
+
+
+ Advanced Settings
+
+
+ (
+
+ onChange(e)}
+ className="w-full"
+ >
+ {expiresInOptions.map(({ label, value: expiresInValue }) => (
+
+ {label}
+
+ ))}
+
+
+ )}
+ />
+ (
+
+ onChange(e)}
+ className="w-full"
+ >
+ {viewLimitOptions.map(({ label, value: viewLimitValue }) => (
+
+ {label}
+
+ ))}
+
+
+ )}
+ />
+
+ {!isPublic && (
+ (
+
+
+
+ )}
+ />
+ )}
+
+
+
+
+
+
+ Create Secret Link
+
+
+
+ );
+
+ if (secretLink === "")
+ return (
+ <>
+
+
+ Shared secret link has been emailed to select users.
+
+ setSecretLink(null)}
+ rightIcon={ }
+ >
+ Share Another Secret
+
+ >
+ );
+
+ return (
<>
{secretLink}
@@ -265,7 +382,7 @@ export const ShareSecretForm = ({
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
- navigator.clipboard.writeText(secretLink);
+ navigator.clipboard.writeText(secretLink || "");
setCopyTextSecret("Copied");
}}
>
@@ -277,7 +394,7 @@ export const ShareSecretForm = ({
colorSchema="primary"
variant="outline_bg"
size="sm"
- onClick={() => setSecretLink("")}
+ onClick={() => setSecretLink(null)}
rightIcon={
}
>
Share Another Secret
diff --git a/frontend/src/pages/public/ViewSecretRequestByIDPage/ViewSecretRequestByIDPage.tsx b/frontend/src/pages/public/ViewSecretRequestByIDPage/ViewSecretRequestByIDPage.tsx
index bce742358..2bb5c7b0f 100644
--- a/frontend/src/pages/public/ViewSecretRequestByIDPage/ViewSecretRequestByIDPage.tsx
+++ b/frontend/src/pages/public/ViewSecretRequestByIDPage/ViewSecretRequestByIDPage.tsx
@@ -175,7 +175,7 @@ export const ViewSecretRequestByIDPage = () => {
Infisical
- 156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
+ 235 2nd st, San Francisco, California, 94105, United States. 🇺🇸
diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx
index 5112bd47e..4c83b0357 100644
--- a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx
+++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx
@@ -38,6 +38,14 @@ export const ViewSharedSecretByIDPage = () => {
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
select: (el) => el.key
});
+ const email = useSearch({
+ from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
+ select: (el) => el.email
+ });
+ const token = useSearch({
+ from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
+ select: (el) => el.token
+ });
const [password, setPassword] = useState();
const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey);
@@ -49,7 +57,9 @@ export const ViewSharedSecretByIDPage = () => {
} = useGetActiveSharedSecretById({
sharedSecretId: id,
hashedHex,
- password
+ password,
+ email,
+ token
});
const navigate = useNavigate();
@@ -57,15 +67,16 @@ export const ViewSharedSecretByIDPage = () => {
const isUnauthorized =
((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 401;
- const isForbidden =
- ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode === 403;
-
const isInvalidCredential =
((error as AxiosError)?.response?.data as { message: string })?.message ===
"Invalid credentials";
+ const isEmailUnauthorized =
+ ((error as AxiosError)?.response?.data as { message: string })?.message ===
+ "Email not authorized to view secret";
+
useEffect(() => {
- if (isUnauthorized && !isInvalidCredential) {
+ if (isUnauthorized && !isInvalidCredential && !isEmailUnauthorized) {
// persist current URL in session storage so that we can come back to this after successful login
sessionStorage.setItem(
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL,
@@ -85,10 +96,10 @@ export const ViewSharedSecretByIDPage = () => {
});
}
- if (isForbidden) {
+ if (error) {
createNotification({
type: "error",
- text: "You do not have access to this shared secret."
+ text: ((error as AxiosError)?.response?.data as { message: string })?.message
});
}
}, [error]);
@@ -195,7 +206,7 @@ export const ViewSharedSecretByIDPage = () => {
Infisical
- 156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
+ 235 2nd st, San Francisco, California, 94105, United States. 🇺🇸
diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx
index 7b98dded3..a1bc4009c 100644
--- a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx
+++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx
@@ -7,7 +7,9 @@ import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage";
const SharedSecretByIDPageQuerySchema = z.object({
- key: z.string().catch("")
+ key: z.string().catch(""),
+ email: z.string().optional(),
+ token: z.string().optional()
});
export const Route = createFileRoute("/shared/secret/$secretId")({
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
index 111381692..8ada43347 100644
--- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
+++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
@@ -4,13 +4,13 @@ import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
-import { SecretSharingSection } from "../SecretSharingSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectProtection } from "../DeleteProjectProtection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
+import { SecretSharingSection } from "../SecretSharingSection";
import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => {
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
index 3daa7dad2..837f32d3e 100644
--- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
+++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
@@ -1,9 +1,10 @@
+import { useState } from "react";
+
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api/workspace/queries";
-import { useState } from "react";
export const SecretSharingSection = () => {
const { currentWorkspace } = useWorkspace();
From 9fd37ca456acc28b58767ed57c338e3f7fcc3d8a Mon Sep 17 00:00:00 2001
From: x032205
Date: Sat, 17 May 2025 01:51:05 -0400
Subject: [PATCH 04/22] greptile review fixes
---
.../server/routes/v1/secret-sharing-router.ts | 2 +-
.../src/hooks/api/secretSharing/queries.ts | 19 ++++++++++++++-----
.../components/ShareSecretForm.tsx | 11 ++++++++---
3 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts
index f191e4deb..71fb9bc68 100644
--- a/backend/src/server/routes/v1/secret-sharing-router.ts
+++ b/backend/src/server/routes/v1/secret-sharing-router.ts
@@ -156,7 +156,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
expiresAt: z.string(),
expiresAfterViews: z.number().min(1).optional(),
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
- emails: z.string().email().array().optional()
+ emails: z.string().email().array().max(100).optional()
}),
response: {
200: z.object({
diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts
index 28448b751..0e8c59947 100644
--- a/frontend/src/hooks/api/secretSharing/queries.ts
+++ b/frontend/src/hooks/api/secretSharing/queries.ts
@@ -11,10 +11,13 @@ export const secretSharingKeys = {
allSecretRequests: () => ["secretRequests"] as const,
specificSecretRequests: ({ offset, limit }: { offset: number; limit: number }) =>
[...secretSharingKeys.allSecretRequests(), { offset, limit }] as const,
- getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
- "shared-secret",
- arg
- ],
+ getSecretById: (arg: {
+ id: string;
+ hashedHex: string | null;
+ password?: string;
+ email?: string;
+ token?: string;
+ }) => ["shared-secret", arg],
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
};
@@ -83,7 +86,13 @@ export const useGetActiveSharedSecretById = ({
token?: string;
}) => {
return useQuery({
- queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
+ queryKey: secretSharingKeys.getSecretById({
+ id: sharedSecretId,
+ hashedHex,
+ password,
+ email,
+ token
+ }),
queryFn: async () => {
const { data } = await apiRequest.post(
`/api/v1/secret-sharing/shared/public/${sharedSecretId}`,
diff --git a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
index 35536831e..0380cde0c 100644
--- a/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
+++ b/frontend/src/pages/public/ShareSecretPage/components/ShareSecretForm.tsx
@@ -52,10 +52,15 @@ const schema = z.object({
.refine(
(val) => {
if (!val) return true;
- return val.split(",").every((email) => z.string().email().safeParse(email.trim()).success);
+ const emails = val
+ .split(",")
+ .map((email) => email.trim())
+ .filter((email) => email !== "");
+ if (emails.length > 100) return false;
+ return emails.every((email) => z.string().email().safeParse(email).success);
},
{
- message: "Must be a comma-separated list of valid emails or empty."
+ message: "Must be a comma-separated list of valid emails (max 100) or empty."
}
)
});
@@ -249,7 +254,7 @@ export const ShareSecretForm = ({
v ? SecretSharingAccessType.Organization : SecretSharingAccessType.Anyone
)
}
- id="delete-secrets"
+ id="org-access-only"
>
Limit access to people within organization
From 16c51af3404d023b855e99b361fc8627c9d495ec Mon Sep 17 00:00:00 2001
From: x032205
Date: Sat, 17 May 2025 02:17:41 -0400
Subject: [PATCH 05/22] review fixes
---
.../src/server/routes/v1/organization-router.ts | 7 ++++++-
.../secret-sharing/secret-sharing-service.ts | 4 +++-
.../OrgSecurityTab/OrgSecretShareLimitSection.tsx | 14 ++++++++++++++
3 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index c39c0ab4d..c489d685d 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -282,9 +282,14 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
sshProductEnabled: z.boolean().optional(),
scannerProductEnabled: z.boolean().optional(),
shareSecretsProductEnabled: z.boolean().optional(),
- maxSharedSecretLifetime: z.number().max(2592000, "Max Shared Secret lifetime cannot exceed 30 days").optional(),
+ maxSharedSecretLifetime: z
+ .number()
+ .min(300, "Max Shared Secret lifetime cannot be under 5 minutes")
+ .max(2592000, "Max Shared Secret lifetime cannot exceed 30 days")
+ .optional(),
maxSharedSecretViewLimit: z
.number()
+ .min(1, "Max Shared Secret view count cannot be lower than 1")
.max(1000, "Max Shared Secret view count cannot exceed 1000")
.nullable()
.optional()
diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts
index a40861790..e216cb939 100644
--- a/backend/src/services/secret-sharing/secret-sharing-service.ts
+++ b/backend/src/services/secret-sharing/secret-sharing-service.ts
@@ -60,7 +60,9 @@ export const secretSharingServiceFactory = ({
}
const fiveMins = 5 * 60 * 1000;
- if (expiryTime - currentTime < fiveMins) {
+
+ // 1 second buffer
+ if (expiryTime - currentTime + 1000 < fiveMins) {
throw new BadRequestError({ message: "Expiration time cannot be less than 5 mins" });
}
};
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
index cb5c99daa..e0d88a082 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
@@ -10,6 +10,7 @@ import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@a
import { useUpdateOrg } from "@app/hooks/api";
const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
+const MIN_SHARED_SECRET_LIFETIME_SECONDS = 5 * 60; // 5 minutes in seconds
// Helper function to convert duration to seconds
const durationToSeconds = (value: number, unit: "m" | "h" | "d"): number => {
@@ -77,6 +78,7 @@ const formSchema = z
const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit);
+ // Check max limit
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
let message = "Duration exceeds maximum allowed limit";
@@ -94,6 +96,17 @@ const formSchema = z
path: ["maxLifetimeValue"]
});
}
+
+ // Check min limit
+ if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
+ const message = `Duration must be at least ${MIN_SHARED_SECRET_LIFETIME_SECONDS / 60} minutes`; // 5 minutes
+
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message,
+ path: ["maxLifetimeValue"]
+ });
+ }
});
type TForm = z.infer;
@@ -187,6 +200,7 @@ export const OrgSecretShareLimitSection = () => {
From 923feb81f34519c2c0f2f97acb9423ac4c991aaf Mon Sep 17 00:00:00 2001
From: x032205
Date: Sat, 17 May 2025 12:44:05 -0400
Subject: [PATCH 06/22] fix bundle endpoint for old certs
---
.../server/routes/v1/certificate-router.ts | 2 +-
.../services/certificate/certificate-fns.ts | 2 +-
.../certificate/certificate-service.ts | 22 ++++++++++++-------
.../src/hooks/api/certificates/queries.tsx | 2 +-
.../components/CertificateCertModal.tsx | 4 ++--
.../OrgProductSelectSection.tsx | 4 ++--
.../ProjectGeneralTab/ProjectGeneralTab.tsx | 2 +-
.../SecretSharingSection.tsx | 3 ++-
8 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts
index dad1d9a80..e7f8a7833 100644
--- a/backend/src/server/routes/v1/certificate-router.ts
+++ b/backend/src/server/routes/v1/certificate-router.ts
@@ -132,7 +132,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
- privateKey: z.string().trim().describe(CERTIFICATES.GET_CERT.privateKey),
+ privateKey: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
}
diff --git a/backend/src/services/certificate/certificate-fns.ts b/backend/src/services/certificate/certificate-fns.ts
index 961fb27ff..7eeb62d93 100644
--- a/backend/src/services/certificate/certificate-fns.ts
+++ b/backend/src/services/certificate/certificate-fns.ts
@@ -105,7 +105,7 @@ export const buildCertificateChain = async ({
kmsService,
kmsId
}: TBuildCertificateChainDTO) => {
- if (!encryptedCertificateChain && (!caCert || !caCertChain)) {
+ if (!encryptedCertificateChain && !caCert) {
return null;
}
diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts
index 73a8caed7..3e52b25ce 100644
--- a/backend/src/services/certificate/certificate-service.ts
+++ b/backend/src/services/certificate/certificate-service.ts
@@ -337,18 +337,24 @@ export const certificateServiceFactory = ({
encryptedCertificateChain: certBody.encryptedCertificateChain || undefined
});
- const { certPrivateKey } = await getCertificateCredentials({
- certId: cert.id,
- projectId: ca.projectId,
- certificateSecretDAL,
- projectDAL,
- kmsService
- });
+ let privateKey: string | null = null;
+ try {
+ const { certPrivateKey } = await getCertificateCredentials({
+ certId: cert.id,
+ projectId: ca.projectId,
+ certificateSecretDAL,
+ projectDAL,
+ kmsService
+ });
+ privateKey = certPrivateKey;
+ } catch (e) {
+ // This will error for older certificates
+ }
return {
certificate,
certificateChain,
- privateKey: certPrivateKey,
+ privateKey,
serialNumber,
cert,
ca
diff --git a/frontend/src/hooks/api/certificates/queries.tsx b/frontend/src/hooks/api/certificates/queries.tsx
index c53cef471..50f2836ed 100644
--- a/frontend/src/hooks/api/certificates/queries.tsx
+++ b/frontend/src/hooks/api/certificates/queries.tsx
@@ -48,7 +48,7 @@ export const useGetCertBundle = (serialNumber: string) => {
certificate: string;
certificateChain: string;
serialNumber: string;
- privateKey: string;
+ privateKey: string | null;
}>(`/api/v1/pki/certificates/${serialNumber}/bundle`);
return data;
},
diff --git a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx
index 54620f1d6..281683d08 100644
--- a/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx
+++ b/frontend/src/pages/cert-manager/CertificatesPage/components/CertificateCertModal.tsx
@@ -35,7 +35,7 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
certificate: string;
certificateChain: string;
serialNumber: string;
- privateKey?: string;
+ privateKey?: string | null;
}
| undefined = canReadPrivateKey ? bundleData : bodyData;
@@ -52,7 +52,7 @@ export const CertificateCertModal = ({ popUp, handlePopUpToggle }: Props) => {
serialNumber={data.serialNumber}
certificate={data.certificate}
certificateChain={data.certificateChain}
- privateKey={data.privateKey}
+ privateKey={data.privateKey || undefined}
/>
) : (
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
index b9991d0e5..2697aeb7d 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
@@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
+import axios from "axios";
+import { createNotification } from "@app/components/notifications";
import { Switch } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
-import axios from "axios";
-import { createNotification } from "@app/components/notifications";
export const OrgProductSelectSection = () => {
const [toggledProducts, setToggledProducts] = useState<{
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
index 111381692..8ada43347 100644
--- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
+++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx
@@ -4,13 +4,13 @@ import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
-import { SecretSharingSection } from "../SecretSharingSection";
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
import { DeleteProjectProtection } from "../DeleteProjectProtection";
import { DeleteProjectSection } from "../DeleteProjectSection";
import { EnvironmentSection } from "../EnvironmentSection";
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
+import { SecretSharingSection } from "../SecretSharingSection";
import { SecretTagsSection } from "../SecretTagsSection";
export const ProjectGeneralTab = () => {
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
index 3daa7dad2..837f32d3e 100644
--- a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
+++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSharingSection/SecretSharingSection.tsx
@@ -1,9 +1,10 @@
+import { useState } from "react";
+
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Checkbox } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api/workspace/queries";
-import { useState } from "react";
export const SecretSharingSection = () => {
const { currentWorkspace } = useWorkspace();
From 645f70f770fe937c3a469d0994a2b24550f06d88 Mon Sep 17 00:00:00 2001
From: x032205
Date: Sat, 17 May 2025 13:05:09 -0400
Subject: [PATCH 07/22] tweaks
---
backend/src/server/routes/v1/certificate-router.ts | 4 ++--
backend/src/services/certificate/certificate-service.ts | 6 +++++-
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts
index e7f8a7833..0e4cec8e1 100644
--- a/backend/src/server/routes/v1/certificate-router.ts
+++ b/backend/src/server/routes/v1/certificate-router.ts
@@ -131,7 +131,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
- certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
+ certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
privateKey: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.privateKey),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
@@ -518,7 +518,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
response: {
200: z.object({
certificate: z.string().trim().describe(CERTIFICATES.GET_CERT.certificate),
- certificateChain: z.string().trim().nullish().describe(CERTIFICATES.GET_CERT.certificateChain),
+ certificateChain: z.string().trim().nullable().describe(CERTIFICATES.GET_CERT.certificateChain),
serialNumber: z.string().trim().describe(CERTIFICATES.GET_CERT.serialNumberRes)
})
}
diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts
index 3e52b25ce..292b5f109 100644
--- a/backend/src/services/certificate/certificate-service.ts
+++ b/backend/src/services/certificate/certificate-service.ts
@@ -29,6 +29,7 @@ import {
TGetCertPrivateKeyDTO,
TRevokeCertDTO
} from "./certificate-types";
+import { NotFoundError } from "@app/lib/errors";
type TCertificateServiceFactoryDep = {
certificateDAL: Pick;
@@ -348,7 +349,10 @@ export const certificateServiceFactory = ({
});
privateKey = certPrivateKey;
} catch (e) {
- // This will error for older certificates
+ // Skip NotFound errors but throw all others
+ if (!(e instanceof NotFoundError)) {
+ throw e;
+ }
}
return {
From fdeefcdfcf566b572f945d9cbecd2ae09f75805a Mon Sep 17 00:00:00 2001
From: x032205
Date: Sat, 17 May 2025 13:10:15 -0400
Subject: [PATCH 08/22] padding to match similar container
---
.../OrgProductSelectSection/OrgProductSelectSection.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
index b9991d0e5..f590bb975 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx
@@ -79,7 +79,7 @@ export const OrgProductSelectSection = () => {
};
return (
-
+
Organization Products
Select which products are available for your organization.
From 021a8ddace314c6af588bb8a962d3775214fd3fc Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 17 May 2025 22:06:51 +0400
Subject: [PATCH 09/22] Update identity-kubernetes-auth-service.ts
---
.../identity-kubernetes-auth-service.ts | 8 ++------
1 file changed, 2 insertions(+), 6 deletions(-)
diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
index 2da7c3881..d93f08f83 100644
--- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
+++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
@@ -79,7 +79,7 @@ export const identityKubernetesAuthServiceFactory = ({
const callbackResult = await withGatewayProxy(
async (port) => {
- const res = await gatewayCallback("localhost", port);
+ const res = await gatewayCallback("https://localhost", port);
return res;
},
{
@@ -138,11 +138,7 @@ export const identityKubernetesAuthServiceFactory = ({
}
const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
- let baseUrl = `https://${host}`;
-
- if (port) {
- baseUrl += `:${port}`;
- }
+ const baseUrl = port ? `${host}:${port}` : host;
const res = await axios
.post(
From 5e192539a1843dd51fe06dbdb6cf1e444419e0c5 Mon Sep 17 00:00:00 2001
From: Daniel Hougaard
Date: Sat, 17 May 2025 22:13:49 +0400
Subject: [PATCH 10/22] Update identity-kubernetes-auth-service.ts
---
.../identity-kubernetes-auth/identity-kubernetes-auth-service.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
index d93f08f83..a3ec1bdeb 100644
--- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
+++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts
@@ -79,6 +79,7 @@ export const identityKubernetesAuthServiceFactory = ({
const callbackResult = await withGatewayProxy(
async (port) => {
+ // Needs to be https protocol or the kubernetes API server will fail with "Client sent an HTTP request to an HTTPS server"
const res = await gatewayCallback("https://localhost", port);
return res;
},
From 0401793d38a89facdea5531a6afa5b751304a6f5 Mon Sep 17 00:00:00 2001
From: x032205
Date: Mon, 19 May 2025 10:48:58 -0400
Subject: [PATCH 11/22] Changed "token" param to "hash" and used hex encoding
for URL
---
.../server/routes/v1/secret-sharing-router.ts | 4 ++--
.../secret-sharing/secret-sharing-service.ts | 18 +++++++++---------
.../secret-sharing/secret-sharing-types.ts | 2 +-
.../src/hooks/api/secretSharing/queries.ts | 10 +++++-----
.../ViewSharedSecretByIDPage.tsx | 6 +++---
.../public/ViewSharedSecretByIDPage/route.tsx | 2 +-
6 files changed, 21 insertions(+), 21 deletions(-)
diff --git a/backend/src/server/routes/v1/secret-sharing-router.ts b/backend/src/server/routes/v1/secret-sharing-router.ts
index 71fb9bc68..e712ee138 100644
--- a/backend/src/server/routes/v1/secret-sharing-router.ts
+++ b/backend/src/server/routes/v1/secret-sharing-router.ts
@@ -64,7 +64,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
hashedHex: z.string().min(1).optional(),
password: z.string().optional(),
email: z.string().optional(),
- token: z.string().optional()
+ hash: z.string().optional()
}),
response: {
200: z.object({
@@ -92,7 +92,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
password: req.body.password,
orgId: req.permission?.orgId,
email: req.body.email,
- token: req.body.token
+ hash: req.body.hash
});
if (sharedSecret.secret?.orgId) {
diff --git a/backend/src/services/secret-sharing/secret-sharing-service.ts b/backend/src/services/secret-sharing/secret-sharing-service.ts
index ec7fa287a..e56b10e46 100644
--- a/backend/src/services/secret-sharing/secret-sharing-service.ts
+++ b/backend/src/services/secret-sharing/secret-sharing-service.ts
@@ -118,7 +118,7 @@ export const secretSharingServiceFactory = ({
}
}
- // Generate salt for signing email tokens (if emails are provided)
+ // Generate salt for signing email hashes (if emails are provided)
salt = crypto.randomBytes(32).toString("hex");
encryptedSalt = encryptWithRoot(Buffer.from(salt));
}
@@ -159,7 +159,7 @@ export const secretSharingServiceFactory = ({
for await (const email of emails) {
try {
const hmac = crypto.createHmac("sha256", salt).update(email);
- const token = hmac.digest("base64");
+ const hash = hmac.digest("hex");
// Only show the username to emails which are part of the organization
const respondentUsername = orgEmails.includes(email) ? user.username : undefined;
@@ -170,7 +170,7 @@ export const secretSharingServiceFactory = ({
substitutions: {
name,
respondentUsername,
- secretRequestUrl: `${appCfg.SITE_URL}/shared/secret/${idToReturn}?email=${encodeURIComponent(email)}&token=${token}`
+ secretRequestUrl: `${appCfg.SITE_URL}/shared/secret/${idToReturn}?email=${encodeURIComponent(email)}&hash=${hash}`
},
template: SmtpTemplates.SecretRequestCompleted
});
@@ -460,7 +460,7 @@ export const secretSharingServiceFactory = ({
orgId,
password,
email,
- token
+ hash
}: TGetActiveSharedSecretByIdDTO) => {
const sharedSecret = isUuidV4(sharedSecretId)
? await secretSharingDAL.findOne({
@@ -512,22 +512,22 @@ export const secretSharingServiceFactory = ({
if (sharedSecret.authorizedEmails && sharedSecret.encryptedSalt) {
// Verify both params were passed
- if (!email || !token) {
+ if (!email || !hash) {
throw new BadRequestError({
- message: "This secret is email protected. Parameters must include email and token."
+ message: "This secret is email protected. Parameters must include email and hash."
});
// Verify that email is authorized to view shared secret
} else if (!(sharedSecret.authorizedEmails as string[]).includes(email)) {
throw new UnauthorizedError({ message: "Email not authorized to view secret" });
- // Verify that token matches
+ // Verify that hash matches
} else {
const salt = decryptWithRoot(sharedSecret.encryptedSalt).toString();
const hmac = crypto.createHmac("sha256", salt).update(email);
- const rebuiltToken = hmac.digest("base64");
+ const rebuiltHash = hmac.digest("hex");
- if (rebuiltToken !== token) {
+ if (rebuiltHash !== hash) {
throw new UnauthorizedError({ message: "Email not authorized to view secret" });
}
}
diff --git a/backend/src/services/secret-sharing/secret-sharing-types.ts b/backend/src/services/secret-sharing/secret-sharing-types.ts
index eae6a48fa..049dbb913 100644
--- a/backend/src/services/secret-sharing/secret-sharing-types.ts
+++ b/backend/src/services/secret-sharing/secret-sharing-types.ts
@@ -41,7 +41,7 @@ export type TGetActiveSharedSecretByIdDTO = {
// For secrets shared with specific emails
email?: string;
- token?: string;
+ hash?: string;
};
export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
diff --git a/frontend/src/hooks/api/secretSharing/queries.ts b/frontend/src/hooks/api/secretSharing/queries.ts
index 0e8c59947..cfd505ff0 100644
--- a/frontend/src/hooks/api/secretSharing/queries.ts
+++ b/frontend/src/hooks/api/secretSharing/queries.ts
@@ -16,7 +16,7 @@ export const secretSharingKeys = {
hashedHex: string | null;
password?: string;
email?: string;
- token?: string;
+ hash?: string;
}) => ["shared-secret", arg],
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
};
@@ -75,7 +75,7 @@ export const useGetActiveSharedSecretById = ({
hashedHex,
password,
email,
- token
+ hash
}: {
sharedSecretId: string;
hashedHex: string | null;
@@ -83,7 +83,7 @@ export const useGetActiveSharedSecretById = ({
// For secrets shared to specific emails (optional)
email?: string;
- token?: string;
+ hash?: string;
}) => {
return useQuery({
queryKey: secretSharingKeys.getSecretById({
@@ -91,7 +91,7 @@ export const useGetActiveSharedSecretById = ({
hashedHex,
password,
email,
- token
+ hash
}),
queryFn: async () => {
const { data } = await apiRequest.post(
@@ -100,7 +100,7 @@ export const useGetActiveSharedSecretById = ({
...(hashedHex && { hashedHex }),
password,
email,
- token
+ hash
}
);
diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx
index 4c83b0357..389aee68f 100644
--- a/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx
+++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/ViewSharedSecretByIDPage.tsx
@@ -42,9 +42,9 @@ export const ViewSharedSecretByIDPage = () => {
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
select: (el) => el.email
});
- const token = useSearch({
+ const hash = useSearch({
from: ROUTE_PATHS.Public.ViewSharedSecretByIDPage.id,
- select: (el) => el.token
+ select: (el) => el.hash
});
const [password, setPassword] = useState();
const { hashedHex, key } = extractDetailsFromUrl(urlEncodedKey);
@@ -59,7 +59,7 @@ export const ViewSharedSecretByIDPage = () => {
hashedHex,
password,
email,
- token
+ hash
});
const navigate = useNavigate();
diff --git a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx
index a1bc4009c..7cbcb59a2 100644
--- a/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx
+++ b/frontend/src/pages/public/ViewSharedSecretByIDPage/route.tsx
@@ -9,7 +9,7 @@ import { ViewSharedSecretByIDPage } from "./ViewSharedSecretByIDPage";
const SharedSecretByIDPageQuerySchema = z.object({
key: z.string().catch(""),
email: z.string().optional(),
- token: z.string().optional()
+ hash: z.string().optional()
});
export const Route = createFileRoute("/shared/secret/$secretId")({
From 6efb630200dcaa0f30a8cd343f55b7682829bdbd Mon Sep 17 00:00:00 2001
From: x032205
Date: Mon, 19 May 2025 12:32:22 -0400
Subject: [PATCH 12/22] Moved secret share limits to secret share settings
---
.../SecretSharingSettingsPage/SecretSharingSettingsPage.tsx | 4 ++--
.../OrgSecretShareLimitSection.tsx | 0
.../components/OrgSecretShareLimitSection/index.tsx | 1 +
.../SecretSharingSettingsGeneralTab.tsx | 2 ++
.../SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx | 2 --
5 files changed, 5 insertions(+), 4 deletions(-)
rename frontend/src/pages/organization/{SettingsPage/components/OrgSecurityTab => SecretSharingSettingsPage/components/OrgSecretShareLimitSection}/OrgSecretShareLimitSection.tsx (100%)
create mode 100644 frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx
index 8ee37f631..ae1aa3772 100644
--- a/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/SecretSharingSettingsPage.tsx
@@ -17,11 +17,11 @@ export const SecretSharingSettingsPage = withPermission(
return (
<>
- {t("common.head-title", { title: t("settings.org.title") })}
+ {t("common.head-title", { title: "Secret Share Settings" })}
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/OrgSecretShareLimitSection.tsx
similarity index 100%
rename from frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecretShareLimitSection.tsx
rename to frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/OrgSecretShareLimitSection.tsx
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx
new file mode 100644
index 000000000..1e83c4be8
--- /dev/null
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/OrgSecretShareLimitSection/index.tsx
@@ -0,0 +1 @@
+export { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
diff --git a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx
index ede3d9fc8..ba849507d 100644
--- a/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx
+++ b/frontend/src/pages/organization/SecretSharingSettingsPage/components/SecretSharingSettingsGeneralTab/SecretSharingSettingsGeneralTab.tsx
@@ -1,9 +1,11 @@
+import { OrgSecretShareLimitSection } from "../OrgSecretShareLimitSection";
import { SecretSharingAllowShareToAnyone } from "../SecretSharingAllowShareToAnyone";
export const SecretSharingSettingsGeneralTab = () => {
return (
+
);
};
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx
index 2d402ab21..981681b7d 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgSecurityTab/OrgSecurityTab.tsx
@@ -6,7 +6,6 @@ import { withPermission } from "@app/hoc";
import { OrgGenericAuthSection } from "./OrgGenericAuthSection";
import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
-import { OrgSecretShareLimitSection } from "./OrgSecretShareLimitSection";
export const OrgSecurityTab = withPermission(
() => {
@@ -29,7 +28,6 @@ export const OrgSecurityTab = withPermission(
-
>
);
},
From b5cf237a4a048463d0767042faafb025fa1eb0ee Mon Sep 17 00:00:00 2001
From: x032205
Date: Mon, 19 May 2025 13:35:35 -0400
Subject: [PATCH 13/22] add product select to docs + change the heading
---
docs/documentation/platform/organization.mdx | 5 +++++
.../organization-settings-general.png | Bin 1008764 -> 1016968 bytes
.../OrgProductSelectSection.tsx | 2 +-
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx
index 3a53484fb..a7756ec0f 100644
--- a/docs/documentation/platform/organization.mdx
+++ b/docs/documentation/platform/organization.mdx
@@ -20,6 +20,11 @@ The **Settings** page lets you manage information about your organization includ
- **Slug**: The slug of your organization.
- **Default Organization Member Role**: The role assigned to users when joining your organization unless otherwise specified.
- **Incident Contacts**: Emails that should be alerted if anything abnormal is detected within the organization.
+- **Enabled Products**: Products which are enabled for your organization.
+
+
+ The **enabled products** section strictly affects the sidebar UI and nothing else. Disabling a product does not disable it's API or routes.
+

diff --git a/docs/images/platform/organization/organization-settings-general.png b/docs/images/platform/organization/organization-settings-general.png
index affcf32ff08b6140c59b11220ccabad7bcf72255..9467b6005a423036b2570fcd853001d1681585fd 100644
GIT binary patch
literal 1016968
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