mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 20:26:17 +00:00
feat: brought back workspace permission and made requested changes
This commit is contained in:
@@ -9,6 +9,10 @@ export enum OrgPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
|
GrantAccessProjects = "grant-access-projects"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionSubjects {
|
export enum OrgPermissionSubjects {
|
||||||
Workspace = "workspace",
|
Workspace = "workspace",
|
||||||
Role = "role",
|
Role = "role",
|
||||||
@@ -22,7 +26,8 @@ export enum OrgPermissionSubjects {
|
|||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
AdminConsole = "admin-console"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -39,7 +44,8 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
const buildAdminPermission = () => {
|
const buildAdminPermission = () => {
|
||||||
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
const { can, build } = new AbilityBuilder<MongoAbility<OrgPermissionSet>>(createMongoAbility);
|
||||||
@@ -107,6 +113,8 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
|
can(OrgPermissionAdminConsoleAction.GrantAccessProjects, OrgPermissionSubjects.AdminConsole);
|
||||||
|
|
||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerPasswordRouter, { prefix: "/password" });
|
await server.register(registerPasswordRouter, { prefix: "/password" });
|
||||||
await server.register(registerOrgRouter, { prefix: "/organization" });
|
await server.register(registerOrgRouter, { prefix: "/organization" });
|
||||||
await server.register(registerAdminRouter, { prefix: "/admin" });
|
await server.register(registerAdminRouter, { prefix: "/admin" });
|
||||||
await server.register(registerOrgAdminRouter, { prefix: "/org-admin" });
|
await server.register(registerOrgAdminRouter, { prefix: "/organization-admin" });
|
||||||
await server.register(registerUserRouter, { prefix: "/user" });
|
await server.register(registerUserRouter, { prefix: "/user" });
|
||||||
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
await server.register(registerInviteOrgRouter, { prefix: "/invite-org" });
|
||||||
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
await server.register(registerUserActionRouter, { prefix: "/user-action" });
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/projects/:projectId/access",
|
url: "/projects/:projectId/grant-admin-access",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ export const registerOrgAdminRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { membership } = await server.services.orgAdmin.accessProject({
|
const { membership } = await server.services.orgAdmin.grantProjectAdminAccess({
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { OrgMembershipRole, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
|
||||||
@@ -42,15 +45,17 @@ export const orgAdminServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod
|
actorAuthMethod
|
||||||
}: TListOrgProjectsDTO) => {
|
}: TListOrgProjectsDTO) => {
|
||||||
const { membership } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isAdmin = membership.role === OrgMembershipRole.Admin;
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" });
|
OrgPermissionAdminConsoleAction.GrantAccessProjects,
|
||||||
|
OrgPermissionSubjects.AdminConsole
|
||||||
|
);
|
||||||
const projects = await projectDAL.find(
|
const projects = await projectDAL.find(
|
||||||
{
|
{
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
@@ -65,16 +70,24 @@ export const orgAdminServiceFactory = ({
|
|||||||
return { projects, count };
|
return { projects, count };
|
||||||
};
|
};
|
||||||
|
|
||||||
const accessProject = async ({ actor, actorId, actorOrgId, actorAuthMethod, projectId }: TAccessProjectDTO) => {
|
const grantProjectAdminAccess = async ({
|
||||||
const { membership } = await permissionService.getOrgPermission(
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
projectId
|
||||||
|
}: TAccessProjectDTO) => {
|
||||||
|
const { permission, membership } = await permissionService.getOrgPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
const isAdmin = membership.role === OrgMembershipRole.Admin;
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
if (!isAdmin) throw new UnauthorizedError({ message: "Admin only operation" });
|
OrgPermissionAdminConsoleAction.GrantAccessProjects,
|
||||||
|
OrgPermissionSubjects.AdminConsole
|
||||||
|
);
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
@@ -174,5 +187,5 @@ export const orgAdminServiceFactory = ({
|
|||||||
return { isExistingMember: false, membership: updatedMembership };
|
return { isExistingMember: false, membership: updatedMembership };
|
||||||
};
|
};
|
||||||
|
|
||||||
return { listOrgProjects, accessProject };
|
return { listOrgProjects, grantProjectAdminAccess };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ export enum OrgPermissionSubjects {
|
|||||||
Billing = "billing",
|
Billing = "billing",
|
||||||
SecretScanning = "secret-scanning",
|
SecretScanning = "secret-scanning",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
Kms = "kms"
|
Kms = "kms",
|
||||||
|
AdminConsole = "admin-console"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAdminConsoleAction {
|
||||||
|
GrantAccessProjects = "grant-access-projects"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrgPermissionSet =
|
export type OrgPermissionSet =
|
||||||
@@ -37,6 +42,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms];
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||||
|
|
||||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import { TOrgAdminAccessProjectDTO } from "./types";
|
|||||||
export const useOrgAdminAccessProject = () =>
|
export const useOrgAdminAccessProject = () =>
|
||||||
useMutation({
|
useMutation({
|
||||||
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
mutationFn: async ({ projectId }: TOrgAdminAccessProjectDTO) => {
|
||||||
const { data } = await apiRequest.post(`/api/v1/org-admin/projects/${projectId}/access`);
|
const { data } = await apiRequest.post(
|
||||||
|
`/api/v1/organization-admin/projects/${projectId}/grant-admin-access`
|
||||||
|
);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const useOrgAdminGetProjects = ({ search, offset, limit = 50 }: TOrgAdmin
|
|||||||
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
queryKey: orgAdminQueryKeys.getProjects({ search, offset, limit }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
const { data } = await apiRequest.get<{ projects: Workspace[]; count: number }>(
|
||||||
"/api/v1/org-admin/projects",
|
"/api/v1/organization-admin/projects",
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ import {
|
|||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
@@ -132,8 +131,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { workspaces, currentWorkspace } = useWorkspace();
|
const { workspaces, currentWorkspace } = useWorkspace();
|
||||||
const { orgs, currentOrg } = useOrganization();
|
const { orgs, currentOrg } = useOrganization();
|
||||||
const { membership } = useOrgPermission();
|
|
||||||
const isOrgAdmin = membership?.role === "admin";
|
|
||||||
|
|
||||||
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
const { data: projectFavorites } = useGetUserProjectFavorites(currentOrg?.id!);
|
||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
@@ -483,6 +480,11 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<Link href={`/org/${currentOrg?.id}/admin`} legacyBehavior>
|
||||||
|
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||||
|
Admin Panel
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</Link>
|
||||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||||
<button type="button" onClick={logOutUser} className="w-full">
|
<button type="button" onClick={logOutUser} className="w-full">
|
||||||
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
<DropdownMenuItem>Log Out</DropdownMenuItem>
|
||||||
@@ -751,18 +753,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
{isOrgAdmin && (
|
|
||||||
<Link href={`/org/${currentOrg?.id}/admin`} passHref>
|
|
||||||
<a>
|
|
||||||
<MenuItem
|
|
||||||
isSelected={router.asPath === `/org/${currentOrg?.id}/admin`}
|
|
||||||
icon="system-outline-109-slider-toggle-settings"
|
|
||||||
>
|
|
||||||
Admin Panel
|
|
||||||
</MenuItem>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<Link href={`/org/${currentOrg?.id}/settings`} passHref>
|
<Link href={`/org/${currentOrg?.id}/settings`} passHref>
|
||||||
<a>
|
<a>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { withPermission } from "@app/hoc";
|
|
||||||
import {
|
import {
|
||||||
fetchOrgUsers,
|
fetchOrgUsers,
|
||||||
useAddUserToWsNonE2EE,
|
useAddUserToWsNonE2EE,
|
||||||
@@ -488,8 +487,7 @@ const formSchema = yup.object({
|
|||||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
// #TODO: Update all the workspaceIds
|
// #TODO: Update all the workspaceIds
|
||||||
const OrganizationPage = withPermission(
|
const OrganizationPage = () => {
|
||||||
() => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -806,8 +804,8 @@ const OrganizationPage = withPermission(
|
|||||||
icon={faExclamationCircle}
|
icon={faExclamationCircle}
|
||||||
className="mr-4 p-4 text-2xl text-mineshaft-50"
|
className="mr-4 p-4 text-2xl text-mineshaft-50"
|
||||||
/>
|
/>
|
||||||
Attention: Updated versions of Infisical now require Redis for full functionality.
|
Attention: Updated versions of Infisical now require Redis for full functionality. Learn
|
||||||
Learn how to configure it
|
how to configure it
|
||||||
<Link
|
<Link
|
||||||
href="https://infisical.com/docs/self-hosting/configuration/redis"
|
href="https://infisical.com/docs/self-hosting/configuration/redis"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -933,10 +931,7 @@ const OrganizationPage = withPermission(
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!(
|
{!(new Date().getTime() - new Date(user?.createdAt).getTime() < 30 * 24 * 60 * 60 * 1000) && (
|
||||||
new Date().getTime() - new Date(user?.createdAt).getTime() <
|
|
||||||
30 * 24 * 60 * 60 * 1000
|
|
||||||
) && (
|
|
||||||
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
<div className="mb-4 flex flex-col items-start justify-start px-6 pb-0 text-3xl">
|
||||||
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
<p className="mr-4 mb-4 font-semibold text-white">Onboarding Guide</p>
|
||||||
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="mb-3 grid w-full grid-cols-1 gap-3 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
@@ -1003,9 +998,7 @@ const OrganizationPage = withPermission(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-28 pr-4 text-right text-sm font-semibold ${
|
className={`w-28 pr-4 text-right text-sm font-semibold ${false && "text-green"}`}
|
||||||
false && "text-green"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
About 2 min
|
About 2 min
|
||||||
</div>
|
</div>
|
||||||
@@ -1058,10 +1051,7 @@ const OrganizationPage = withPermission(
|
|||||||
name="addMembers"
|
name="addMembers"
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
render={({ field: { onBlur, value, onChange } }) => (
|
render={({ field: { onBlur, value, onChange } }) => (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
|
||||||
I={OrgPermissionActions.Read}
|
|
||||||
a={OrgPermissionSubjects.Member}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -1149,12 +1139,7 @@ const OrganizationPage = withPermission(
|
|||||||
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
{/* <DeleteUserDialog isOpen={isDeleteOpen} closeModal={closeDeleteModal} submitModal={deleteMembership} userIdToBeDeleted={userIdToBeDeleted}/> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
{
|
|
||||||
action: OrgPermissionActions.Read,
|
|
||||||
subject: OrgPermissionSubjects.Workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.assign(OrganizationPage, { requireAuth: true });
|
Object.assign(OrganizationPage, { requireAuth: true });
|
||||||
|
|
||||||
|
|||||||
-133
@@ -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();
|
.optional();
|
||||||
|
|
||||||
|
const adminConsolePermissionSchmea = z
|
||||||
|
.object({
|
||||||
|
"grant-access-projects": z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
description: z.string().trim().optional(),
|
description: z.string().trim().optional(),
|
||||||
@@ -23,7 +29,6 @@ export const formSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
workspace: z
|
workspace: z
|
||||||
.object({
|
.object({
|
||||||
read: z.boolean().optional(),
|
|
||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -38,7 +43,8 @@ export const formSchema = z.object({
|
|||||||
scim: generalPermissionSchema,
|
scim: generalPermissionSchema,
|
||||||
ldap: generalPermissionSchema,
|
ldap: generalPermissionSchema,
|
||||||
billing: generalPermissionSchema,
|
billing: generalPermissionSchema,
|
||||||
identity: generalPermissionSchema
|
identity: generalPermissionSchema,
|
||||||
|
"admin-console": adminConsolePermissionSchmea
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|||||||
+135
@@ -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: "grant-access-projects", label: "Grant access projects" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OrgPermissionAdminConsoleRow = ({ isEditable, control, setValue }: Props) => {
|
||||||
|
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.admin-console"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
if (rule?.["grant-access-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.admin-console",
|
||||||
|
{ "grant-access-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>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.admin-console.${action}`}
|
||||||
|
key={`permissions.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.admin-console.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+129
@@ -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.admin-console.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-1
@@ -61,7 +61,10 @@ const getPermissionList = (option: string) => {
|
|||||||
type Props = {
|
type Props = {
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
formName: keyof Omit<Exclude<TFormSchema["permissions"], undefined>, "workspace">;
|
formName: keyof Omit<
|
||||||
|
Exclude<TFormSchema["permissions"], undefined>,
|
||||||
|
"workspace" | "admin-console"
|
||||||
|
>;
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
control: Control<TFormSchema>;
|
control: Control<TFormSchema>;
|
||||||
};
|
};
|
||||||
|
|||||||
+12
@@ -12,6 +12,8 @@ import {
|
|||||||
TFormSchema
|
TFormSchema
|
||||||
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
} from "@app/views/Org/RolePage/components/OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||||
|
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||||
import { RolePermissionRow } from "./RolePermissionRow";
|
import { RolePermissionRow } from "./RolePermissionRow";
|
||||||
|
|
||||||
const SIMPLE_PERMISSION_OPTIONS = [
|
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>
|
</TBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -23,11 +23,18 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr} from "@app/components/v2";
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
OrgPermissionAdminConsoleAction,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { withPermission } from "@app/hoc";
|
||||||
import { useDebounce } from "@app/hooks";
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
import { useOrgAdminAccessProject, useOrgAdminGetProjects } from "@app/hooks/api";
|
||||||
|
|
||||||
export const OrgAdminProjects = () => {
|
export const OrgAdminProjects = withPermission(
|
||||||
|
() => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const debouncedSearch = useDebounce(search);
|
const debouncedSearch = useDebounce(search);
|
||||||
@@ -152,4 +159,9 @@ export const OrgAdminProjects = () => {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
},
|
||||||
|
{
|
||||||
|
action: OrgPermissionAdminConsoleAction.GrantAccessProjects,
|
||||||
|
subject: OrgPermissionSubjects.AdminConsole
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user