mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +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) => {
|
||||
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) {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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.
|
||||
|
||||

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

|
||||

|
||||
|
||||
<Note>
|
||||
Secret once set cannot be changed. This is to ensure that the secret is not
|
||||
|
||||
BIN
docs/images/platform/secret-sharing/create-new-secret.png
Normal file
BIN
docs/images/platform/secret-sharing/create-new-secret.png
Normal file
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;
|
||||
tag: string;
|
||||
hashedHex: string;
|
||||
expiresAt?: Date;
|
||||
expiresAfterViews?: number;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number;
|
||||
};
|
||||
|
||||
export type TViewSharedSecretResponse = {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user