feat: privilege api better permission inputs and required changes in ui for role and privilege

This commit is contained in:
=
2024-05-26 22:49:35 +05:30
parent 719f3beab0
commit ee5130f56c
18 changed files with 397 additions and 304 deletions
@@ -5,10 +5,15 @@ import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ProjectPermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas"; import {
ProjectPermissionSchema,
ProjectSpecificPrivilegePermissionSchema,
SanitizedIdentityPrivilegeSchema
} from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -39,7 +44,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
).optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -49,6 +59,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission } = req.body;
if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
}
const permission = privilegePermission
? privilegePermission.actions.map((action) => ({
action,
subject: privilegePermission.subject,
conditions: privilegePermission.conditions
}))
: permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.create({ const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -57,7 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false, isTemporary: false,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: JSON.stringify(packRules(permission))
}); });
return { privilege }; return { privilege };
} }
@@ -90,7 +112,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
).optional(),
temporaryMode: z temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -111,6 +138,19 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const { permissions, privilegePermission } = req.body;
if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
}
const permission = privilegePermission
? privilegePermission.actions.map((action) => ({
action,
subject: privilegePermission.subject,
conditions: privilegePermission.conditions
}))
: permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.create({ const privilege = await server.services.identityProjectAdditionalPrivilege.create({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -119,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true, isTemporary: true,
permissions: JSON.stringify(packRules(req.body.permissions)) permissions: JSON.stringify(packRules(permission))
}); });
return { privilege }; return { privilege };
} }
@@ -156,13 +196,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}) })
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
).optional(),
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
temporaryMode: z temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z temporaryRange: z
.string() .string()
.refine((val) => ms(val) > 0, "Temporary range must be a positive number") .refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number")
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange), .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
temporaryAccessStartTime: z temporaryAccessStartTime: z
.string() .string()
@@ -179,7 +222,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => { handler: async (req) => {
const updatedInfo = req.body.privilegeDetails; const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
if (!permissions && !privilegePermission) {
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
}
const permission = privilegePermission
? privilegePermission.actions.map((action) => ({
action,
subject: privilegePermission.subject,
conditions: privilegePermission.conditions
}))
: permissions!;
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({ const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -190,7 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: req.body.projectSlug, projectSlug: req.body.projectSlug,
data: { data: {
...updatedInfo, ...updatedInfo,
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined permissions: permission ? JSON.stringify(packRules(permission)) : undefined
} }
}); });
return { privilege }; return { privilege };
+2 -1
View File
@@ -521,7 +521,8 @@ export const registerRoutes = async (
permissionService, permissionService,
projectRoleDAL, projectRoleDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL identityProjectMembershipRoleDAL,
projectDAL
}); });
const snapshotService = secretSnapshotServiceFactory({ const snapshotService = secretSnapshotServiceFactory({
@@ -4,6 +4,7 @@ import {
DynamicSecretsSchema, DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema, IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema, IntegrationAuthsSchema,
ProjectRolesSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
@@ -88,10 +89,38 @@ export const ProjectPermissionSchema = z.object({
.optional() .optional()
}); });
export const ProjectSpecificPrivilegePermissionSchema = z.object({
actions: z
.nativeEnum(ProjectPermissionActions)
.describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read")
.array()
.min(1),
subject: z
.enum([ProjectPermissionSub.Secrets])
.describe("The entity this permission pertains to. Possible options: secrets, environments"),
conditions: z
.object({
environment: z.string().describe("The environment slug this permission should allow."),
secretPath: z
.object({
$glob: z
.string()
.min(1)
.describe("The secret path this permission should allow. Can be a glob pattern such as /folder-name/*/** ")
})
.optional()
})
.describe("When specified, only matching conditions will be allowed to access given resource.")
});
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({ export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
permissions: UnpackedPermissionSchema.array() permissions: UnpackedPermissionSchema.array()
}); });
export const SanitizedRoleSchema = ProjectRolesSchema.extend({
permissions: UnpackedPermissionSchema.array()
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true, inputIV: true,
inputTag: true, inputTag: true,
@@ -12,21 +12,30 @@ export type TIdentityProjectPrivilege = {
updatedAt: Date; updatedAt: Date;
permissions?: TProjectPermission[]; permissions?: TProjectPermission[];
} & ( } & (
| { | {
isTemporary: true; isTemporary: true;
temporaryMode: string; temporaryMode: string;
temporaryRange: string; temporaryRange: string;
temporaryAccessStartTime: string; temporaryAccessStartTime: string;
temporaryAccessEndTime?: string; temporaryAccessEndTime?: string;
} }
| { | {
isTemporary: false; isTemporary: false;
temporaryMode?: null; temporaryMode?: null;
temporaryRange?: null; temporaryRange?: null;
temporaryAccessStartTime?: null; temporaryAccessStartTime?: null;
temporaryAccessEndTime?: null; temporaryAccessEndTime?: null;
} }
); );
export type TProjectSpecificPrivilegePermission = {
conditions: {
environment: string;
secretPath?: { $glob: string };
};
actions: string[];
subject: string;
};
export type TCreateIdentityProjectPrivilegeDTO = { export type TCreateIdentityProjectPrivilegeDTO = {
identityId: string; identityId: string;
@@ -36,14 +45,16 @@ export type TCreateIdentityProjectPrivilegeDTO = {
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode; temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
temporaryRange?: string; temporaryRange?: string;
temporaryAccessStartTime?: string; temporaryAccessStartTime?: string;
permissions: TProjectPermission[]; privilegePermission: TProjectSpecificPrivilegePermission;
}; };
export type TUpdateIdentityProjectPrivlegeDTO = { export type TUpdateIdentityProjectPrivlegeDTO = {
projectSlug: string; projectSlug: string;
identityId: string; identityId: string;
privilegeSlug: string; privilegeSlug: string;
privilegeDetails: Partial<Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">>; privilegeDetails: Partial<
Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">
>;
}; };
export type TDeleteIdentityProjectPrivilegeDTO = { export type TDeleteIdentityProjectPrivilegeDTO = {
+1
View File
@@ -8,6 +8,7 @@ export {
} from "./mutation"; } from "./mutation";
export { export {
useGetOrgRoles, useGetOrgRoles,
useGetProjectRoleBySlug,
useGetProjectRoles, useGetProjectRoles,
useGetUserOrgPermissions, useGetUserOrgPermissions,
useGetUserProjectPermissions useGetUserProjectPermissions
+12 -20
View File
@@ -17,13 +17,10 @@ export const useCreateProjectRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ projectId, permissions, ...dto }: TCreateProjectRoleDTO) => mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
apiRequest.post(`/api/v1/workspace/${projectId}/roles`, { apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
...dto, onSuccess: (_, { projectSlug }) => {
permissions: permissions.length ? packRules(permissions) : [] queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
} }
}); });
}; };
@@ -32,13 +29,10 @@ export const useUpdateProjectRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ id, projectId, permissions, ...dto }: TUpdateProjectRoleDTO) => mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
apiRequest.patch(`/api/v1/workspace/${projectId}/roles/${id}`, { apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
...dto, onSuccess: (_, { projectSlug }) => {
permissions: permissions?.length ? packRules(permissions) : [] queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
} }
}); });
}; };
@@ -47,12 +41,10 @@ export const useDeleteProjectRole = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ projectId, id }: TDeleteProjectRoleDTO) => mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
apiRequest.delete(`/api/v1/workspace/${projectId}/roles/${id}`, { apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
data: { projectId } onSuccess: (_, { projectSlug }) => {
}), queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
} }
}); });
}; };
+23 -13
View File
@@ -14,7 +14,6 @@ import {
TGetUserProjectPermissionDTO, TGetUserProjectPermissionDTO,
TOrgRole, TOrgRole,
TPermission, TPermission,
TProjectPermission,
TProjectRole TProjectRole
} from "./types"; } from "./types";
@@ -37,7 +36,9 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob }); const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
export const roleQueryKeys = { export const roleQueryKeys = {
getProjectRoles: (projectId: string) => ["roles", { projectId }] as const, getProjectRoles: (projectSlug: string) => ["roles", { projectSlug }] as const,
getProjectRoleBySlug: (projectSlug: string, roleSlug: string) =>
["roles", { projectSlug, roleSlug }] as const,
getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const, getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) => getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
["user-permissions", { orgId }] as const, ["user-permissions", { orgId }] as const,
@@ -46,20 +47,29 @@ export const roleQueryKeys = {
}; };
const getProjectRoles = async (projectId: string) => { const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
data: { roles: Array<Omit<TProjectRole, "permissions"> & { permissions: unknown }> }; `/api/v1/workspace/${projectId}/roles`
}>(`/api/v1/workspace/${projectId}/roles`); );
return data.data.roles.map(({ permissions, ...el }) => ({ return data.roles;
...el,
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
}));
}; };
export const useGetProjectRoles = (projectId: string) => export const useGetProjectRoles = (projectSlug: string) =>
useQuery({ useQuery({
queryKey: roleQueryKeys.getProjectRoles(projectId), queryKey: roleQueryKeys.getProjectRoles(projectSlug),
queryFn: () => getProjectRoles(projectId), queryFn: () => getProjectRoles(projectSlug),
enabled: Boolean(projectId) enabled: Boolean(projectSlug)
});
export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =>
useQuery({
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
queryFn: async () => {
const { data } = await apiRequest.get<{ role: TProjectRole }>(
`/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}`
);
return data.role;
},
enabled: Boolean(projectSlug && roleSlug)
}); });
const getOrgRoles = async (orgId: string) => { const getOrgRoles = async (orgId: string) => {
+3 -3
View File
@@ -71,7 +71,7 @@ export type TDeleteOrgRoleDTO = {
}; };
export type TCreateProjectRoleDTO = { export type TCreateProjectRoleDTO = {
projectId: string; projectSlug: string;
name: string; name: string;
description?: string; description?: string;
slug: string; slug: string;
@@ -79,11 +79,11 @@ export type TCreateProjectRoleDTO = {
}; };
export type TUpdateProjectRoleDTO = { export type TUpdateProjectRoleDTO = {
projectId: string; projectSlug: string;
id: string; id: string;
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>; } & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
export type TDeleteProjectRoleDTO = { export type TDeleteProjectRoleDTO = {
projectId: string; projectSlug: string;
id: string; id: string;
}; };
@@ -5,25 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
Button,
FormControl,
Modal,
ModalContent,
Select,
SelectItem} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { import {
useAddGroupToWorkspace, useAddGroupToWorkspace,
useGetOrganizationGroups, useGetOrganizationGroups,
useGetProjectRoles, useGetProjectRoles,
useListWorkspaceGroups, useListWorkspaceGroups
} from "@app/hooks/api"; } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({ const schema = z.object({
slug: z.string(), slug: z.string(),
role: z.string() role: z.string()
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -33,150 +27,146 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
}; };
export const GroupModal = ({ export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
popUp, const { currentOrg } = useOrganization();
handlePopUpToggle const { currentWorkspace } = useWorkspace();
}: Props) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: groups } = useGetOrganizationGroups(orgId);
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
const { data: roles } = useGetProjectRoles(workspaceId);
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
const filteredGroupMembershipOrgs = useMemo(() => {
const wsGroupIds = new Map();
groupMemberships?.forEach((groupMembership) => { const { data: groups } = useGetOrganizationGroups(orgId);
wsGroupIds.set(groupMembership.group.id, true); const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
});
return (groups || []).filter(({ id }) => !wsGroupIds.has(id)); const { data: roles } = useGetProjectRoles(projectSlug);
}, [groups, groupMemberships]);
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
const {
control, const filteredGroupMembershipOrgs = useMemo(() => {
handleSubmit, const wsGroupIds = new Map();
reset,
formState: { isSubmitting } groupMemberships?.forEach((groupMembership) => {
} = useForm<FormData>({ wsGroupIds.set(groupMembership.group.id, true);
resolver: zodResolver(schema) });
return (groups || []).filter(({ id }) => !wsGroupIds.has(id));
}, [groups, groupMemberships]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const onFormSubmit = async ({ slug, role }: FormData) => {
try {
await addGroupToWorkspaceMutateAsync({
projectSlug: currentWorkspace?.slug || "",
groupSlug: slug,
role: role || undefined
}); });
const onFormSubmit = async ({ slug, role }: FormData) => { reset();
try { handlePopUpToggle("group", false);
await addGroupToWorkspaceMutateAsync({
projectSlug: currentWorkspace?.slug || "", createNotification({
groupSlug: slug, text: "Successfully added group to project",
role: role || undefined type: "success"
}); });
} catch (err) {
reset(); createNotification({
handlePopUpToggle("group", false); text: "Failed to add group to project",
type: "error"
createNotification({ });
text: "Successfully added group to project",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed to add group to project",
type: "error"
});
}
} }
};
return (
<Modal return (
isOpen={popUp?.group?.isOpen} <Modal
onOpenChange={(isOpen) => { isOpen={popUp?.group?.isOpen}
handlePopUpToggle("group", isOpen); onOpenChange={(isOpen) => {
reset(); handlePopUpToggle("group", isOpen);
}} reset();
> }}
<ModalContent title="Add Group to Project"> >
{filteredGroupMembershipOrgs.length ? ( <ModalContent title="Add Group to Project">
<form onSubmit={handleSubmit(onFormSubmit)}> {filteredGroupMembershipOrgs.length ? (
<Controller <form onSubmit={handleSubmit(onFormSubmit)}>
control={control} <Controller
name="slug" control={control}
defaultValue={filteredGroupMembershipOrgs?.[0]?.id} name="slug"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}> render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<Select <FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
defaultValue={field.value} <Select
{...field} defaultValue={field.value}
onValueChange={(e) => onChange(e)} {...field}
className="w-full" onValueChange={(e) => onChange(e)}
> className="w-full"
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => ( >
<SelectItem value={slug} key={`org-group-${id}`}> {filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
{name} <SelectItem value={slug} key={`org-group-${id}`}>
</SelectItem> {name}
))} </SelectItem>
</Select> ))}
</FormControl> </Select>
)} </FormControl>
/> )}
<Controller />
control={control} <Controller
name="role" control={control}
defaultValue="" name="role"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( defaultValue=""
<FormControl render={({ field: { onChange, ...field }, fieldState: { error } }) => (
label="Role" <FormControl
errorText={error?.message} label="Role"
isError={Boolean(error)} errorText={error?.message}
className="mt-4" isError={Boolean(error)}
> className="mt-4"
<Select >
defaultValue={field.value} <Select
{...field} defaultValue={field.value}
onValueChange={(e) => onChange(e)} {...field}
className="w-full" onValueChange={(e) => onChange(e)}
> className="w-full"
{(roles || []).map(({ name, slug }) => ( >
<SelectItem value={slug} key={`st-role-${slug}`}> {(roles || []).map(({ name, slug }) => (
{name} <SelectItem value={slug} key={`st-role-${slug}`}>
</SelectItem> {name}
))} </SelectItem>
</Select> ))}
</FormControl> </Select>
)} </FormControl>
/> )}
<div className="flex items-center"> />
<Button <div className="flex items-center">
className="mr-4" <Button
size="sm" className="mr-4"
type="submit" size="sm"
isLoading={isSubmitting} type="submit"
isDisabled={isSubmitting} isLoading={isSubmitting}
> isDisabled={isSubmitting}
{popUp?.group?.data ? "Update" : "Create"} >
</Button> {popUp?.group?.data ? "Update" : "Create"}
<Button colorSchema="secondary" variant="plain"> </Button>
Cancel <Button colorSchema="secondary" variant="plain">
</Button> Cancel
</div> </Button>
</form> </div>
) : ( </form>
<div className="flex flex-col space-y-4"> ) : (
<div className="text-sm"> <div className="flex flex-col space-y-4">
All groups in your organization have already been added to this project. <div className="text-sm">
</div> All groups in your organization have already been added to this project.
<Link href={`/org/${currentWorkspace?.orgId}/members`}> </div>
<Button variant="outline_bg">Create a new group</Button> <Link href={`/org/${currentWorkspace?.orgId}/members`}>
</Link> <Button variant="outline_bg">Create a new group</Button>
</div> </Link>
)} </div>
</ModalContent> )}
</Modal> </ModalContent>
); </Modal>
} );
};
@@ -201,11 +201,7 @@ export type TMemberRolesProp = {
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
export const GroupRoles = ({ export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemberRolesProp) => {
roles = [],
disableEdit = false,
groupSlug
}: TMemberRolesProp) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
const [searchRoles, setSearchRoles] = useState(""); const [searchRoles, setSearchRoles] = useState("");
@@ -220,9 +216,9 @@ export const GroupRoles = ({
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole(); const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
@@ -317,7 +313,7 @@ export const GroupRoles = ({
icon={faClock} icon={faClock}
className={twMerge( className={twMerge(
new Date() > new Date(temporaryAccessEndTime as string) && new Date() > new Date(temporaryAccessEndTime as string) &&
"text-red-600" "text-red-600"
)} )}
/> />
</Tooltip> </Tooltip>
@@ -390,14 +386,14 @@ export const GroupRoles = ({
defaultValue={ defaultValue={
userProjectRoleDetails?.isTemporary userProjectRoleDetails?.isTemporary
? { ? {
isTemporary: true, isTemporary: true,
temporaryAccessStartTime: temporaryAccessStartTime:
userProjectRoleDetails.temporaryAccessStartTime as string, userProjectRoleDetails.temporaryAccessStartTime as string,
temporaryRange: temporaryRange:
userProjectRoleDetails.temporaryRange as string, userProjectRoleDetails.temporaryRange as string,
temporaryAccessEndTime: temporaryAccessEndTime:
userProjectRoleDetails.temporaryAccessEndTime userProjectRoleDetails.temporaryAccessEndTime
} }
: false : false
} }
render={({ field }) => ( render={({ field }) => (
@@ -30,17 +30,17 @@ type Props = {
}; };
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => { export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || ""; const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId); const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId); const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
const { data: roles } = useGetProjectRoles(workspaceId); const { data: roles } = useGetProjectRoles(projectSlug);
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace(); const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
@@ -65,7 +65,8 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const projectSlug = currentWorkspace?.slug || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const isMemberEditDisabled = permission.cannot( const isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
@@ -79,14 +80,14 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
slug: customRoleSlug || role, slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary temporaryAccess: dto.isTemporary
? { ? {
isTemporary: true, isTemporary: true,
temporaryRange: dto.temporaryRange, temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime, temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime temporaryAccessStartTime: dto.temporaryAccessStartTime
} }
: { : {
isTemporary: dto.isTemporary isTemporary: dto.isTemporary
} }
})) }))
} }
}); });
@@ -191,9 +192,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
? isExpired ? isExpired
? "Timed Access Expired" ? "Timed Access Expired"
: `Until ${format( : `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss" "yyyy-MM-dd HH:mm:ss"
)}` )}`
: "Non expiry access" : "Non expiry access"
} }
> >
@@ -212,9 +213,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
? isExpired ? isExpired
? "Access Expired" ? "Access Expired"
: formatDistance( : formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date() new Date()
) )
: "Permanent"} : "Permanent"}
</Button> </Button>
</Tooltip> </Tooltip>
@@ -338,7 +339,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
type="submit" type="submit"
className={twMerge( className={twMerge(
"transition-all", "transition-all",
"opacity-0 cursor-default", "cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100" roleForm.formState.isDirty && "cursor-pointer opacity-100"
)} )}
isDisabled={!roleForm.formState.isDirty} isDisabled={!roleForm.formState.isDirty}
@@ -131,20 +131,17 @@ const SpecificPrivilegeSecretForm = ({
{ action: ProjectPermissionActions.Delete, allowed: data.delete }, { action: ProjectPermissionActions.Delete, allowed: data.delete },
{ action: ProjectPermissionActions.Edit, allowed: data.edit } { action: ProjectPermissionActions.Edit, allowed: data.edit }
]; ];
const conditions: Record<string, any> = { environment: data.environmentSlug };
if (data.secretPath) {
conditions.secretPath = { $glob: data.secretPath };
}
await updateIdentityPrivilege.mutateAsync({ await updateIdentityPrivilege.mutateAsync({
privilegeDetails: { privilegeDetails: {
...data.temporaryAccess, ...data.temporaryAccess,
permissions: actions privilegePermission: {
.filter(({ allowed }) => allowed) actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action),
.map(({ action }) => ({ subject: ProjectPermissionSub.Secrets,
action, conditions: {
subject: ProjectPermissionSub.Secrets, environment: data.environmentSlug,
conditions ...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {})
})) }
}
}, },
privilegeSlug: privilege.slug, privilegeSlug: privilege.slug,
identityId, identityId,
@@ -474,15 +471,13 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
if (createIdentityPrivilege.isLoading) return; if (createIdentityPrivilege.isLoading) return;
try { try {
await createIdentityPrivilege.mutateAsync({ await createIdentityPrivilege.mutateAsync({
permissions: [ privilegePermission: {
{ actions: [ProjectPermissionActions.Read],
action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Secrets,
subject: ProjectPermissionSub.Secrets, conditions: {
conditions: { environment: currentWorkspace?.environments?.[0].slug as string
environment: currentWorkspace?.environments?.[0].slug
}
} }
], },
identityId, identityId,
projectSlug projectSlug
}); });
@@ -65,7 +65,8 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const workspaceId = currentWorkspace?.id || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const projectSlug = currentWorkspace?.slug || "";
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const isMemberEditDisabled = permission.cannot( const isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
@@ -79,14 +80,14 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
slug: customRoleSlug || role, slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary temporaryAccess: dto.isTemporary
? { ? {
isTemporary: true, isTemporary: true,
temporaryRange: dto.temporaryRange, temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime, temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime temporaryAccessStartTime: dto.temporaryAccessStartTime
} }
: { : {
isTemporary: dto.isTemporary isTemporary: dto.isTemporary
} }
})) }))
} }
}); });
@@ -191,9 +192,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
? isExpired ? isExpired
? "Timed Access Expired" ? "Timed Access Expired"
: `Until ${format( : `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss" "yyyy-MM-dd HH:mm:ss"
)}` )}`
: "Non expiry access" : "Non expiry access"
} }
> >
@@ -212,9 +213,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
? isExpired ? isExpired
? "Access Expired" ? "Access Expired"
: formatDistance( : formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""), new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date() new Date()
) )
: "Permanent"} : "Permanent"}
</Button> </Button>
</Tooltip> </Tooltip>
@@ -335,7 +336,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
type="submit" type="submit"
className={twMerge( className={twMerge(
"transition-all", "transition-all",
"opacity-0 cursor-default", "cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100" roleForm.formState.isDirty && "cursor-pointer opacity-100"
)} )}
isDisabled={!roleForm.formState.isDirty} isDisabled={!roleForm.formState.isDirty}
@@ -3,7 +3,6 @@ import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc"; import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectRoleList } from "./components/ProjectRoleList"; import { ProjectRoleList } from "./components/ProjectRoleList";
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection"; import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
@@ -21,7 +20,7 @@ export const ProjectRoleListTab = withProjectPermission(
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<ProjectRoleModifySection <ProjectRoleModifySection
role={popUp.editRole.data as TProjectRole} roleSlug={popUp.editRole.data as string}
onGoBack={() => handlePopUpClose("editRole")} onGoBack={() => handlePopUpClose("editRole")}
/> />
</motion.div> </motion.div>
@@ -33,7 +32,7 @@ export const ProjectRoleListTab = withProjectPermission(
animate={{ opacity: 1, translateX: 0 }} animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }} exit={{ opacity: 0, translateX: -30 }}
> >
<ProjectRoleList onSelectRole={(role) => handlePopUpOpen("editRole", role)} /> <ProjectRoleList onSelectRole={(slug) => handlePopUpOpen("editRole", slug)} />
</motion.div> </motion.div>
); );
}, },
@@ -24,7 +24,7 @@ import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types"; import { TProjectRole } from "@app/hooks/api/roles/types";
type Props = { type Props = {
onSelectRole: (role?: TProjectRole) => void; onSelectRole: (slug?: string) => void;
}; };
export const ProjectRoleList = ({ onSelectRole }: Props) => { export const ProjectRoleList = ({ onSelectRole }: Props) => {
@@ -32,10 +32,9 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const); const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
console.log(roles);
const { mutateAsync: deleteRole } = useDeleteProjectRole(); const { mutateAsync: deleteRole } = useDeleteProjectRole();
@@ -43,7 +42,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { id } = popUp?.deleteRole?.data as TProjectRole; const { id } = popUp?.deleteRole?.data as TProjectRole;
try { try {
await deleteRole({ await deleteRole({
projectId: workspaceId, projectSlug,
id id
}); });
createNotification({ type: "success", text: "Successfully removed the role" }); createNotification({ type: "success", text: "Successfully removed the role" });
@@ -109,7 +108,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
<IconButton <IconButton
isDisabled={!isAllowed} isDisabled={!isAllowed}
ariaLabel="edit" ariaLabel="edit"
onClick={() => onSelectRole(role)} onClick={() => onSelectRole(role.slug)}
variant="plain" variant="plain"
> >
<FontAwesomeIcon icon={faEdit} /> <FontAwesomeIcon icon={faEdit} />
@@ -146,9 +145,8 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
</div> </div>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteRole.isOpen} isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${ title={`Are you sure want to delete ${(popUp?.deleteRole?.data as TProjectRole)?.name || " "
(popUp?.deleteRole?.data as TProjectRole)?.name || " " } role?`}
} role?`}
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""} deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
onClose={() => handlePopUpClose("deleteRole")} onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete} onDeleteApproved={handleRoleDelete}
@@ -19,9 +19,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input, Spinner } from "@app/components/v2";
import { ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { useCreateProjectRole, useUpdateProjectRole } from "@app/hooks/api"; import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole
} from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types"; import { TProjectRole } from "@app/hooks/api/roles/types";
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission"; import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
@@ -117,17 +121,20 @@ const SINGLE_PERMISSION_LIST = [
] as const; ] as const;
type Props = { type Props = {
role?: TProjectRole; roleSlug?: string;
onGoBack: VoidFunction; onGoBack: VoidFunction;
}; };
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => { export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(role?.slug || ""); const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
const isNewRole = !role?.slug; const isNewRole = !roleSlug;
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || ""; const projectSlug = currentWorkspace?.slug || "";
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
currentWorkspace?.slug || "",
roleSlug as string
);
const { const {
handleSubmit, handleSubmit,
@@ -137,19 +144,21 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
getValues, getValues,
control control
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, values: roleDetails
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
: ({} as TProjectRole),
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const { mutateAsync: createRole } = useCreateProjectRole(); const { mutateAsync: createRole } = useCreateProjectRole();
const { mutateAsync: updateRole } = useUpdateProjectRole(); const { mutateAsync: updateRole } = useUpdateProjectRole();
const handleRoleUpdate = async (el: TFormSchema) => { const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?.id) return; if (!roleDetails?.id) return;
try { try {
await updateRole({ await updateRole({
id: role?.id, id: roleDetails?.id as string,
projectId: workspaceId, projectSlug,
...el, ...el,
permissions: formRolePermission2API(el.permissions) permissions: formRolePermission2API(el.permissions)
}); });
@@ -169,7 +178,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
try { try {
await createRole({ await createRole({
projectId: workspaceId, projectSlug,
...el, ...el,
permissions: formRolePermission2API(el.permissions) permissions: formRolePermission2API(el.permissions)
}); });
@@ -181,6 +190,14 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
} }
}; };
if (!isNewRole && isRoleDetailsLoading) {
return (
<div className="flex w-full items-center justify-center p-8">
<Spinner />
</div>
);
}
return ( return (
<div> <div>
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
@@ -95,10 +95,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: Record<string, any> = {}; const formVal: Record<string, any> = {};
permissions.forEach((permission) => { permissions.forEach((permission) => {
const { const { subject: caslSub, action } = permission;
subject: [subject], const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
action
} = permission;
if (!formVal?.[subject]) formVal[subject] = {}; if (!formVal?.[subject]) formVal[subject] = {};
if (subject === "secrets") { if (subject === "secrets") {
@@ -123,7 +121,7 @@ const multiEnvForm2Api = (
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]); const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
// if any of them is set in all push it without any condition // if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => { PERMISSION_ACTIONS.forEach((action) => {
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] }); if (formVal?.all?.[action]) permissions.push({ action, subject });
}); });
if (!isFullAccess) { if (!isFullAccess) {
@@ -144,7 +142,7 @@ const multiEnvForm2Api = (
if (formVal[slug]?.secretPath) if (formVal[slug]?.secretPath)
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath }; conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
permissions.push({ action, subject: [subject], conditions }); permissions.push({ action, subject, conditions });
} }
}); });
}); });
@@ -161,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
} else { } else {
Object.entries(actions).forEach(([action, isAllowed]) => { Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) { if (isAllowed) {
permissions.push({ subject: [rule], action }); permissions.push({ subject: rule, action });
} }
}); });
} }