mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: allow sharing of secrets publicly
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.uuid("orgId").nullable().alter();
|
||||
t.uuid("userId").nullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.SecretSharing)) {
|
||||
await knex.schema.alterTable(TableName.SecretSharing, (t) => {
|
||||
t.uuid("orgId").notNullable().alter();
|
||||
t.uuid("userId").notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ export const SecretSharingSchema = z.object({
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z.date(),
|
||||
userId: z.string().uuid(),
|
||||
orgId: z.string().uuid(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
expiresAfterViews: z.number().nullable().optional()
|
||||
|
||||
@@ -70,6 +70,43 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/public",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
encryptedValue: z.string(),
|
||||
iv: z.string(),
|
||||
tag: z.string(),
|
||||
hashedHex: z.string(),
|
||||
expiresAt: z
|
||||
.string()
|
||||
.refine((date) => date === undefined || new Date(date) > new Date(), "Expires at should be a future date"),
|
||||
expiresAfterViews: z.number()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
id: z.string().uuid()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = req.body;
|
||||
const sharedSecret = await req.server.services.secretSharing.createPublicSharedSecret({
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt: new Date(expiresAt),
|
||||
expiresAfterViews
|
||||
});
|
||||
return { id: sharedSecret.id };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/",
|
||||
|
||||
@@ -2,7 +2,12 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
|
||||
import {
|
||||
TCreatePublicSharedSecretDTO,
|
||||
TCreateSharedSecretDTO,
|
||||
TDeleteSharedSecretDTO,
|
||||
TSharedSecretPermission
|
||||
} from "./secret-sharing-types";
|
||||
|
||||
type TSecretSharingServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
@@ -44,6 +49,19 @@ export const secretSharingServiceFactory = ({
|
||||
return { id: newSharedSecret.id };
|
||||
};
|
||||
|
||||
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
|
||||
const newSharedSecret = await secretSharingDAL.create({
|
||||
encryptedValue,
|
||||
iv,
|
||||
tag,
|
||||
hashedHex,
|
||||
expiresAt,
|
||||
expiresAfterViews
|
||||
});
|
||||
return { id: newSharedSecret.id };
|
||||
};
|
||||
|
||||
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
@@ -77,6 +95,7 @@ export const secretSharingServiceFactory = ({
|
||||
|
||||
return {
|
||||
createSharedSecret,
|
||||
createPublicSharedSecret,
|
||||
getSharedSecrets,
|
||||
deleteSharedSecretById,
|
||||
getActiveSharedSecretByIdAndHashedHex
|
||||
|
||||
@@ -8,14 +8,16 @@ export type TSharedSecretPermission = {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = {
|
||||
export type TCreatePublicSharedSecretDTO = {
|
||||
encryptedValue: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
hashedHex: string;
|
||||
expiresAt: Date;
|
||||
expiresAfterViews: number;
|
||||
} & TSharedSecretPermission;
|
||||
};
|
||||
|
||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||
|
||||
export type TDeleteSharedSecretDTO = {
|
||||
sharedSecretId: string;
|
||||
|
||||
@@ -15,13 +15,23 @@ export const useCreateSharedSecret = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreatePublicSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.post<TSharedSecret>(
|
||||
"/api/v1/secret-sharing/public",
|
||||
inputData
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSharedSecret = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<
|
||||
TSharedSecret,
|
||||
{ message: string },
|
||||
{ sharedSecretId: string }
|
||||
>({
|
||||
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||
`/api/v1/secret-sharing/${sharedSecretId}`
|
||||
|
||||
@@ -22,9 +22,8 @@ import {
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization } from "@app/context";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
const expirationUnitsAndActions = [
|
||||
@@ -65,9 +64,10 @@ type Props = {
|
||||
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
isPublic: boolean;
|
||||
};
|
||||
|
||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
export const AddShareSecretModal = ({ popUp, handlePopUpToggle, isPublic }: Props) => {
|
||||
const {
|
||||
control,
|
||||
reset,
|
||||
@@ -76,8 +76,10 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
} = useForm<FormData>({
|
||||
resolver: yupResolver(schema)
|
||||
});
|
||||
const createSharedSecret = useCreateSharedSecret();
|
||||
const { currentOrg } = useOrganization();
|
||||
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
||||
|
||||
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||
const hasSharedSecret = Boolean(newSharedSecret);
|
||||
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||
@@ -101,7 +103,6 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
expiresAfterViews
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!currentOrg?.id) return;
|
||||
const key = crypto.randomBytes(16).toString("hex");
|
||||
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
@@ -180,7 +181,7 @@ export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<SecretInput
|
||||
isVisible={false}
|
||||
{...field}
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[100px]"
|
||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 min-h-[70px]"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ShareSecretSection = () => {
|
||||
const onDeleteApproved = async () => {
|
||||
try {
|
||||
deleteSharedSecret.mutateAsync({
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id,
|
||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully deleted shared secret",
|
||||
@@ -40,7 +40,6 @@ export const ShareSecretSection = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<Head>
|
||||
<title>Secret Sharing</title>
|
||||
@@ -60,14 +59,13 @@ export const ShareSecretSection = () => {
|
||||
Share Secret
|
||||
</Button>
|
||||
</div>
|
||||
<ShareSecretsTable
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
/>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||
<ShareSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic={false} />
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} shared secret?`}
|
||||
title={`Delete ${
|
||||
(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||
} shared secret?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
|
||||
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
|
||||
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
|
||||
@@ -75,4 +73,4 @@ export const ShareSecretSection = () => {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import Head from "next/head";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { usePopUp, useTimedReset } from "@app/hooks";
|
||||
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
||||
|
||||
import { DragonMainImage, SecretTable } from "./components";
|
||||
import { AddShareSecretModal } from "../ShareSecretPage/components/AddShareSecretModal";
|
||||
import { SecretTable } from "./components";
|
||||
|
||||
export const ShareSecretPublicPage = () => {
|
||||
const router = useRouter();
|
||||
@@ -53,36 +58,92 @@ export const ShareSecretPublicPage = () => {
|
||||
navigator.clipboard.writeText(decryptedSecret);
|
||||
setIsUrlCopied(true);
|
||||
};
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["createSharedSecret"] as const);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
|
||||
<div className="flex h-screen flex-col bg-bunker-800 text-gray-200">
|
||||
<Head>
|
||||
<title>Secret Shared | Infisical</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
|
||||
<div className="my-4 flex justify-center md:my-8">
|
||||
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<Link href="https://infisical.com">
|
||||
<Image
|
||||
src="/images/biglogo.png"
|
||||
height={60}
|
||||
width={80}
|
||||
alt="Infisical logo"
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
|
||||
A secret has been shared with you securely via Infisical
|
||||
</p>
|
||||
<div className="flex min-h-screen w-full flex-col md:flex-row">
|
||||
<DragonMainImage />
|
||||
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
|
||||
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
|
||||
Shared Secret
|
||||
|
||||
<div className="flex flex-1 flex-col items-center justify-center px-4">
|
||||
<div className="flex w-full max-w-4xl flex-col items-center gap-4 md:gap-20">
|
||||
<p className="text-center text-xl font-semibold text-gray-200 md:text-3xl">
|
||||
Secret Shared via Infisical
|
||||
</p>
|
||||
<div className="mb-4 rounded-lg md:p-2">
|
||||
<SecretTable
|
||||
isLoading={isLoading}
|
||||
decryptedSecret={decryptedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
<div className="flex w-full flex-grow flex-col gap-6 md:flex-row md:gap-12">
|
||||
<div className="flex-1 self-center pt-4 text-center md:pt-0 md:text-left">
|
||||
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||
Safe & Secure
|
||||
</p>
|
||||
<p className="md:text-md text-sm">
|
||||
Infisical uses <span className="text-primary">Zero Knowledge</span> to ensure that
|
||||
your secrets are truly private (even from us).
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 rounded-lg p-2">
|
||||
<SecretTable
|
||||
isLoading={isLoading}
|
||||
decryptedSecret={decryptedSecret}
|
||||
isUrlCopied={isUrlCopied}
|
||||
copyUrlToClipboard={copyUrlToClipboard}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 self-center text-center md:text-right">
|
||||
<p className="pb-2 font-semibold text-mineshaft-100 md:pb-4 md:text-xl">
|
||||
Open Source
|
||||
</p>
|
||||
<p className="md:text-md text-sm">
|
||||
Infisical is open source. <br className="hidden md:inline" />
|
||||
Check us out on{" "}
|
||||
<a
|
||||
href="https://github.com/infisical/infisical"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-4 max-w-[600px] md:mt-0"
|
||||
colorSchema="primary"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("createSharedSecret");
|
||||
}}
|
||||
>
|
||||
Share your own Secret
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} isPublic />
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<p className="text-center text-sm text-gray-200">
|
||||
Developed by{" "}
|
||||
<a className="text-primary" href="https://infisical.com">
|
||||
Infisical
|
||||
</a>
|
||||
<br />
|
||||
Open Source Secret Management{" "}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import Image from "next/image";
|
||||
|
||||
export const DragonMainImage = () => {
|
||||
return (
|
||||
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
|
||||
<Image
|
||||
src="/images/dragon-book.svg"
|
||||
height={1000}
|
||||
width={1413}
|
||||
alt="Infisical Dragon - Came to send you a secret!"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
export { DragonMainImage } from "./MainImage";
|
||||
export { SecretTable } from "./SecretTable";
|
||||
|
||||
Reference in New Issue
Block a user