mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 19:28:51 +00:00
feat(rbac): made changes from testing with maidul
This commit is contained in:
@@ -191,7 +191,15 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||||
const { workspaceId } = req.params;
|
const {
|
||||||
|
params: { workspaceId }
|
||||||
|
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
|
||||||
|
|
||||||
|
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.Workspace
|
||||||
|
);
|
||||||
|
|
||||||
// delete workspace
|
// delete workspace
|
||||||
await deleteWork({
|
await deleteWork({
|
||||||
@@ -246,7 +254,14 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||||
const { workspaceId } = req.params;
|
const {
|
||||||
|
params: { workspaceId }
|
||||||
|
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
|
||||||
|
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
ProjectPermissionSub.Integrations
|
||||||
|
);
|
||||||
|
|
||||||
const integrations = await Integration.find({
|
const integrations = await Integration.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ router.post(
|
|||||||
workspaceController.createWorkspace
|
workspaceController.createWorkspace
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
"/:workspaceId",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: [AuthMode.JWT]
|
||||||
|
}),
|
||||||
|
workspaceController.deleteWorkspace
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/:workspaceId/name",
|
"/:workspaceId/name",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ import { Tooltip } from "../v2";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
|
// this prop is used when there exist already a tooltip as helper text for users
|
||||||
|
// so when permission is allowed same tooltip will be reused to show helpertext
|
||||||
|
renderTooltip?: boolean;
|
||||||
|
allowedLabel?: string;
|
||||||
} & BoundCanProps<TOrgPermission>;
|
} & BoundCanProps<TOrgPermission>;
|
||||||
|
|
||||||
export const OrgPermissionCan: FunctionComponent<Props> = ({
|
export const OrgPermissionCan: FunctionComponent<Props> = ({
|
||||||
label = "Permission Denied. Kindly contact your org admin",
|
label = "Permission Denied. Kindly contact your org admin",
|
||||||
children,
|
children,
|
||||||
passThrough = true,
|
passThrough = true,
|
||||||
|
renderTooltip,
|
||||||
|
allowedLabel,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const permission = useOrgPermission();
|
const permission = useOrgPermission();
|
||||||
@@ -30,6 +36,10 @@ export const OrgPermissionCan: FunctionComponent<Props> = ({
|
|||||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAllowed && renderTooltip) {
|
||||||
|
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAllowed) return null;
|
if (!isAllowed) return null;
|
||||||
|
|
||||||
return finalChild;
|
return finalChild;
|
||||||
|
|||||||
@@ -7,12 +7,18 @@ import { Tooltip } from "../v2";
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
|
// this prop is used when there exist already a tooltip as helper text for users
|
||||||
|
// so when permission is allowed same tooltip will be reused to show helpertext
|
||||||
|
renderTooltip?: boolean;
|
||||||
|
allowedLabel?: string;
|
||||||
} & BoundCanProps<TProjectPermission>;
|
} & BoundCanProps<TProjectPermission>;
|
||||||
|
|
||||||
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
||||||
label = "Permission Denied. Kindly contact your project admin",
|
label = "Permission Denied. Kindly contact your project admin",
|
||||||
children,
|
children,
|
||||||
passThrough = true,
|
passThrough = true,
|
||||||
|
renderTooltip,
|
||||||
|
allowedLabel,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const permission = useProjectPermission();
|
const permission = useProjectPermission();
|
||||||
@@ -30,6 +36,10 @@ export const ProjectPermissionCan: FunctionComponent<Props> = ({
|
|||||||
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
return <Tooltip content={label}>{finalChild}</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAllowed && renderTooltip) {
|
||||||
|
return <Tooltip content={allowedLabel}>{finalChild}</Tooltip>;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isAllowed) return null;
|
if (!isAllowed) return null;
|
||||||
|
|
||||||
return finalChild;
|
return finalChild;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const Checkbox = ({
|
|||||||
<FontAwesomeIcon icon={faCheck} size="sm" />
|
<FontAwesomeIcon icon={faCheck} size="sm" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxPrimitive.Indicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
<label className="text-sm" htmlFor={id}>
|
<label className="text-sm whitespace-nowrap" htmlFor={id}>
|
||||||
{children}
|
{children}
|
||||||
{isRequired && <span className="pl-1 text-red">*</span>}
|
{isRequired && <span className="pl-1 text-red">*</span>}
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -15,40 +15,16 @@ export type TRole<T extends string | undefined> = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TPermission = TWorkspacePermission | TGeneralPermission;
|
export type TPermission = {
|
||||||
|
|
||||||
type TGeneralPermission = {
|
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
action: "read" | "edit" | "create" | "delete";
|
action: string;
|
||||||
subject: "member" | "role" | "incident-contact" | "sso" | "billing" | "settings";
|
subject: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TWorkspacePermission = {
|
export type TProjectPermission = {
|
||||||
conditions?: Record<string, any>;
|
conditions?: Record<string, any>;
|
||||||
action: "read" | "create";
|
action: string;
|
||||||
subject: "workspace";
|
subject: string;
|
||||||
};
|
|
||||||
|
|
||||||
export type TProjectPermission = TProjectGeneralPermission | TProjectWorkspacePermission;
|
|
||||||
|
|
||||||
type TProjectGeneralPermission = {
|
|
||||||
conditions?: Record<string, any>;
|
|
||||||
action: "read" | "edit" | "create" | "delete";
|
|
||||||
subject:
|
|
||||||
| "member"
|
|
||||||
| "role"
|
|
||||||
| "settings"
|
|
||||||
| "secrets"
|
|
||||||
| "environments"
|
|
||||||
| "folders"
|
|
||||||
| "secret-imports"
|
|
||||||
| "service-tokens";
|
|
||||||
};
|
|
||||||
|
|
||||||
type TProjectWorkspacePermission = {
|
|
||||||
conditions?: Record<string, any>;
|
|
||||||
action: "delete" | "edit";
|
|
||||||
subject: "workspace";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateRoleDTO<T extends string | undefined> = {
|
export type TCreateRoleDTO<T extends string | undefined> = {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
@@ -50,7 +51,14 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription, useUser, useWorkspace } from "@app/context";
|
import {
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
useOrganization,
|
||||||
|
useSubscription,
|
||||||
|
useUser,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
@@ -395,22 +403,30 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
<hr className="mt-1 mb-1 h-px border-0 bg-gray-700" />
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Button
|
<OrgPermissionCan
|
||||||
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
I={OrgPermissionActions.Create}
|
||||||
colorSchema="primary"
|
a={OrgPermissionSubjects.Workspace}
|
||||||
variant="outline_bg"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (isAddingProjectsAllowed) {
|
|
||||||
handlePopUpOpen("addNewWs");
|
|
||||||
} else {
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
>
|
>
|
||||||
Add Project
|
{(isAllowed) => (
|
||||||
</Button>
|
<Button
|
||||||
|
className="w-full bg-mineshaft-700 py-2 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
onClick={() => {
|
||||||
|
if (isAddingProjectsAllowed) {
|
||||||
|
handlePopUpOpen("addNewWs");
|
||||||
|
} else {
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Project
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const SettingsBilling = withPermission<{}, TOrgPermission>(
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
{ action: OrgPermissionActions.Delete, subject: OrgPermissionSubjects.Billing }
|
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Billing }
|
||||||
);
|
);
|
||||||
|
|
||||||
Object.assign(SettingsBilling, { requireAuth: true });
|
Object.assign(SettingsBilling, { requireAuth: true });
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.rdp-day,
|
.rdp-day,
|
||||||
.rdp-nav_button {
|
.rdp-nav_button {
|
||||||
@apply rounded-md hover:text-mineshaft-500;
|
@apply rounded-md hover:text-mineshaft-500;
|
||||||
@@ -108,7 +112,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tags-conic-bg {
|
.tags-conic-bg {
|
||||||
background: conic-gradient(rgb(235, 87, 87), rgb(242, 201, 76), rgb(76, 183, 130), rgb(78, 167, 252), rgb(250, 96, 122));
|
background: conic-gradient(
|
||||||
|
rgb(235, 87, 87),
|
||||||
|
rgb(242, 201, 76),
|
||||||
|
rgb(76, 183, 130),
|
||||||
|
rgb(78, 167, 252),
|
||||||
|
rgb(250, 96, 122)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.show-tags {
|
.show-tags {
|
||||||
|
|||||||
@@ -1138,7 +1138,6 @@ export const DashboardPage = withProjectPermission(
|
|||||||
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
onEnvCompare={(key) => handlePopUpOpen("compareSecrets", key)}
|
||||||
/>
|
/>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
<SecretDropzone
|
<SecretDropzone
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
isSmaller={!isEmptyPage}
|
isSmaller={!isEmptyPage}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
|
||||||
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
|
||||||
import {
|
import {
|
||||||
@@ -33,7 +34,6 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
|
||||||
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
|
||||||
import { useGetProjectSecrets } from "@app/hooks/api";
|
import { useGetProjectSecrets } from "@app/hooks/api";
|
||||||
import { UserWsKeyPair } from "@app/hooks/api/types";
|
import { UserWsKeyPair } from "@app/hooks/api/types";
|
||||||
@@ -80,343 +80,355 @@ type Props = {
|
|||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretDropzone = withProjectPermission(
|
export const SecretDropzone = ({
|
||||||
({
|
isSmaller,
|
||||||
isSmaller,
|
onParsedEnv,
|
||||||
onParsedEnv,
|
onAddNewSecret,
|
||||||
onAddNewSecret,
|
environments = [],
|
||||||
environments = [],
|
workspaceId,
|
||||||
|
decryptFileKey
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDragActive, setDragActive] = useToggle();
|
||||||
|
const [isLoading, setIsLoading] = useToggle();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
|
||||||
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
setValue,
|
||||||
|
formState: { isDirty }
|
||||||
|
} = useForm<TFormSchema>({
|
||||||
|
resolver: yupResolver(formSchema),
|
||||||
|
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretPath = watch("secretPath");
|
||||||
|
const selectedEnvSlug = watch("environment");
|
||||||
|
const debouncedSecretPath = useDebounce(secretPath);
|
||||||
|
|
||||||
|
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
env: selectedEnvSlug,
|
||||||
|
secretPath: debouncedSecretPath,
|
||||||
|
isPaused:
|
||||||
|
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
|
||||||
|
!popUp.importSecEnv.isOpen,
|
||||||
decryptFileKey
|
decryptFileKey
|
||||||
}: Props): JSX.Element => {
|
});
|
||||||
const { t } = useTranslation();
|
|
||||||
const [isDragActive, setDragActive] = useToggle();
|
|
||||||
const [isLoading, setIsLoading] = useToggle();
|
|
||||||
const { createNotification } = useNotificationContext();
|
|
||||||
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
|
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
|
||||||
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
|
|
||||||
|
|
||||||
const {
|
useEffect(() => {
|
||||||
handleSubmit,
|
setValue("secrets", {});
|
||||||
control,
|
setSearchFilter("");
|
||||||
watch,
|
}, [debouncedSecretPath]);
|
||||||
register,
|
|
||||||
reset,
|
|
||||||
setValue,
|
|
||||||
formState: { isDirty }
|
|
||||||
} = useForm<TFormSchema>({
|
|
||||||
resolver: yupResolver(formSchema),
|
|
||||||
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretPath = watch("secretPath");
|
const handleDrag = (e: DragEvent) => {
|
||||||
const selectedEnvSlug = watch("environment");
|
e.preventDefault();
|
||||||
const debouncedSecretPath = useDebounce(secretPath);
|
e.stopPropagation();
|
||||||
|
if (e.type === "dragenter" || e.type === "dragover") {
|
||||||
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
|
setDragActive.on();
|
||||||
workspaceId,
|
} else if (e.type === "dragleave") {
|
||||||
env: selectedEnvSlug,
|
|
||||||
secretPath: debouncedSecretPath,
|
|
||||||
isPaused:
|
|
||||||
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedSecretPath)) &&
|
|
||||||
!popUp.importSecEnv.isOpen,
|
|
||||||
decryptFileKey
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue("secrets", {});
|
|
||||||
setSearchFilter("");
|
|
||||||
}, [debouncedSecretPath]);
|
|
||||||
|
|
||||||
const handleDrag = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (e.type === "dragenter" || e.type === "dragover") {
|
|
||||||
setDragActive.on();
|
|
||||||
} else if (e.type === "dragleave") {
|
|
||||||
setDragActive.off();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseFile = (file?: File, isJson?: boolean) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
if (!file) {
|
|
||||||
createNotification({
|
|
||||||
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
|
||||||
type: "error",
|
|
||||||
timeoutMs: 10000
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// const fileType = file.name.split('.')[1];
|
|
||||||
setIsLoading.on();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
if (!event?.target?.result) return;
|
|
||||||
// parse function's argument looks like to be ArrayBuffer
|
|
||||||
const env = isJson
|
|
||||||
? parseJson(event.target.result as ArrayBuffer)
|
|
||||||
: parseDotEnv(event.target.result as ArrayBuffer);
|
|
||||||
setIsLoading.off();
|
|
||||||
onParsedEnv(env);
|
|
||||||
};
|
|
||||||
|
|
||||||
// If something is wrong show an error
|
|
||||||
try {
|
|
||||||
reader.readAsText(file);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!e.dataTransfer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.dataTransfer.dropEffect = "copy";
|
|
||||||
setDragActive.off();
|
setDragActive.off();
|
||||||
parseFile(e.dataTransfer.files[0]);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
const parseFile = (file?: File, isJson?: boolean) => {
|
||||||
e.preventDefault();
|
const reader = new FileReader();
|
||||||
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
|
if (!file) {
|
||||||
};
|
createNotification({
|
||||||
|
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
|
||||||
const handleFormSubmit = (data: TFormSchema) => {
|
type: "error",
|
||||||
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
timeoutMs: 10000
|
||||||
Object.keys(data.secrets || {}).forEach((key) => {
|
|
||||||
if (data.secrets[key]) {
|
|
||||||
secretsToBePulled[key] = {
|
|
||||||
value: (shouldIncludeValues && data.secrets[key]) || "",
|
|
||||||
comments: [""]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
onParsedEnv(secretsToBePulled);
|
return;
|
||||||
handlePopUpClose("importSecEnv");
|
}
|
||||||
reset();
|
// const fileType = file.name.split('.')[1];
|
||||||
|
setIsLoading.on();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (!event?.target?.result) return;
|
||||||
|
// parse function's argument looks like to be ArrayBuffer
|
||||||
|
const env = isJson
|
||||||
|
? parseJson(event.target.result as ArrayBuffer)
|
||||||
|
: parseDotEnv(event.target.result as ArrayBuffer);
|
||||||
|
setIsLoading.off();
|
||||||
|
onParsedEnv(env);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSecSelectAll = () => {
|
// If something is wrong show an error
|
||||||
if (secrets?.secrets) {
|
try {
|
||||||
setValue(
|
reader.readAsText(file);
|
||||||
"secrets",
|
} catch (error) {
|
||||||
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
|
console.log(error);
|
||||||
{ shouldDirty: true }
|
}
|
||||||
);
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (!e.dataTransfer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dataTransfer.dropEffect = "copy";
|
||||||
|
setDragActive.off();
|
||||||
|
parseFile(e.dataTransfer.files[0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = (data: TFormSchema) => {
|
||||||
|
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
|
||||||
|
Object.keys(data.secrets || {}).forEach((key) => {
|
||||||
|
if (data.secrets[key]) {
|
||||||
|
secretsToBePulled[key] = {
|
||||||
|
value: (shouldIncludeValues && data.secrets[key]) || "",
|
||||||
|
comments: [""]
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
onParsedEnv(secretsToBePulled);
|
||||||
|
handlePopUpClose("importSecEnv");
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleSecSelectAll = () => {
|
||||||
<div
|
if (secrets?.secrets) {
|
||||||
onDragEnter={handleDrag}
|
setValue(
|
||||||
onDragLeave={handleDrag}
|
"secrets",
|
||||||
onDragOver={handleDrag}
|
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
|
||||||
onDrop={handleDrop}
|
{ shouldDirty: true }
|
||||||
className={twMerge(
|
);
|
||||||
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
}
|
||||||
isDragActive && "opacity-100",
|
};
|
||||||
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
|
|
||||||
isLoading && "bg-bunker-800"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="mb-16 flex items-center justify-center pt-16">
|
|
||||||
<img
|
|
||||||
src="/images/loading/loading.gif"
|
|
||||||
height={70}
|
|
||||||
width={120}
|
|
||||||
alt="loading animation"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
|
||||||
<div className="flex items-center justify-cente flex-col space-y-2">
|
|
||||||
<div>
|
|
||||||
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="fileSelect"
|
|
||||||
type="file"
|
|
||||||
className="absolute h-full w-full cursor-pointer opacity-0"
|
|
||||||
accept=".txt,.env,.yml,.yaml,.json"
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
|
||||||
"flex w-full flex-row items-center justify-center py-4",
|
|
||||||
isSmaller && "py-1"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
|
||||||
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
|
||||||
<div className="w-1/5 border-t border-mineshaft-700" />
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center space-x-8">
|
|
||||||
<Modal
|
|
||||||
isOpen={popUp.importSecEnv.isOpen}
|
|
||||||
onOpenChange={(isOpen) => {
|
|
||||||
handlePopUpToggle("importSecEnv", isOpen);
|
|
||||||
reset();
|
|
||||||
setSearchFilter("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button variant="star" size={isSmaller ? "xs" : "sm"}>
|
|
||||||
Copy Secrets From An Environment
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent
|
|
||||||
className="max-w-2xl"
|
|
||||||
title="Copy Secret From An Environment"
|
|
||||||
subTitle="Copy/paste secrets from other environments into this context"
|
|
||||||
>
|
|
||||||
<form>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="environment"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<FormControl label="Environment" isRequired className="w-1/3">
|
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
onValueChange={(val) => onChange(val)}
|
|
||||||
className="w-full border border-mineshaft-500"
|
|
||||||
defaultValue={environments?.[0]?.slug}
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{environments.map((sourceEnvironment) => (
|
|
||||||
<SelectItem
|
|
||||||
value={sourceEnvironment.slug}
|
|
||||||
key={`source-environment-${sourceEnvironment.slug}`}
|
|
||||||
>
|
|
||||||
{sourceEnvironment.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
|
||||||
<Input
|
|
||||||
{...register("secretPath")}
|
|
||||||
placeholder="Provide a path, default is /"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-mineshaft-600 pt-4">
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
|
||||||
<div>Secrets</div>
|
|
||||||
<div className="w-1/2 flex items-center space-x-2">
|
|
||||||
<Input
|
|
||||||
placeholder="Search for secret"
|
|
||||||
value={searchFilter}
|
|
||||||
size="xs"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
|
||||||
onChange={(evt) => setSearchFilter(evt.target.value)}
|
|
||||||
/>
|
|
||||||
<Tooltip content="Select All">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="Select all"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="xs"
|
|
||||||
onClick={handleSecSelectAll}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="Unselect All">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="UnSelect all"
|
|
||||||
variant="outline_bg"
|
|
||||||
size="xs"
|
|
||||||
onClick={() => reset()}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!isSecretsLoading && !secrets?.secrets?.length && (
|
|
||||||
<EmptyState title="No secrets found" icon={faKey} />
|
|
||||||
)}
|
|
||||||
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
|
|
||||||
{isSecretsLoading &&
|
|
||||||
Array.apply(0, Array(2)).map((_x, i) => (
|
|
||||||
<Skeleton
|
|
||||||
key={`secret-pull-loading-${i + 1}`}
|
|
||||||
className="bg-mineshaft-700"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{secrets?.secrets
|
return (
|
||||||
?.filter(({ key }) =>
|
<div
|
||||||
key.toLowerCase().includes(searchFilter.toLowerCase())
|
onDragEnter={handleDrag}
|
||||||
)
|
onDragLeave={handleDrag}
|
||||||
?.map(({ _id, key, value: secVal }) => (
|
onDragOver={handleDrag}
|
||||||
<Controller
|
onDrop={handleDrop}
|
||||||
key={`pull-secret--${_id}`}
|
className={twMerge(
|
||||||
control={control}
|
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 text-sm px-2 text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
|
||||||
name={`secrets.${key}`}
|
isDragActive && "opacity-100",
|
||||||
render={({ field: { value, onChange } }) => (
|
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
|
||||||
<Checkbox
|
isLoading && "bg-bunker-800"
|
||||||
id={`pull-secret-${_id}`}
|
)}
|
||||||
isChecked={Boolean(value)}
|
>
|
||||||
onCheckedChange={(isChecked) =>
|
{isLoading ? (
|
||||||
onChange(isChecked ? secVal : "")
|
<div className="mb-16 flex items-center justify-center pt-16">
|
||||||
}
|
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
|
||||||
>
|
</div>
|
||||||
{key}
|
) : (
|
||||||
</Checkbox>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
)}
|
<div className="flex items-center justify-cente flex-col space-y-2">
|
||||||
/>
|
<div>
|
||||||
))}
|
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
|
||||||
</div>
|
|
||||||
<div className="mt-6 mb-4">
|
|
||||||
<Checkbox
|
|
||||||
id="populate-include-value"
|
|
||||||
isChecked={shouldIncludeValues}
|
|
||||||
onCheckedChange={(isChecked) =>
|
|
||||||
setShouldIncludeValues(isChecked as boolean)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Include secret values
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
|
||||||
type="submit"
|
|
||||||
isDisabled={!isDirty}
|
|
||||||
>
|
|
||||||
Paste Secrets
|
|
||||||
</Button>
|
|
||||||
<Button variant="plain" colorSchema="secondary">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
{!isSmaller && (
|
|
||||||
<Button variant="star" onClick={onAddNewSecret}>
|
|
||||||
Add a new secret
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
<div>
|
||||||
)}
|
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
<ProjectPermissionCan
|
||||||
},
|
I={ProjectPermissionActions.Create}
|
||||||
{ action: ProjectPermissionActions.Create, subject: ProjectPermissionSub.Secrets }
|
a={ProjectPermissionSub.Secrets}
|
||||||
);
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<input
|
||||||
|
id="fileSelect"
|
||||||
|
disabled={!isAllowed}
|
||||||
|
type="file"
|
||||||
|
className="absolute h-full w-full cursor-pointer opacity-0"
|
||||||
|
accept=".txt,.env,.yml,.yaml,.json"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex w-full flex-row items-center justify-center py-4",
|
||||||
|
isSmaller && "py-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||||
|
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
|
||||||
|
<div className="w-1/5 border-t border-mineshaft-700" />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center space-x-8">
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.importSecEnv.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("importSecEnv", isOpen);
|
||||||
|
reset();
|
||||||
|
setSearchFilter("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Secrets}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button isDisabled={!isAllowed} variant="star" size={isSmaller ? "xs" : "sm"}>
|
||||||
|
Copy Secrets From An Environment
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-2xl"
|
||||||
|
title="Copy Secret From An Environment"
|
||||||
|
subTitle="Copy/paste secrets from other environments into this context"
|
||||||
|
>
|
||||||
|
<form>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="environment"
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<FormControl label="Environment" isRequired className="w-1/3">
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
onValueChange={(val) => onChange(val)}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
defaultValue={environments?.[0]?.slug}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{environments.map((sourceEnvironment) => (
|
||||||
|
<SelectItem
|
||||||
|
value={sourceEnvironment.slug}
|
||||||
|
key={`source-environment-${sourceEnvironment.slug}`}
|
||||||
|
>
|
||||||
|
{sourceEnvironment.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormControl label="Secret Path" className="flex-grow" isRequired>
|
||||||
|
<Input
|
||||||
|
{...register("secretPath")}
|
||||||
|
placeholder="Provide a path, default is /"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-mineshaft-600 pt-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>Secrets</div>
|
||||||
|
<div className="w-1/2 flex items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Search for secret"
|
||||||
|
value={searchFilter}
|
||||||
|
size="xs"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||||
|
onChange={(evt) => setSearchFilter(evt.target.value)}
|
||||||
|
/>
|
||||||
|
<Tooltip content="Select All">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="Select all"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
onClick={handleSecSelectAll}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Unselect All">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="UnSelect all"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
onClick={() => reset()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!isSecretsLoading && !secrets?.secrets?.length && (
|
||||||
|
<EmptyState title="No secrets found" icon={faKey} />
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-2 gap-4 max-h-64 overflow-auto thin-scrollbar ">
|
||||||
|
{isSecretsLoading &&
|
||||||
|
Array.apply(0, Array(2)).map((_x, i) => (
|
||||||
|
<Skeleton
|
||||||
|
key={`secret-pull-loading-${i + 1}`}
|
||||||
|
className="bg-mineshaft-700"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{secrets?.secrets
|
||||||
|
?.filter(({ key }) =>
|
||||||
|
key.toLowerCase().includes(searchFilter.toLowerCase())
|
||||||
|
)
|
||||||
|
?.map(({ _id, key, value: secVal }) => (
|
||||||
|
<Controller
|
||||||
|
key={`pull-secret--${_id}`}
|
||||||
|
control={control}
|
||||||
|
name={`secrets.${key}`}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Checkbox
|
||||||
|
id={`pull-secret-${_id}`}
|
||||||
|
isChecked={Boolean(value)}
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 mb-4">
|
||||||
|
<Checkbox
|
||||||
|
id="populate-include-value"
|
||||||
|
isChecked={shouldIncludeValues}
|
||||||
|
onCheckedChange={(isChecked) =>
|
||||||
|
setShouldIncludeValues(isChecked as boolean)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Include secret values
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faClone} />}
|
||||||
|
type="submit"
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
>
|
||||||
|
Paste Secrets
|
||||||
|
</Button>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
{!isSmaller && (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Secrets}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>
|
||||||
|
Add a new secret
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+27
-33
@@ -16,37 +16,37 @@ export const formSchema = z.object({
|
|||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
slug: z.string().trim(),
|
slug: z.string().trim(),
|
||||||
permissions: z.object({
|
permissions: z
|
||||||
workspace: z
|
.object({
|
||||||
.object({
|
workspace: z
|
||||||
read: z.boolean().optional(),
|
.object({
|
||||||
create: z.boolean().optional()
|
read: z.boolean().optional(),
|
||||||
})
|
create: z.boolean().optional()
|
||||||
.optional(),
|
})
|
||||||
member: generalPermissionSchema,
|
.optional(),
|
||||||
role: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
settings: generalPermissionSchema,
|
role: generalPermissionSchema,
|
||||||
"service-account": generalPermissionSchema,
|
settings: generalPermissionSchema,
|
||||||
"incident-contact": generalPermissionSchema,
|
"service-account": generalPermissionSchema,
|
||||||
"secret-scanning": generalPermissionSchema,
|
"incident-contact": generalPermissionSchema,
|
||||||
sso: generalPermissionSchema,
|
"secret-scanning": generalPermissionSchema,
|
||||||
billing: generalPermissionSchema
|
sso: generalPermissionSchema,
|
||||||
})
|
billing: generalPermissionSchema
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TFormSchema = z.infer<typeof formSchema>;
|
export type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
// convert role permission to form compatiable data structure
|
// convert role permission to form compatiable data structure
|
||||||
export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
||||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
// any because if it set it as form type due to the discriminated union type of ts
|
||||||
|
// i would have to write a if loop with both conditions same
|
||||||
|
const formVal: Record<string, any> = {};
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
const { subject, action } = permission;
|
const { subject, action } = permission;
|
||||||
if (!formVal?.[subject]) formVal[subject] = {};
|
if (!formVal?.[subject]) formVal[subject] = {};
|
||||||
|
formVal[subject][action] = true;
|
||||||
// akhilmhdh: this is typecast as something other than workspace key else i would need an if loop with same condition on both side
|
|
||||||
const key = subject as keyof TFormSchema["permissions"];
|
|
||||||
(formVal[key] as Exclude<TFormSchema["permissions"]["member"], undefined>)[action] = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return formVal;
|
return formVal;
|
||||||
@@ -54,19 +54,13 @@ export const rolePermission2Form = (permissions: TPermission[] = []) => {
|
|||||||
|
|
||||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||||
const permissions: TPermission[] = [];
|
const permissions: TPermission[] = [];
|
||||||
(Object.keys(formVal) as Array<keyof typeof formVal>).forEach((rule) => {
|
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||||
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
|
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||||
// quite annoying i know
|
if (isAllowed) {
|
||||||
const actions = Object.keys(formVal[rule] || {}) as Array<
|
permissions.push({ subject: rule, action });
|
||||||
keyof z.infer<typeof generalPermissionSchema>
|
|
||||||
>;
|
|
||||||
|
|
||||||
actions.forEach((action) => {
|
|
||||||
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
|
|
||||||
if (formVal?.[rule]?.[action as keyof typeof formVal.workspace]) {
|
|
||||||
permissions.push({ subject: rule, action } as any);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return permissions;
|
return permissions;
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-2
@@ -11,7 +11,7 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { TFormSchema } from "./OrgRoleModifySection.utils";
|
import { TFormSchema } from "./OrgRoleModifySection.utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
formName: keyof Omit<TFormSchema["permissions"], "workspace">;
|
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
||||||
isNonEditable?: boolean;
|
isNonEditable?: boolean;
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
control: Control<TFormSchema>;
|
control: Control<TFormSchema>;
|
||||||
@@ -34,6 +34,31 @@ const PERMISSIONS = [
|
|||||||
{ action: "delete", label: "Remove" }
|
{ action: "delete", label: "Remove" }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const SECRET_SCANNING_PERMISSIONS = [
|
||||||
|
{ action: "read", label: "View risks" },
|
||||||
|
{ action: "create", label: "Add integrations" },
|
||||||
|
{ action: "edit", label: "Edit risk status" },
|
||||||
|
{ action: "delete", label: "Remove integrations" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const BILLING_PERMISSIONS = [
|
||||||
|
{ action: "read", label: "View bills" },
|
||||||
|
{ action: "create", label: "Add payment methods" },
|
||||||
|
{ action: "edit", label: "Edit payments" },
|
||||||
|
{ action: "delete", label: "Remove payments" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const getPermissionList = (option: Props["formName"]) => {
|
||||||
|
switch (option) {
|
||||||
|
case "secret-scanning":
|
||||||
|
return SECRET_SCANNING_PERMISSIONS;
|
||||||
|
case "billing":
|
||||||
|
return BILLING_PERMISSIONS;
|
||||||
|
default:
|
||||||
|
return PERMISSIONS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const SimpleLevelPermissionOption = ({
|
export const SimpleLevelPermissionOption = ({
|
||||||
isNonEditable,
|
isNonEditable,
|
||||||
setValue,
|
setValue,
|
||||||
@@ -138,7 +163,7 @@ export const SimpleLevelPermissionOption = ({
|
|||||||
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
|
className="overflow-hidden grid gap-8 grid-flow-col auto-cols-min"
|
||||||
>
|
>
|
||||||
{isCustom &&
|
{isCustom &&
|
||||||
PERMISSIONS.map(({ action, label }) => (
|
getPermissionList(formName).map(({ action, label }) => (
|
||||||
<Controller
|
<Controller
|
||||||
name={`permissions.${formName}.${action}`}
|
name={`permissions.${formName}.${action}`}
|
||||||
key={`permissions.${formName}.${action}`}
|
key={`permissions.${formName}.${action}`}
|
||||||
|
|||||||
+3
-1
@@ -86,7 +86,9 @@ export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props)
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-grow flex flex-col">
|
<div className="flex-grow flex flex-col">
|
||||||
<div className="font-medium mb-1 text-lg">Project</div>
|
<div className="font-medium mb-1 text-lg">Project</div>
|
||||||
<div className="text-xs font-light">Project management control</div>
|
<div className="text-xs font-light">
|
||||||
|
More fine granined project access control can be defined with project level roles
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
@@ -16,10 +17,9 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tooltip,
|
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteRole } from "@app/hooks/api";
|
import { useDeleteRole } from "@app/hooks/api";
|
||||||
import { TRole } from "@app/hooks/api/roles/types";
|
import { TRole } from "@app/hooks/api/roles/types";
|
||||||
@@ -65,9 +65,17 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
|||||||
placeholder="Search roles..."
|
placeholder="Search roles..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button leftIcon={<FontAwesomeIcon icon={faPlus} />} onClick={() => onSelectRole()}>
|
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Role}>
|
||||||
Add Role
|
{(isAllowed) => (
|
||||||
</Button>
|
<Button
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => onSelectRole()}
|
||||||
|
>
|
||||||
|
Add Role
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
@@ -95,27 +103,42 @@ export const OrgRoleTable = ({ isRolesLoading, roles = [], onSelectRole }: Props
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<Tooltip content="Edit">
|
<OrgPermissionCan
|
||||||
<IconButton
|
I={OrgPermissionActions.Edit}
|
||||||
ariaLabel="edit"
|
a={OrgPermissionSubjects.Role}
|
||||||
onClick={() => onSelectRole(role)}
|
renderTooltip
|
||||||
variant="plain"
|
allowedLabel="Edit"
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip
|
|
||||||
content={isNonMutatable ? "Reserved roles are non-removable" : "Delete"}
|
|
||||||
>
|
>
|
||||||
<IconButton
|
{(isAllowed) => (
|
||||||
ariaLabel="delete"
|
<IconButton
|
||||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
isDisabled={!isAllowed}
|
||||||
variant="plain"
|
ariaLabel="edit"
|
||||||
isDisabled={isNonMutatable}
|
onClick={() => onSelectRole(role)}
|
||||||
>
|
variant="plain"
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
>
|
||||||
</IconButton>
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionActions.Delete}
|
||||||
|
a={OrgPermissionSubjects.Role}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel={
|
||||||
|
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="delete"
|
||||||
|
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||||
|
variant="plain"
|
||||||
|
isDisabled={isNonMutatable || !isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
|||||||
+22
-29
@@ -17,7 +17,6 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tooltip,
|
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
@@ -116,43 +115,37 @@ export const ProjectRoleList = ({ isRolesLoading, roles = [], onSelectRole }: Pr
|
|||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={ProjectPermissionSub.Role}
|
a={ProjectPermissionSub.Role}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Edit"
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div>
|
<IconButton
|
||||||
<Tooltip content="Edit">
|
isDisabled={!isAllowed}
|
||||||
<IconButton
|
ariaLabel="edit"
|
||||||
isDisabled={!isAllowed}
|
onClick={() => onSelectRole(role)}
|
||||||
ariaLabel="edit"
|
variant="plain"
|
||||||
onClick={() => onSelectRole(role)}
|
>
|
||||||
variant="plain"
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
>
|
</IconButton>
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={ProjectPermissionSub.Role}
|
a={ProjectPermissionSub.Role}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel={
|
||||||
|
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div>
|
<IconButton
|
||||||
<Tooltip
|
ariaLabel="delete"
|
||||||
content={
|
onClick={() => handlePopUpOpen("deleteRole", role)}
|
||||||
isNonMutatable ? "Reserved roles are non-removable" : "Delete"
|
variant="plain"
|
||||||
}
|
isDisabled={isNonMutatable || !isAllowed}
|
||||||
>
|
>
|
||||||
<IconButton
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
ariaLabel="delete"
|
</IconButton>
|
||||||
onClick={() => handlePopUpOpen("deleteRole", role)}
|
|
||||||
variant="plain"
|
|
||||||
isDisabled={isNonMutatable || !isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+45
-53
@@ -29,39 +29,41 @@ export const formSchema = z.object({
|
|||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
slug: z.string().trim(),
|
slug: z.string().trim(),
|
||||||
permissions: z.object({
|
permissions: z
|
||||||
secrets: z.record(multiEnvPermissionSchema).optional(),
|
.object({
|
||||||
folders: z.record(multiEnvPermissionSchema).optional(),
|
secrets: z.record(multiEnvPermissionSchema).optional(),
|
||||||
"secret-imports": z.record(multiEnvPermissionSchema).optional(),
|
folders: z.record(multiEnvPermissionSchema).optional(),
|
||||||
member: generalPermissionSchema,
|
"secret-imports": z.record(multiEnvPermissionSchema).optional(),
|
||||||
role: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
integrations: generalPermissionSchema,
|
role: generalPermissionSchema,
|
||||||
webhooks: generalPermissionSchema,
|
integrations: generalPermissionSchema,
|
||||||
"service-tokens": generalPermissionSchema,
|
webhooks: generalPermissionSchema,
|
||||||
settings: generalPermissionSchema,
|
"service-tokens": generalPermissionSchema,
|
||||||
environments: generalPermissionSchema,
|
settings: generalPermissionSchema,
|
||||||
tags: generalPermissionSchema,
|
environments: generalPermissionSchema,
|
||||||
"audit-logs": generalPermissionSchema,
|
tags: generalPermissionSchema,
|
||||||
"ip-allowlist": generalPermissionSchema,
|
"audit-logs": generalPermissionSchema,
|
||||||
workspace: z
|
"ip-allowlist": generalPermissionSchema,
|
||||||
.object({
|
workspace: z
|
||||||
edit: z.boolean().optional(),
|
.object({
|
||||||
delete: z.boolean().optional()
|
edit: z.boolean().optional(),
|
||||||
})
|
delete: z.boolean().optional()
|
||||||
.optional(),
|
})
|
||||||
"secret-rollback": z
|
.optional(),
|
||||||
.object({
|
"secret-rollback": z
|
||||||
read: z.boolean().optional(),
|
.object({
|
||||||
create: z.boolean().optional()
|
read: z.boolean().optional(),
|
||||||
})
|
create: z.boolean().optional()
|
||||||
.optional()
|
})
|
||||||
})
|
.optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TFormSchema = z.infer<typeof formSchema>;
|
export type TFormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
const multiEnvApi2Form = (
|
const multiEnvApi2Form = (
|
||||||
formVal: TFormSchema["permissions"]["secrets"],
|
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||||
permission: TProjectPermission
|
permission: TProjectPermission
|
||||||
) => {
|
) => {
|
||||||
const isCustomRule = Boolean(permission?.conditions?.environment);
|
const isCustomRule = Boolean(permission?.conditions?.environment);
|
||||||
@@ -76,28 +78,26 @@ const multiEnvApi2Form = (
|
|||||||
if (formVal && !formVal?.[secretEnv]) {
|
if (formVal && !formVal?.[secretEnv]) {
|
||||||
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
formVal[secretEnv] = { read: false, edit: false, create: false, delete: false, secretPath };
|
||||||
}
|
}
|
||||||
formVal![secretEnv]![permission.action] = true;
|
|
||||||
|
formVal[secretEnv][permission.action] = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// convert role permission to form compatiable data structure
|
// convert role permission to form compatiable data structure
|
||||||
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
||||||
const formVal: Partial<TFormSchema["permissions"]> = {};
|
// any because if it set it as form type due to the discriminated union type of ts
|
||||||
|
// i would have to write a if loop with both conditions same
|
||||||
|
const formVal: Record<string, any> = {};
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
const { subject, action } = permission;
|
const { subject, action } = permission;
|
||||||
if (!formVal?.[subject]) formVal[subject] = {};
|
if (!formVal?.[subject]) formVal[subject] = {};
|
||||||
|
|
||||||
if (["secrets", "folders", "secret-imports"].includes(subject)) {
|
if (["secrets", "folders", "secret-imports"].includes(subject)) {
|
||||||
multiEnvApi2Form(formVal[subject] as TFormSchema["permissions"]["secrets"], permission);
|
multiEnvApi2Form(formVal[subject], permission);
|
||||||
} else {
|
} else {
|
||||||
// everything else follows same pattern
|
// everything else follows same pattern
|
||||||
// formVal[settings][read | write] = true
|
// formVal[settings][read | write] = true
|
||||||
formVal[
|
formVal[subject][action] = true;
|
||||||
subject as keyof Omit<
|
|
||||||
TFormSchema["permissions"],
|
|
||||||
"secrets" | "workspace" | "secret-rollback"
|
|
||||||
>
|
|
||||||
]![action] = true;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
|
|
||||||
const multiEnvForm2Api = (
|
const multiEnvForm2Api = (
|
||||||
permissions: TProjectPermission[],
|
permissions: TProjectPermission[],
|
||||||
formVal: TFormSchema["permissions"]["secrets"],
|
formVal: Record<string, { secretPath?: string } & { [key: string]: boolean }>,
|
||||||
subject: (typeof MULTI_ENV_KEY)[number]
|
subject: (typeof MULTI_ENV_KEY)[number]
|
||||||
) => {
|
) => {
|
||||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||||
@@ -142,24 +142,16 @@ const multiEnvForm2Api = (
|
|||||||
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
||||||
const permissions: TProjectPermission[] = [];
|
const permissions: TProjectPermission[] = [];
|
||||||
MULTI_ENV_KEY.forEach((formName) => {
|
MULTI_ENV_KEY.forEach((formName) => {
|
||||||
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal[formName] || {})), formName);
|
multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(formVal?.[formName] || {})), formName);
|
||||||
});
|
});
|
||||||
// other than workspace everything else follows same
|
// other than workspace everything else follows same
|
||||||
// if in future there is a different follow the above on how workspace is done
|
// if in future there is a different follow the above on how workspace is done
|
||||||
(Object.keys(formVal) as Array<keyof typeof formVal>)
|
Object.entries(formVal || {}).forEach(([rule, actions]) => {
|
||||||
.filter((key) => !["secret-imports", "folders", "secrets"].includes(key))
|
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||||
.forEach((rule) => {
|
if (isAllowed) {
|
||||||
// all these type annotations are due to Object.keys of ts cannot infer and put it just a string[]
|
permissions.push({ subject: rule, action });
|
||||||
// quite annoying i know
|
}
|
||||||
const actions = Object.keys(formVal[rule] || {}) as Array<
|
|
||||||
keyof z.infer<typeof generalPermissionSchema>
|
|
||||||
>;
|
|
||||||
actions.forEach((action) => {
|
|
||||||
// akhilmhdh: set it as any due to the union type bug i would end up writing an if else with same condition on both side
|
|
||||||
if (formVal[rule]?.[action as keyof typeof formVal.workspace]) {
|
|
||||||
permissions.push({ subject: rule, action } as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
return permissions;
|
return permissions;
|
||||||
};
|
};
|
||||||
|
|||||||
+4
-4
@@ -23,8 +23,8 @@ enum Permission {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PERMISSIONS = [
|
const PERMISSIONS = [
|
||||||
{ action: "edit", label: "Update" },
|
{ action: "edit", label: "Update workspace details" },
|
||||||
{ action: "delete", label: "Remove" }
|
{ action: "delete", label: "Delete workspace" }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
|
export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props) => {
|
||||||
@@ -80,8 +80,8 @@ export const WsProjectPermission = ({ isNonEditable, setValue, control }: Props)
|
|||||||
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
<FontAwesomeIcon icon={faPuzzlePiece} className="text-4xl" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow flex flex-col">
|
<div className="flex-grow flex flex-col">
|
||||||
<div className="font-medium mb-1 text-lg">Workspace</div>
|
<div className="font-medium mb-1 text-lg">Project</div>
|
||||||
<div className="text-xs font-light">Workspace control actions</div>
|
<div className="text-xs font-light">Project control actions</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection";
|
||||||
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
import { OrgNameChangeSection } from "../OrgNameChangeSection";
|
||||||
import { OrgServiceAccountsTable } from "../OrgServiceAccountsTable";
|
|
||||||
|
|
||||||
export const OrgGeneralTab = () => {
|
export const OrgGeneralTab = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<OrgNameChangeSection />
|
<OrgNameChangeSection />
|
||||||
<OrgServiceAccountsTable />
|
|
||||||
<OrgIncidentContactsSection />
|
<OrgIncidentContactsSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user