feat: secrets expire either with time or views

This commit is contained in:
ShubhamPalriwala
2024-05-31 12:47:19 +05:30
parent e7d32b5f2d
commit 29b106c5bd
15 changed files with 88 additions and 134 deletions
@@ -8,8 +8,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretSharing, (t) => { await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (!hasExpiresAfterViewsColumn) { if (!hasExpiresAfterViewsColumn) {
t.integer("expiresAfterViews").nullable(); t.integer("expiresAfterViews");
t.timestamp("expiresAt").nullable().alter();
} }
if (hasSecretNameColumn) { if (hasSecretNameColumn) {
@@ -25,7 +24,6 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretSharing, (t) => { await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (hasExpiresAfterViewsColumn) { if (hasExpiresAfterViewsColumn) {
t.dropColumn("expiresAfterViews"); t.dropColumn("expiresAfterViews");
t.timestamp("expiresAt").notNullable().alter();
} }
if (!hasSecretNameColumn) { if (!hasSecretNameColumn) {
+1 -1
View File
@@ -13,7 +13,7 @@ export const SecretSharingSchema = z.object({
iv: z.string(), iv: z.string(),
tag: z.string(), tag: z.string(),
hashedHex: z.string(), hashedHex: z.string(),
expiresAt: z.date().nullable().optional(), expiresAt: z.date(),
userId: z.string().uuid(), userId: z.string().uuid(),
orgId: z.string().uuid(), orgId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
@@ -84,9 +84,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
hashedHex: z.string(), hashedHex: z.string(),
expiresAt: z expiresAt: z
.string() .string()
.optional()
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"), .refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
expiresAfterViews: z.number().optional() expiresAfterViews: z.number()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -107,7 +106,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
iv, iv,
tag, tag,
hashedHex, hashedHex,
expiresAt: expiresAt ? new Date(expiresAt) : undefined, expiresAt: new Date(expiresAt),
expiresAfterViews expiresAfterViews
}); });
return { id: sharedSecret.id }; return { id: sharedSecret.id };
@@ -13,8 +13,8 @@ export type TCreateSharedSecretDTO = {
iv: string; iv: string;
tag: string; tag: string;
hashedHex: string; hashedHex: string;
expiresAt?: Date; expiresAt: Date;
expiresAfterViews?: number; expiresAfterViews: number;
} & TSharedSecretPermission; } & TSharedSecretPermission;
export type TDeleteSharedSecretDTO = { export type TDeleteSharedSecretDTO = {
+4 -10
View File
@@ -1,11 +1,11 @@
--- ---
title: "Secret Sharing" title: "Secret Sharing"
sidebarTitle: "Secret Sharing" sidebarTitle: "Secret Sharing"
description: "Learn how to share time or view-count bound secrets securely with anyone on the internet." description: "Learn how to share time & view-count bound secrets securely with anyone on the internet."
--- ---
Developers frequently need to share secrets with team members, contractors, or other third parties, which can be risky due to potential leaks or misuse. Developers frequently need to share secrets with team members, contractors, or other third parties, which can be risky due to potential leaks or misuse.
Infisical offers a secure solution for sharing secrets over the internet in a time-bound manner as well as view count bound manner. Infisical offers a secure solution for sharing secrets over the internet in a time and view count bound manner.
With its zero-knowledge architecture, secrets shared via Infisical remain unreadable even to Infisical itself. With its zero-knowledge architecture, secrets shared via Infisical remain unreadable even to Infisical itself.
@@ -21,15 +21,9 @@ With its zero-knowledge architecture, secrets shared via Infisical remain unread
zero knowledge architecture. zero knowledge architecture.
</Note> </Note>
3. Click on the **Share Secret** button. 3. Click on the **Share Secret** button. Set the secret, its expiration time as well as the number of views allowed. It expires as soon as any of the conditions are met.
a. Time-bound secret: Set the expiration time in minutes, hours, days, or weeks. ![Add View-Bound Sharing Secret](../../images/platform/secret-sharing/create-new-secret.png)
![Add Time-Bound Sharing Secret](../../images/platform/secret-sharing/new-time-bound-secret.png)
b. View Count-bound secret: Set the number of views after which the secret will expire.
![Add View-Bound Sharing Secret](../../images/platform/secret-sharing/new-view-bound-secret.png)
<Note> <Note>
Secret once set cannot be changed. This is to ensure that the secret is not Secret once set cannot be changed. This is to ensure that the secret is not
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 467 KiB

@@ -11,8 +11,8 @@ export type TCreateSharedSecretRequest = {
iv: string; iv: string;
tag: string; tag: string;
hashedHex: string; hashedHex: string;
expiresAt?: Date; expiresAt: Date;
expiresAfterViews?: number; expiresAfterViews: number;
}; };
export type TViewSharedSecretResponse = { export type TViewSharedSecretResponse = {
@@ -20,7 +20,7 @@ import {
ModalContent, ModalContent,
SecretInput, SecretInput,
Select, Select,
SelectItem, SelectItem
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
@@ -62,10 +62,9 @@ const expirationUnitsAndActions = [
const schema = yup.object({ const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"), value: yup.string().max(10000).required().label("Shared Secret Value"),
expiryOption: yup.string().optional().label("Expiration Option").default("Time"), expiresAfterViews: yup.number().min(1).required().label("Expires After Views"),
expiresAfterViews: yup.number().min(1).optional().label("Expires After Views"), expiresInValue: yup.number().min(1).required().label("Expiration Value"),
expiresInValue: yup.number().min(1).optional().label("Expiration Value"), expiresInUnit: yup.string().required().label("Expiration Unit")
expiresInUnit: yup.string().optional().label("Expiration Unit")
}); });
export type FormData = yup.InferType<typeof schema>; export type FormData = yup.InferType<typeof schema>;
@@ -90,7 +89,6 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
const createSharedSecret = useCreateSharedSecret(); const createSharedSecret = useCreateSharedSecret();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const [newSharedSecret, setnewSharedSecret] = useState(""); const [newSharedSecret, setnewSharedSecret] = useState("");
const [expiryOption, setExpiryOption] = useState<"Time" | "Views">("Time");
const hasSharedSecret = Boolean(newSharedSecret); const hasSharedSecret = Boolean(newSharedSecret);
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({ const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false initialState: false
@@ -134,8 +132,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
iv, iv,
tag, tag,
hashedHex, hashedHex,
expiresAt: expiryOption === "Time" ? expiresAt : undefined, expiresAt,
expiresAfterViews: expiryOption === "Views" ? expiresAfterViews : undefined expiresAfterViews
}); });
setnewSharedSecret( setnewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent( `${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
@@ -198,36 +196,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
)} )}
/> />
<div className="flex w-full flex-row"> <div className="flex w-full flex-row">
<div className="flex w-1/5"> <div className="w-2/7 flex">
<Controller
control={control}
name="expiryOption"
defaultValue="Time"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Expire On"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e: "Time" | "Views") => setExpiryOption(e)}
value={expiryOption}
className="w-full"
>
{["Time", "Views"].map((unit) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-4/5">
{expiryOption === "Views" ? (
<Controller <Controller
control={control} control={control}
name="expiresAfterViews" name="expiresAfterViews"
@@ -243,16 +212,20 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
) : ( </div>
<div className="w-1/7 flex items-center justify-center px-2">
<p className="px-4 text-sm text-gray-400">OR</p>
</div>
<div className="w-4/7 flex">
<div className="flex w-full"> <div className="flex w-full">
<div className="w-3/5"> <div className="flex w-2/5 w-full justify-center">
<Controller <Controller
control={control} control={control}
name="expiresInValue" name="expiresInValue"
defaultValue={1} defaultValue={1}
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Expiration Value" label="Expires after Time"
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
@@ -261,14 +234,14 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
)} )}
/> />
</div> </div>
<div className="w-2/5 pl-4"> <div className="flex w-3/5 w-full justify-center">
<Controller <Controller
control={control} control={control}
name="expiresInUnit" name="expiresInUnit"
defaultValue={expirationUnitsAndActions[0].unit} defaultValue={expirationUnitsAndActions[0].unit}
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Expiration Unit" label="Unit"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
@@ -289,7 +262,6 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
/> />
</div> </div>
</div> </div>
)}
</div> </div>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
@@ -110,22 +110,17 @@ export const ShareSecretsRow = ({
<p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p> <p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p>
</Td> </Td>
<Td> <Td>
{row.expiresAfterViews ? (
<p
className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}
>
Valid for {row.expiresAfterViews} more views
</p>
) : (
<> <>
<p <p className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}>
className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}
>
{getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)} {getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)}
</p> </p>
<p className="text-xs text-gray-500">{formatDate(row.expiresAt!)}</p> <p className="text-xs text-gray-500">{formatDate(row.expiresAt!)}</p>
</> </>
)} </Td>
<Td>
<p className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}>
{row.expiresAfterViews}
</p>
</Td> </Td>
<Td> <Td>
<IconButton <IconButton
@@ -33,15 +33,11 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
const { isLoading, data = [] } = useGetSharedSecrets(); const { isLoading, data = [] } = useGetSharedSecrets();
let tableData = data.filter( let tableData = data.filter(
(secret) => (secret) => new Date(secret.expiresAt) > new Date() && secret.expiresAfterViews > 0
(secret.expiresAt && new Date(secret.expiresAt) > new Date()) ||
(secret.expiresAfterViews && secret.expiresAfterViews > 0)
); );
const handleSecretExpiration = () => { const handleSecretExpiration = () => {
tableData = data.filter( tableData = data.filter(
(secret) => (secret) => new Date(secret.expiresAt) > new Date() && secret.expiresAfterViews > 0
(secret.expiresAt && new Date(secret.expiresAt) > new Date()) ||
(secret.expiresAfterViews && secret.expiresAfterViews > 0)
); );
}; };
@@ -50,7 +46,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
<Table> <Table>
<THead> <THead>
<Tr> <Tr>
<Th>Encrypted Secret</Th> <Th>Created</Th> <Th>Valid Until</Th> <Th>Encrypted Secret</Th> <Th>Created</Th> <Th>Valid Until</Th> <Th>Views Left</Th>
<Th aria-label="button" /> <Th aria-label="button" />
</Tr> </Tr>
</THead> </THead>
@@ -27,7 +27,7 @@ export const SecretTable = ({
)} )}
{!isLoading && decryptedSecret && ( {!isLoading && decryptedSecret && (
<> <>
<div className="max-w-[20rem] flex-1 break-words pr-4"> <div className="min-w-[12rem] max-w-[20rem] flex-1 break-words pr-4">
<SecretInput isVisible value={decryptedSecret} readOnly /> <SecretInput isVisible value={decryptedSecret} readOnly />
</div> </div>
<IconButton <IconButton