Merge branch 'main' of github.com:Infisical/infisical into password-protect-secret-share

This commit is contained in:
lemmyMwaura
2024-08-06 19:49:25 +03:00
77 changed files with 2445 additions and 917 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState } from "react";
import { ReactNode, useEffect, useState } from "react";
import { useToggle } from "@app/hooks";
@@ -16,6 +16,7 @@ type Props = {
subTitle?: string;
onDeleteApproved: () => Promise<void>;
buttonText?: string;
children?: ReactNode;
};
export const DeleteActionModal = ({
@@ -26,7 +27,8 @@ export const DeleteActionModal = ({
onDeleteApproved,
title,
subTitle = "This action is irreversible.",
buttonText = "Delete"
buttonText = "Delete",
children
}: Props): JSX.Element => {
const [inputData, setInputData] = useState("");
const [isLoading, setIsLoading] = useToggle();
@@ -97,6 +99,7 @@ export const DeleteActionModal = ({
placeholder={`Type ${deleteKey} here`}
/>
</FormControl>
{children}
</form>
</ModalContent>
</Modal>
@@ -50,7 +50,7 @@ export const Pagination = ({
>
<div className="mr-6 flex items-center space-x-2">
<div className="text-xs">
{(page - 1) * perPage} - {(page - 1) * perPage + perPage} of {count}
{(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity",
Kms = "kms"
Kms = "kms",
AdminConsole = "organization-admin-console"
}
export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
export type OrgPermissionSet =
@@ -37,6 +42,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
@@ -56,7 +56,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.GET_CERT]: "Get certificate",
[EventType.DELETE_CERT]: "Delete certificate",
[EventType.REVOKE_CERT]: "Revoke certificate",
[EventType.GET_CERT_BODY]: "Get certificate body"
[EventType.GET_CERT_BODY]: "Get certificate body",
[EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project"
};
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+2 -1
View File
@@ -70,5 +70,6 @@ export enum EventType {
GET_CERT = "get-cert",
DELETE_CERT = "delete-cert",
REVOKE_CERT = "revoke-cert",
GET_CERT_BODY = "get-cert-body"
GET_CERT_BODY = "get-cert-body",
ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project"
}
+12 -1
View File
@@ -579,6 +579,16 @@ interface GetCertBody {
};
}
interface OrgAdminAccessProjectEvent {
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
metadata: {
userId: string;
username: string;
email: string;
projectId: string;
}; // no metadata yet
}
export type Event =
| GetSecretsEvent
| GetSecretEvent
@@ -635,7 +645,8 @@ export type Event =
| GetCert
| DeleteCert
| RevokeCert
| GetCertBody;
| GetCertBody
| OrgAdminAccessProjectEvent;
export type AuditLog = {
id: string;
+1
View File
@@ -19,6 +19,7 @@ export * from "./keys";
export * from "./kms";
export * from "./ldapConfig";
export * from "./oidcConfig";
export * from "./orgAdmin";
export * from "./organization";
export * from "./projectUserAdditionalPrivilege";
export * from "./rateLimit";
@@ -110,8 +110,15 @@ export const useCreateIntegration = () => {
export const useDeleteIntegration = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, { id: string; workspaceId: string }>({
mutationFn: ({ id }) => apiRequest.delete(`/api/v1/integration/${id}`),
return useMutation<
{},
{},
{ id: string; workspaceId: string; shouldDeleteIntegrationSecrets: boolean }
>({
mutationFn: ({ id, shouldDeleteIntegrationSecrets }) =>
apiRequest.delete(
`/api/v1/integration/${id}?shouldDeleteIntegrationSecrets=${shouldDeleteIntegrationSecrets}`
),
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceIntegrations(workspaceId));
queryClient.invalidateQueries(workspaceKeys.getWorkspaceAuthorization(workspaceId));
@@ -0,0 +1,2 @@
export { useOrgAdminAccessProject } from "./mutation";
export { useOrgAdminGetProjects } from "./queries";
@@ -0,0 +1,15 @@
import { useMutation } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TOrgAdminAccessProjectDTO } from "./types";
export const useOrgAdminAccessProject = () =>
useMutation({
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
const { data } = await apiRequest.post(
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
);
return data;
}
});
@@ -0,0 +1,30 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { Workspace } from "../types";
import { TOrgAdminGetProjectsDTO } from "./types";
export const orgAdminQueryKeys = {
getProjects: (filter: TOrgAdminGetProjectsDTO) => ["org-admin-projects", filter] as const
};
export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdminGetProjectsDTO) => {
return useQuery({
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
queryFn: async () => {
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
"/api/v1/organization-admin/projects",
{
params: {
limit,
offset,
search
}
}
);
return data;
}
});
};
+9
View File
@@ -0,0 +1,9 @@
export type TOrgAdminGetProjectsDTO = {
limit?: number;
offset?: number;
search?: string;
};
export type TOrgAdminAccessProjectDTO = {
projectId: string;
};
@@ -5,6 +5,5 @@ export type TRateLimit = {
authRateLimit: number;
inviteUserRateLimit: number;
mfaRateLimit: number;
creationLimit: number;
publicEndpointLimit: number;
};
@@ -317,6 +317,7 @@ export const useDeleteWorkspace = () => {
},
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
queryClient.invalidateQueries(["org-admin-projects"]);
}
});
};
@@ -21,6 +21,7 @@ export type Workspace = {
pitVersionLimit: number;
auditLogsRetentionDays: number;
slug: string;
createdAt: string;
};
export type WorkspaceEnv = {
+1 -1
View File
@@ -13,7 +13,7 @@ interface UsePopUpProps {
export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
isOpen: boolean;
data?: unknown;
data?: any;
};
};
+6 -1
View File
@@ -476,10 +476,15 @@ export const AppLayout = ({ children }: LayoutProps) => {
{user?.superAdmin && (
<Link href="/admin" legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Admin Panel
Server Admin Panel
</DropdownMenuItem>
</Link>
)}
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<button type="button" onClick={logOutUser} className="w-full">
<DropdownMenuItem>Log Out</DropdownMenuItem>
@@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { OrgAdminPage } from "@app/views/OrgAdminPage";
export default function SettingsOrg() {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("settings.org.title") })}</title>
<link rel="icon" href="/infisical.ico" />
</Head>
<OrgAdminPage />
</>
);
}
SettingsOrg.requireAuth = true;
File diff suppressed because it is too large Load Diff
@@ -106,9 +106,13 @@ export const IntegrationsPage = withProjectPermission(
handleProviderIntegration(provider);
};
const handleIntegrationDelete = async (integrationId: string, cb: () => void) => {
const handleIntegrationDelete = async (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
cb: () => void
) => {
try {
await deleteIntegration({ id: integrationId, workspaceId });
await deleteIntegration({ id: integrationId, workspaceId, shouldDeleteIntegrationSecrets });
if (cb) cb();
createNotification({
type: "success",
@@ -152,7 +156,7 @@ export const IntegrationsPage = withProjectPermission(
isLoading={isIntegrationLoading}
integrations={integrations}
environments={environments}
onIntegrationDelete={({ id }, cb) => handleIntegrationDelete(id, cb)}
onIntegrationDelete={handleIntegrationDelete}
workspaceId={workspaceId}
/>
<CloudIntegrationSection
@@ -7,6 +7,7 @@ import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Checkbox,
DeleteActionModal,
EmptyState,
FormLabel,
@@ -16,7 +17,7 @@ import {
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { usePopUp } from "@app/hooks";
import { usePopUp, useToggle } from "@app/hooks";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
import { TIntegration } from "@app/hooks/api/types";
@@ -25,7 +26,11 @@ type Props = {
environments: Array<{ name: string; slug: string; id: string }>;
integrations?: TIntegration[];
isLoading?: boolean;
onIntegrationDelete: (integration: TIntegration, cb: () => void) => void;
onIntegrationDelete: (
integrationId: string,
shouldDeleteIntegrationSecrets: boolean,
cb: () => void
) => Promise<void>;
workspaceId: string;
};
@@ -37,10 +42,12 @@ export const IntegrationsSection = ({
workspaceId
}: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"deleteConfirmation"
"deleteConfirmation",
"deleteSecretsConfirmation"
] as const);
const { mutate: syncIntegration } = useSyncIntegration();
const [shouldDeleteSecrets, setShouldDeleteSecrets] = useToggle(false);
return (
<div className="mb-8">
@@ -249,7 +256,10 @@ export const IntegrationsSection = ({
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
<Tooltip content="Remove Integration">
<IconButton
onClick={() => handlePopUpOpen("deleteConfirmation", integration)}
onClick={() => {
setShouldDeleteSecrets.off();
handlePopUpOpen("deleteConfirmation", integration);
}}
ariaLabel="delete"
isDisabled={!isAllowed}
colorSchema="danger"
@@ -281,11 +291,49 @@ export const IntegrationsSection = ({
(popUp?.deleteConfirmation?.data as TIntegration)?.integration ||
""
}
onDeleteApproved={async () =>
onIntegrationDelete(popUp?.deleteConfirmation.data as TIntegration, () =>
handlePopUpClose("deleteConfirmation")
)
}
onDeleteApproved={async () => {
if (shouldDeleteSecrets) {
handlePopUpOpen("deleteSecretsConfirmation");
return;
}
await onIntegrationDelete(
(popUp?.deleteConfirmation.data as TIntegration).id,
false,
() => handlePopUpClose("deleteConfirmation")
);
}}
>
{(popUp?.deleteConfirmation?.data as TIntegration)?.integration === "github" && (
<div className="mt-4">
<Checkbox
id="delete-integration-secrets"
checkIndicatorBg="text-white"
onCheckedChange={() => setShouldDeleteSecrets.toggle()}
>
Delete previously synced secrets from the destination
</Checkbox>
</div>
)}
</DeleteActionModal>
<DeleteActionModal
isOpen={popUp.deleteSecretsConfirmation.isOpen}
title={`Are you sure you also want to delete secrets on ${
(popUp?.deleteConfirmation.data as TIntegration)?.integration
}?`}
subTitle="By confirming, you acknowledge that all secrets managed by this integration will be removed from the destination. This action is irreversible."
onChange={(isOpen) => handlePopUpToggle("deleteSecretsConfirmation", isOpen)}
deleteKey="confirm"
onDeleteApproved={async () => {
await onIntegrationDelete(
(popUp?.deleteConfirmation.data as TIntegration).id,
true,
() => {
handlePopUpClose("deleteSecretsConfirmation");
handlePopUpClose("deleteConfirmation");
}
);
}}
/>
</div>
);
@@ -1,133 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faMoneyBill } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion";
import { twMerge } from "tailwind-merge";
import { Checkbox, Select, SelectItem } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../../../../RolePage/components/OrgRoleModifySection.utils";
type Props = {
isNonEditable?: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-acess",
Custom = "custom"
}
const PERMISSIONS = [
{ action: "read", label: "View projects" },
{ action: "create", label: "Create new projects" }
] as const;
export const WorkspacePermission = ({ isNonEditable, setValue, control }: Props) => {
const rule = useWatch({
control,
name: "permissions.workspace"
});
const [isCustom, setIsCustom] = useToggle();
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.read) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
const handlePermissionChange = (val: Permission) => {
if (val === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
switch (val) {
case Permission.NoAccess:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
break;
case Permission.FullAccess:
setValue("permissions.workspace", { read: true, create: true }, { shouldDirty: true });
break;
case Permission.ReadOnly:
setValue("permissions.workspace", { read: true, create: false }, { shouldDirty: true });
break;
default:
setValue("permissions.workspace", { read: false, create: false }, { shouldDirty: true });
break;
}
};
return (
<div
className={twMerge(
"rounded-md bg-mineshaft-800 px-10 py-6",
selectedPermissionCategory !== Permission.NoAccess && "border-l-2 border-primary-600"
)}
>
<div className="flex items-center space-x-4">
<div>
<FontAwesomeIcon icon={faMoneyBill} className="text-4xl" />
</div>
<div className="flex flex-grow flex-col">
<div className="mb-1 text-lg font-medium">Project</div>
<div className="text-xs font-light">
View and create new projects in this organization
</div>
</div>
<div>
<Select
defaultValue={Permission.NoAccess}
isDisabled={isNonEditable}
value={selectedPermissionCategory}
onValueChange={handlePermissionChange}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</div>
</div>
<motion.div
initial={false}
animate={{ height: isCustom ? "2.5rem" : 0, paddingTop: isCustom ? "1rem" : 0 }}
className="grid auto-cols-min grid-flow-col gap-8 overflow-hidden"
>
{isCustom &&
PERMISSIONS.map(({ action, label }) => (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={field.onChange}
id={`permissions.workspace.${action}`}
isDisabled={isNonEditable}
>
{label}
</Checkbox>
)}
/>
))}
</motion.div>
</div>
);
};
@@ -12,6 +12,12 @@ const generalPermissionSchema = z
})
.optional();
const adminConsolePermissionSchmea = z
.object({
"access-all-projects": z.boolean().optional()
})
.optional();
export const formSchema = z.object({
name: z.string().trim(),
description: z.string().trim().optional(),
@@ -23,7 +29,6 @@ export const formSchema = z.object({
.object({
workspace: z
.object({
read: z.boolean().optional(),
create: z.boolean().optional()
})
.optional(),
@@ -38,7 +43,8 @@ export const formSchema = z.object({
scim: generalPermissionSchema,
ldap: generalPermissionSchema,
billing: generalPermissionSchema,
identity: generalPermissionSchema
identity: generalPermissionSchema,
"organization-admin-console": adminConsolePermissionSchmea
})
.optional()
});
@@ -0,0 +1,135 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [
{ action: "access-all-projects", label: "Access all organization projects" }
] as const;
export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.organization-admin-console"
});
const selectedPermissionCategory = useMemo(() => {
if (rule?.["access-all-projects"]) {
return Permission.Custom;
}
return Permission.NoAccess;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
if (val === Permission.NoAccess) {
setValue(
"permissions.organization-admin-console",
{ "access-all-projects": false },
{ shouldDirty: true }
);
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Organization Admin Console</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.organization-admin-console.${action}`}
key={`permissions.organization-admin-console.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.organization-admin-console.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -0,0 +1,129 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [{ action: "create", label: "Create projects" }] as const;
export const OrgRoleWorkspaceRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.workspace"
});
const selectedPermissionCategory = useMemo(() => {
if (rule?.create) {
return Permission.Custom;
}
return Permission.NoAccess;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
if (val === Permission.NoAccess) {
setValue("permissions.workspace", { create: false }, { shouldDirty: true });
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Project</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && " border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.workspace.${action}`}
key={`permissions.workspace.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={field.value}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.organization-admin-console.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -61,7 +61,10 @@ const getPermissionList = (option: string) => {
type Props = {
isEditable: boolean;
title: string;
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
@@ -12,6 +12,8 @@ import {
TFormSchema
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
const SIMPLE_PERMISSION_OPTIONS = [
@@ -153,6 +155,16 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
/>
);
})}
<OrgRoleWorkspaceRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionAdminConsoleRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
</TBody>
</Table>
</TableContainer>
@@ -0,0 +1,30 @@
import { useState } from "react";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { OrgAdminProjects } from "./components/OrgAdminProjects";
enum TabSections {
Projects = "projects"
}
export const OrgAdminPage = () => {
const [activeTab, setActiveTab] = useState<TabSections>(TabSections.Projects);
return (
<div className="flex w-full justify-center bg-bunker-800 py-6 text-white">
<div className="w-full max-w-6xl px-6">
<div className="mb-4">
<p className="text-3xl font-semibold text-gray-200">Organization Admin Console</p>
</div>
<Tabs value={activeTab} onValueChange={(el) => setActiveTab(el as TabSections)}>
<TabList>
<Tab value={TabSections.Projects}>Projects</Tab>
</TabList>
<TabPanel value={TabSections.Projects}>
<OrgAdminProjects />
</TabPanel>
</Tabs>
</div>
</div>
);
};
@@ -0,0 +1,167 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { faEllipsis, faMagnifyingGlass, faSignIn } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { motion } from "framer-motion";
import { createNotification } from "@app/components/notifications";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import {
OrgPermissionAdminConsoleAction,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
export const OrgAdminProjects = withPermission(
() => {
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
const debouncedSearch = useDebounce(search);
const [perPage, setPerPage] = useState(25);
const router = useRouter();
const orgAdminAccessProject = useOrgAdminAccessProject();
const { data, isLoading: isProjectsLoading } = useOrgAdminGetProjects({
offset: (page - 1) * perPage,
limit: perPage,
search: debouncedSearch || undefined
});
const projects = data?.projects || [];
const projectCount = data?.count || 0;
const isEmpty = !isProjectsLoading && projects.length === 0;
const handleAccessProject = async (projectId: string) => {
try {
await orgAdminAccessProject.mutateAsync({
projectId
});
await router.push({
pathname: "/project/[projectId]/secrets/overview",
query: {
projectId
}
});
} catch {
createNotification({
text: "Failed to access project",
type: "error"
});
}
};
return (
<motion.div
key="panel-projects"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Projects</p>
</div>
<div>
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search by project name"
/>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th>Name</Th>
<Th>Slug</Th>
<Th>Created At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isProjectsLoading && <TableSkeleton columns={4} innerKey="projects" />}
{!isProjectsLoading &&
projects?.map(({ name, slug, createdAt, id }) => (
<Tr key={`project-${id}`} className="group w-full">
<Td>{name}</Td>
<Td>{slug}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd, hh:mm aaa")}</Td>
<Td>
<div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<Button
variant="link"
className="text-bunker-300 hover:text-primary-400 data-[state=open]:text-primary-400"
>
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
handleAccessProject(id);
}}
icon={<FontAwesomeIcon icon={faSignIn} />}
disabled={
orgAdminAccessProject.variables?.projectId === id &&
orgAdminAccessProject.isLoading
}
>
Access{" "}
{orgAdminAccessProject.variables?.projectId === id &&
orgAdminAccessProject.isLoading && <Spinner size="xs" />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isProjectsLoading && (
<Pagination
count={projectCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
{isEmpty && <EmptyState title="No projects found" />}
</TableContainer>
</div>
</div>
</motion.div>
);
},
{
action: OrgPermissionAdminConsoleAction.AccessAllProjects,
subject: OrgPermissionSubjects.AdminConsole
}
);
@@ -0,0 +1 @@
export { OrgAdminProjects } from "./OrgAdminProjects";
@@ -0,0 +1 @@
export { OrgAdminPage } from "./OrgAdminPage";
@@ -317,6 +317,12 @@ export const LogsTableRow = ({ auditLog }: Props) => {
})}
</Td>
);
case EventType.ORG_ADMIN_ACCESS_PROJECT:
return (
<Td>
<p>{`Email: ${event.metadata.email}`}</p>
</Td>
);
case EventType.CREATE_CA:
case EventType.GET_CA:
case EventType.UPDATE_CA:
@@ -15,7 +15,6 @@ const formSchema = z.object({
authRateLimit: z.number(),
inviteUserRateLimit: z.number(),
mfaRateLimit: z.number(),
creationLimit: z.number(),
publicEndpointLimit: z.number()
});
@@ -41,7 +40,6 @@ export const RateLimitPanel = () => {
authRateLimit: rateLimit?.authRateLimit ?? 60,
inviteUserRateLimit: rateLimit?.inviteUserRateLimit ?? 30,
mfaRateLimit: rateLimit?.mfaRateLimit ?? 20,
creationLimit: rateLimit?.creationLimit ?? 30,
publicEndpointLimit: rateLimit?.publicEndpointLimit ?? 30
}
});
@@ -60,7 +58,6 @@ export const RateLimitPanel = () => {
authRateLimit,
inviteUserRateLimit,
mfaRateLimit,
creationLimit,
publicEndpointLimit
} = formData;
@@ -71,7 +68,6 @@ export const RateLimitPanel = () => {
authRateLimit,
inviteUserRateLimit,
mfaRateLimit,
creationLimit,
publicEndpointLimit
});
createNotification({
@@ -210,25 +206,6 @@ export const RateLimitPanel = () => {
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}
name="creationLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="New resource creation requests per minute"
className="w-72"
isError={Boolean(error)}
errorText={error?.message}
>
<Input
{...field}
value={field.value || ""}
onChange={(e) => field.onChange(Number(e.target.value))}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue={300}