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

View File

@@ -8,8 +8,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (!hasExpiresAfterViewsColumn) {
t.integer("expiresAfterViews").nullable();
t.timestamp("expiresAt").nullable().alter();
t.integer("expiresAfterViews");
}
if (hasSecretNameColumn) {
@@ -25,7 +24,6 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
if (hasExpiresAfterViewsColumn) {
t.dropColumn("expiresAfterViews");
t.timestamp("expiresAt").notNullable().alter();
}
if (!hasSecretNameColumn) {

View File

@@ -13,7 +13,7 @@ export const SecretSharingSchema = z.object({
iv: z.string(),
tag: z.string(),
hashedHex: z.string(),
expiresAt: z.date().nullable().optional(),
expiresAt: z.date(),
userId: z.string().uuid(),
orgId: z.string().uuid(),
createdAt: z.date(),

View File

@@ -84,9 +84,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
hashedHex: z.string(),
expiresAt: z
.string()
.optional()
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
expiresAfterViews: z.number().optional()
expiresAfterViews: z.number()
}),
response: {
200: z.object({
@@ -107,7 +106,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
iv,
tag,
hashedHex,
expiresAt: expiresAt ? new Date(expiresAt) : undefined,
expiresAt: new Date(expiresAt),
expiresAfterViews
});
return { id: sharedSecret.id };

View File

@@ -13,8 +13,8 @@ export type TCreateSharedSecretDTO = {
iv: string;
tag: string;
hashedHex: string;
expiresAt?: Date;
expiresAfterViews?: number;
expiresAt: Date;
expiresAfterViews: number;
} & TSharedSecretPermission;
export type TDeleteSharedSecretDTO = {

View File

@@ -1,11 +1,11 @@
---
title: "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.
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.
@@ -21,15 +21,9 @@ With its zero-knowledge architecture, secrets shared via Infisical remain unread
zero knowledge architecture.
</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 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)
![Add View-Bound Sharing Secret](../../images/platform/secret-sharing/create-new-secret.png)
<Note>
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

View File

@@ -11,8 +11,8 @@ export type TCreateSharedSecretRequest = {
iv: string;
tag: string;
hashedHex: string;
expiresAt?: Date;
expiresAfterViews?: number;
expiresAt: Date;
expiresAfterViews: number;
};
export type TViewSharedSecretResponse = {

View File

@@ -20,7 +20,7 @@ import {
ModalContent,
SecretInput,
Select,
SelectItem,
SelectItem
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useTimedReset } from "@app/hooks";
@@ -62,10 +62,9 @@ const expirationUnitsAndActions = [
const schema = yup.object({
value: yup.string().max(10000).required().label("Shared Secret Value"),
expiryOption: yup.string().optional().label("Expiration Option").default("Time"),
expiresAfterViews: yup.number().min(1).optional().label("Expires After Views"),
expiresInValue: yup.number().min(1).optional().label("Expiration Value"),
expiresInUnit: yup.string().optional().label("Expiration Unit")
expiresAfterViews: yup.number().min(1).required().label("Expires After Views"),
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
expiresInUnit: yup.string().required().label("Expiration Unit")
});
export type FormData = yup.InferType<typeof schema>;
@@ -90,7 +89,6 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
const createSharedSecret = useCreateSharedSecret();
const { currentOrg } = useOrganization();
const [newSharedSecret, setnewSharedSecret] = useState("");
const [expiryOption, setExpiryOption] = useState<"Time" | "Views">("Time");
const hasSharedSecret = Boolean(newSharedSecret);
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
initialState: false
@@ -134,8 +132,8 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
iv,
tag,
hashedHex,
expiresAt: expiryOption === "Time" ? expiresAt : undefined,
expiresAfterViews: expiryOption === "Views" ? expiresAfterViews : undefined
expiresAt,
expiresAfterViews
});
setnewSharedSecret(
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
@@ -198,98 +196,72 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
)}
/>
<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 } }) => (
name="expiresAfterViews"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Expire On"
errorText={error?.message}
className="mb-4 w-full"
label="Expires After Views"
isError={Boolean(error)}
errorText={error?.message}
>
<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>
<Input {...field} type="number" min={1} />
</FormControl>
)}
/>
</div>
<div className="flex w-4/5">
{expiryOption === "Views" ? (
<Controller
control={control}
name="expiresAfterViews"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
className="mb-4 w-full"
label="Expires After Views"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="number" min={1} />
</FormControl>
)}
/>
) : (
<div className="flex w-full">
<div className="w-3/5">
<Controller
control={control}
name="expiresInValue"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Expiration Value"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="number" min={0} />
</FormControl>
)}
/>
</div>
<div className="w-2/5 pl-4">
<Controller
control={control}
name="expiresInUnit"
defaultValue={expirationUnitsAndActions[0].unit}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Expiration Unit"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirationUnitsAndActions.map(({ unit }) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</Select>
</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-2/5 w-full justify-center">
<Controller
control={control}
name="expiresInValue"
defaultValue={1}
render={({ field, fieldState: { error } }) => (
<FormControl
label="Expires after Time"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} type="number" min={0} />
</FormControl>
)}
/>
</div>
)}
<div className="flex w-3/5 w-full justify-center">
<Controller
control={control}
name="expiresInUnit"
defaultValue={expirationUnitsAndActions[0].unit}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Unit"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{expirationUnitsAndActions.map(({ unit }) => (
<SelectItem value={unit} key={unit}>
{unit}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
</div>
</div>
</div>
<div className="flex items-center">

View File

@@ -110,22 +110,17 @@ export const ShareSecretsRow = ({
<p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p>
</Td>
<Td>
{row.expiresAfterViews ? (
<p
className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}
>
Valid for {row.expiresAfterViews} more views
<>
<p className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}>
{getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)}
</p>
) : (
<>
<p
className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}
>
{getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)}
</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>
<IconButton

View File

@@ -33,15 +33,11 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
const { isLoading, data = [] } = useGetSharedSecrets();
let tableData = data.filter(
(secret) =>
(secret.expiresAt && new Date(secret.expiresAt) > new Date()) ||
(secret.expiresAfterViews && secret.expiresAfterViews > 0)
(secret) => new Date(secret.expiresAt) > new Date() && secret.expiresAfterViews > 0
);
const handleSecretExpiration = () => {
tableData = data.filter(
(secret) =>
(secret.expiresAt && new Date(secret.expiresAt) > new Date()) ||
(secret.expiresAfterViews && secret.expiresAfterViews > 0)
(secret) => new Date(secret.expiresAt) > new Date() && secret.expiresAfterViews > 0
);
};
@@ -50,7 +46,7 @@ export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
<Table>
<THead>
<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" />
</Tr>
</THead>

View File

@@ -27,7 +27,7 @@ export const SecretTable = ({
)}
{!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 />
</div>
<IconButton