mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 09:28:06 +00:00
feat: secrets expire either with time or views
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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.
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
b. View Count-bound secret: Set the number of views after which the secret will expire.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<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,98 +196,72 @@ 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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="expiryOption"
|
name="expiresAfterViews"
|
||||||
defaultValue="Time"
|
defaultValue={1}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Expire On"
|
className="mb-4 w-full"
|
||||||
errorText={error?.message}
|
label="Expires After Views"
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select
|
<Input {...field} type="number" min={1} />
|
||||||
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>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-4/5">
|
<div className="w-1/7 flex items-center justify-center px-2">
|
||||||
{expiryOption === "Views" ? (
|
<p className="px-4 text-sm text-gray-400">OR</p>
|
||||||
<Controller
|
</div>
|
||||||
control={control}
|
<div className="w-4/7 flex">
|
||||||
name="expiresAfterViews"
|
<div className="flex w-full">
|
||||||
defaultValue={1}
|
<div className="flex w-2/5 w-full justify-center">
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Controller
|
||||||
<FormControl
|
control={control}
|
||||||
className="mb-4 w-full"
|
name="expiresInValue"
|
||||||
label="Expires After Views"
|
defaultValue={1}
|
||||||
isError={Boolean(error)}
|
render={({ field, fieldState: { error } }) => (
|
||||||
errorText={error?.message}
|
<FormControl
|
||||||
>
|
label="Expires after Time"
|
||||||
<Input {...field} type="number" min={1} />
|
isError={Boolean(error)}
|
||||||
</FormControl>
|
errorText={error?.message}
|
||||||
)}
|
>
|
||||||
/>
|
<Input {...field} type="number" min={0} />
|
||||||
) : (
|
</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>
|
</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>
|
</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
|
<p className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}>
|
||||||
className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}
|
{getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)}
|
||||||
>
|
|
||||||
Valid for {row.expiresAfterViews} more views
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
<p className="text-xs text-gray-500">{formatDate(row.expiresAt!)}</p>
|
||||||
<>
|
</>
|
||||||
<p
|
</Td>
|
||||||
className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}
|
<Td>
|
||||||
>
|
<p className={`text-sm ${row.expiresAfterViews <= 0 ? "text-red-500" : "text-green-500"}`}>
|
||||||
{getValidityStatusText(row.expiresAt!) + timeAgo(row.expiresAt!, currentTime)}
|
{row.expiresAfterViews}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">{formatDate(row.expiresAt!)}</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
|
||||||
|
|||||||
Reference in New Issue
Block a user