mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 09:26:47 +00:00
Merge pull request #2152 from aheruz/feat/ENG-985-secret-share-organization
feat(ENG-985): secret share within organization
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
|
||||||
|
if (!hasColumn) {
|
||||||
|
await knex.schema.table(TableName.SecretSharing, (table) => {
|
||||||
|
table.string("accessType").notNullable().defaultTo(SecretSharingAccessType.Anyone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasColumn = await knex.schema.hasColumn(TableName.SecretSharing, "accessType");
|
||||||
|
if (hasColumn) {
|
||||||
|
await knex.schema.table(TableName.SecretSharing, (table) => {
|
||||||
|
table.dropColumn("accessType");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
@@ -16,6 +18,7 @@ export const SecretSharingSchema = z.object({
|
|||||||
expiresAt: z.date(),
|
expiresAt: z.date(),
|
||||||
userId: z.string().uuid().nullable().optional(),
|
userId: z.string().uuid().nullable().optional(),
|
||||||
orgId: z.string().uuid().nullable().optional(),
|
orgId: z.string().uuid().nullable().optional(),
|
||||||
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
expiresAfterViews: z.number().nullable().optional()
|
expiresAfterViews: z.number().nullable().optional()
|
||||||
|
|||||||
@@ -47,3 +47,8 @@ export enum EnforcementLevel {
|
|||||||
Hard = "hard",
|
Hard = "hard",
|
||||||
Soft = "soft"
|
Soft = "soft"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SecretSharingAccessType {
|
||||||
|
Anyone = "anyone",
|
||||||
|
Organization = "organization"
|
||||||
|
}
|
||||||
|
|||||||
@@ -737,7 +737,8 @@ export const registerRoutes = async (
|
|||||||
|
|
||||||
const secretSharingService = secretSharingServiceFactory({
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL
|
secretSharingDAL,
|
||||||
|
orgDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
const secretApprovalRequestService = secretApprovalRequestServiceFactory({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSharingSchema } from "@app/db/schemas";
|
import { SecretSharingSchema } from "@app/db/schemas";
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
import {
|
import {
|
||||||
publicEndpointLimit,
|
publicEndpointLimit,
|
||||||
publicSecretShareCreationLimit,
|
publicSecretShareCreationLimit,
|
||||||
@@ -55,14 +56,18 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
iv: true,
|
iv: true,
|
||||||
tag: true,
|
tag: true,
|
||||||
expiresAt: true,
|
expiresAt: true,
|
||||||
expiresAfterViews: true
|
expiresAfterViews: true,
|
||||||
|
accessType: true
|
||||||
|
}).extend({
|
||||||
|
orgName: z.string().optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
|
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
|
||||||
req.params.id,
|
req.params.id,
|
||||||
req.query.hashedHex
|
req.query.hashedHex,
|
||||||
|
req.permission?.orgId
|
||||||
);
|
);
|
||||||
if (!sharedSecret) return undefined;
|
if (!sharedSecret) return undefined;
|
||||||
return {
|
return {
|
||||||
@@ -70,7 +75,9 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
iv: sharedSecret.iv,
|
iv: sharedSecret.iv,
|
||||||
tag: sharedSecret.tag,
|
tag: sharedSecret.tag,
|
||||||
expiresAt: sharedSecret.expiresAt,
|
expiresAt: sharedSecret.expiresAt,
|
||||||
expiresAfterViews: sharedSecret.expiresAfterViews
|
expiresAfterViews: sharedSecret.expiresAfterViews,
|
||||||
|
accessType: sharedSecret.accessType,
|
||||||
|
orgName: sharedSecret.orgName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -104,7 +111,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews
|
expiresAfterViews,
|
||||||
|
accessType: SecretSharingAccessType.Anyone
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
}
|
}
|
||||||
@@ -123,7 +131,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
tag: z.string(),
|
tag: z.string(),
|
||||||
hashedHex: z.string(),
|
hashedHex: z.string(),
|
||||||
expiresAt: z.string(),
|
expiresAt: z.string(),
|
||||||
expiresAfterViews: z.number()
|
expiresAfterViews: z.number(),
|
||||||
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -145,7 +154,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews
|
expiresAfterViews,
|
||||||
|
accessType: req.body.accessType
|
||||||
});
|
});
|
||||||
return { id: sharedSecret.id };
|
return { id: sharedSecret.id };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import {
|
import {
|
||||||
TCreatePublicSharedSecretDTO,
|
TCreatePublicSharedSecretDTO,
|
||||||
@@ -12,13 +14,15 @@ import {
|
|||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
secretSharingDAL: TSecretSharingDALFactory;
|
secretSharingDAL: TSecretSharingDALFactory;
|
||||||
|
orgDAL: TOrgDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||||
|
|
||||||
export const secretSharingServiceFactory = ({
|
export const secretSharingServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL
|
secretSharingDAL,
|
||||||
|
orgDAL
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
||||||
const {
|
const {
|
||||||
@@ -30,6 +34,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
encryptedValue,
|
encryptedValue,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
|
accessType,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews
|
expiresAfterViews
|
||||||
@@ -62,13 +67,14 @@ export const secretSharingServiceFactory = ({
|
|||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
orgId
|
orgId,
|
||||||
|
accessType
|
||||||
});
|
});
|
||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
const createPublicSharedSecret = async (createSharedSecretInput: TCreatePublicSharedSecretDTO) => {
|
||||||
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews } = createSharedSecretInput;
|
const { encryptedValue, iv, tag, hashedHex, expiresAt, expiresAfterViews, accessType } = createSharedSecretInput;
|
||||||
if (new Date(expiresAt) < new Date()) {
|
if (new Date(expiresAt) < new Date()) {
|
||||||
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
throw new BadRequestError({ message: "Expiration date cannot be in the past" });
|
||||||
}
|
}
|
||||||
@@ -92,7 +98,8 @@ export const secretSharingServiceFactory = ({
|
|||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews
|
expiresAfterViews,
|
||||||
|
accessType
|
||||||
});
|
});
|
||||||
return { id: newSharedSecret.id };
|
return { id: newSharedSecret.id };
|
||||||
};
|
};
|
||||||
@@ -105,9 +112,21 @@ export const secretSharingServiceFactory = ({
|
|||||||
return userSharedSecrets;
|
return userSharedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string, orgId?: string) => {
|
||||||
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||||
if (!sharedSecret) return;
|
if (!sharedSecret) return;
|
||||||
|
|
||||||
|
const orgName = sharedSecret.orgId ? (await orgDAL.findOrgById(sharedSecret.orgId))?.name : "";
|
||||||
|
// Support organization level access for secret sharing
|
||||||
|
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId !== sharedSecret.orgId) {
|
||||||
|
return {
|
||||||
|
...sharedSecret,
|
||||||
|
encryptedValue: "",
|
||||||
|
iv: "",
|
||||||
|
tag: "",
|
||||||
|
orgName
|
||||||
|
};
|
||||||
|
}
|
||||||
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
if (sharedSecret.expiresAt && sharedSecret.expiresAt < new Date()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -118,7 +137,10 @@ export const secretSharingServiceFactory = ({
|
|||||||
}
|
}
|
||||||
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
await secretSharingDAL.updateById(sharedSecretId, { $decr: { expiresAfterViews: 1 } });
|
||||||
}
|
}
|
||||||
return sharedSecret;
|
if (sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId) {
|
||||||
|
return { ...sharedSecret, orgName };
|
||||||
|
}
|
||||||
|
return { ...sharedSecret, orgName: undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
|
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
export type TSharedSecretPermission = {
|
export type TSharedSecretPermission = {
|
||||||
@@ -6,6 +8,7 @@ export type TSharedSecretPermission = {
|
|||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
|
accessType?: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreatePublicSharedSecretDTO = {
|
export type TCreatePublicSharedSecretDTO = {
|
||||||
@@ -15,6 +18,7 @@ export type TCreatePublicSharedSecretDTO = {
|
|||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews: number;
|
expiresAfterViews: number;
|
||||||
|
accessType: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ 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. 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.
|
3. Click on the **Share Secret** button. Set the secret, its expiration time and specify if the secret can be viewed only once. It expires as soon as any of the conditions are met.
|
||||||
|
Also, specify if the secret can be accessed by anyone or only people within your organization.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 39 KiB |
@@ -21,7 +21,7 @@ export const EmptyState = ({
|
|||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex w-full flex-col items-center bg-mineshaft-800 px-2 pt-6 text-bunker-300",
|
"flex w-full flex-col items-center bg-mineshaft-800 px-2 pt-4 text-bunker-300",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
|
import { SecretSharingAccessType, TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||||
|
|
||||||
export const useGetSharedSecrets = () => {
|
export const useGetSharedSecrets = () => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
@@ -17,7 +17,7 @@ export const useGetSharedSecrets = () => {
|
|||||||
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||||
return useQuery<TViewSharedSecretResponse, [string]>({
|
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "" });
|
if(!id || !hashedHex) return Promise.resolve({ encryptedValue: "", iv: "", tag: "", accessType: SecretSharingAccessType.Organization, orgName: "" });
|
||||||
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||||
);
|
);
|
||||||
@@ -25,6 +25,8 @@ export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex:
|
|||||||
encryptedValue: data.encryptedValue,
|
encryptedValue: data.encryptedValue,
|
||||||
iv: data.iv,
|
iv: data.iv,
|
||||||
tag: data.tag,
|
tag: data.tag,
|
||||||
|
accessType: data.accessType,
|
||||||
|
orgName: data.orgName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,14 +13,22 @@ export type TCreateSharedSecretRequest = {
|
|||||||
hashedHex: string;
|
hashedHex: string;
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews: number;
|
expiresAfterViews: number;
|
||||||
|
accessType: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TViewSharedSecretResponse = {
|
export type TViewSharedSecretResponse = {
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
|
accessType: SecretSharingAccessType;
|
||||||
|
orgName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteSharedSecretRequest = {
|
export type TDeleteSharedSecretRequest = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum SecretSharingAccessType {
|
||||||
|
Anyone = "anyone",
|
||||||
|
Organization = "organization"
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -7,13 +8,14 @@ import * as yup from "yup";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
import { encryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
import { Button, Checkbox, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
import { Button, Checkbox, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
|
||||||
import { useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
import { SecretSharingAccessType, useCreatePublicSharedSecret, useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
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"),
|
||||||
expiresAfterSingleView: yup.boolean().required().label("Expires After Views"),
|
expiresAfterSingleView: yup.boolean().required().label("Expires After Views"),
|
||||||
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
||||||
expiresInUnit: yup.string().required().label("Expiration Unit")
|
expiresInUnit: yup.string().required().label("Expiration Unit"),
|
||||||
|
accessType: yup.string().required().label("General Access")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -35,6 +37,14 @@ export const AddShareSecretForm = ({
|
|||||||
setNewSharedSecret: (value: string) => void;
|
setNewSharedSecret: (value: string) => void;
|
||||||
isInputDisabled?: boolean;
|
isInputDisabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const isMounted = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
const publicSharedSecretCreator = useCreatePublicSharedSecret();
|
||||||
const privateSharedSecretCreator = useCreateSharedSecret();
|
const privateSharedSecretCreator = useCreateSharedSecret();
|
||||||
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
const createSharedSecret = isPublic ? publicSharedSecretCreator : privateSharedSecretCreator;
|
||||||
@@ -65,7 +75,8 @@ export const AddShareSecretForm = ({
|
|||||||
value,
|
value,
|
||||||
expiresInValue,
|
expiresInValue,
|
||||||
expiresInUnit,
|
expiresInUnit,
|
||||||
expiresAfterSingleView
|
expiresAfterSingleView,
|
||||||
|
accessType
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
const key = crypto.randomBytes(16).toString("hex");
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
@@ -89,18 +100,21 @@ export const AddShareSecretForm = ({
|
|||||||
tag,
|
tag,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
expiresAfterViews: expiresAfterSingleView ? 1 : 1000
|
expiresAfterViews: expiresAfterSingleView ? 1 : 1000,
|
||||||
|
accessType: accessType as SecretSharingAccessType
|
||||||
});
|
});
|
||||||
setNewSharedSecret(
|
|
||||||
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
|
||||||
hashedHex
|
|
||||||
)}-${encodeURIComponent(key)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
createNotification({
|
if (isMounted.current) {
|
||||||
text: "Successfully created a shared secret",
|
setNewSharedSecret(
|
||||||
type: "success"
|
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(
|
||||||
});
|
hashedHex
|
||||||
|
)}-${encodeURIComponent(key)}`
|
||||||
|
);
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created a shared secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
const axiosError = err as AxiosError;
|
const axiosError = err as AxiosError;
|
||||||
@@ -143,7 +157,7 @@ export const AddShareSecretForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col md:flex-row justify-start">
|
<div className="flex w-full flex-col md:flex-row justify-stretch">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<div className="flex w-full justify-center pr-2">
|
<div className="flex w-full justify-center pr-2">
|
||||||
@@ -188,8 +202,8 @@ export const AddShareSecretForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-1/7 mx-auto items-center justify-center px-6 hidden md:flex">
|
<div className="sm:w-1/7 mx-auto items-center justify-center hidden md:flex">
|
||||||
<p className="px-4 mt-2 text-sm text-gray-400">AND</p>
|
<p className="mt-2 text-sm text-gray-400">AND</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="items-center pb-4 md:pb-0 md:pt-2 flex">
|
<div className="items-center pb-4 md:pb-0 md:pt-2 flex">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -227,7 +241,25 @@ export const AddShareSecretForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex items-center ${!inModal && "justify-start pt-2"}`}>
|
{!isPublic && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessType"
|
||||||
|
defaultValue="organization"
|
||||||
|
render={({ field: { onChange, ...field } }) => (
|
||||||
|
<FormControl label="General Access">
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
>
|
||||||
|
<SelectItem value="organization">People within your organization</SelectItem>
|
||||||
|
<SelectItem value="anyone">Anyone</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={`flex items-center space-x-4 pt-2 ${!inModal && ""}`}>
|
||||||
<Button className="mr-0" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
<Button className="mr-0" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
{inModal ? "Create" : "Share Secret"}
|
{inModal ? "Create" : "Share Secret"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
|
|||||||
const [hashedHex, key] = urlEncodedPublicKey
|
const [hashedHex, key] = urlEncodedPublicKey
|
||||||
? urlEncodedPublicKey.toString().split("-")
|
? urlEncodedPublicKey.toString().split("-")
|
||||||
: ["", ""];
|
: ["", ""];
|
||||||
|
|
||||||
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||||
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(
|
||||||
id as string,
|
id as string,
|
||||||
hashedHex as string
|
hashedHex as string
|
||||||
);
|
);
|
||||||
|
const accessType = data?.accessType;
|
||||||
|
const orgName = data?.orgName;
|
||||||
|
|
||||||
const decryptedSecret = useMemo(() => {
|
const decryptedSecret = useMemo(() => {
|
||||||
if (data && data.encryptedValue && publicKey) {
|
if (data && data.encryptedValue && publicKey) {
|
||||||
@@ -87,6 +89,8 @@ export const ShareSecretPublicPage = ({ isNewSession }: { isNewSession: boolean
|
|||||||
decryptedSecret={decryptedSecret}
|
decryptedSecret={decryptedSecret}
|
||||||
isUrlCopied={isUrlCopied}
|
isUrlCopied={isUrlCopied}
|
||||||
copyUrlToClipboard={copyUrlToClipboard}
|
copyUrlToClipboard={copyUrlToClipboard}
|
||||||
|
accessType={accessType}
|
||||||
|
orgName={orgName}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowRight, faCheck, faCopy, faEye, faEyeSlash, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { EmptyState, IconButton, Td, Tr } from "@app/components/v2";
|
import { Button, EmptyState, IconButton, Td, Tr } from "@app/components/v2";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { SecretSharingAccessType } from "@app/hooks/api/secretSharing/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
decryptedSecret: string;
|
decryptedSecret: string;
|
||||||
isUrlCopied: boolean;
|
isUrlCopied: boolean;
|
||||||
copyUrlToClipboard: () => void;
|
copyUrlToClipboard: () => void;
|
||||||
|
accessType?: SecretSharingAccessType;
|
||||||
|
orgName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const replaceContentWithDot = (str: string) => {
|
const replaceContentWithDot = (str: string) => {
|
||||||
@@ -24,20 +27,49 @@ export const SecretTable = ({
|
|||||||
isLoading,
|
isLoading,
|
||||||
decryptedSecret,
|
decryptedSecret,
|
||||||
isUrlCopied,
|
isUrlCopied,
|
||||||
copyUrlToClipboard
|
copyUrlToClipboard,
|
||||||
|
accessType,
|
||||||
|
orgName
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isVisible, setIsVisible] = useToggle(false);
|
const [isVisible, setIsVisible] = useToggle(false);
|
||||||
|
const title = orgName
|
||||||
|
? (<p>Someone from <strong>{orgName}</strong> organization has shared a secret with you</p>)
|
||||||
|
: (<p>You need to be logged in to view this secret</p>);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
|
<div className="flex w-full items-center justify-center rounded-md border border-solid border-mineshaft-700 bg-mineshaft-800 p-2">
|
||||||
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
{isLoading && <div className="bg-mineshaft-800 text-center text-bunker-400">Loading...</div>}
|
||||||
{!isLoading && !decryptedSecret && (
|
{!isLoading && !decryptedSecret && accessType !== SecretSharingAccessType.Organization && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
|
<EmptyState title="Secret has either expired or does not exist!" icon={faKey} />
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
|
{!isLoading && !decryptedSecret && accessType === SecretSharingAccessType.Organization && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-4000">
|
||||||
|
<EmptyState title={title} icon={faKey}>
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center pt-6">
|
||||||
|
<a
|
||||||
|
href="/login"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {}}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="ml-2" />}
|
||||||
|
>
|
||||||
|
Login into <strong>{orgName}</strong> to view this secret
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</EmptyState>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
{!isLoading && decryptedSecret && (
|
{!isLoading && decryptedSecret && (
|
||||||
<div className="dark relative flex h-full w-full items-center overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 pr-2 md:p-3">
|
<div className="dark relative flex h-full w-full items-center overflow-y-auto rounded-md border border-mineshaft-700 bg-mineshaft-900 p-2 pr-2 md:p-3">
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user