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,
@@ -28,6 +28,15 @@ export type TIdentityProjectPrivilege = {
} }
); );
export type TProjectSpecificPrivilegePermission = {
conditions: {
environment: string;
secretPath?: { $glob: string };
};
actions: string[];
subject: string;
};
export type TCreateIdentityProjectPrivilegeDTO = { export type TCreateIdentityProjectPrivilegeDTO = {
identityId: string; identityId: string;
projectSlug: string; projectSlug: 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,19 +5,13 @@ 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";
@@ -33,20 +27,17 @@ 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,
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 projectSlug = currentWorkspace?.slug || "";
const { data: groups } = useGetOrganizationGroups(orgId); const { data: groups } = useGetOrganizationGroups(orgId);
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || ""); const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
const { data: roles } = useGetProjectRoles(workspaceId); const { data: roles } = useGetProjectRoles(projectSlug);
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace(); const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
@@ -84,14 +75,13 @@ export const GroupModal = ({
text: "Successfully added group to project", text: "Successfully added group to project",
type: "success" type: "success"
}); });
} catch (err) { } catch (err) {
createNotification({ createNotification({
text: "Failed to add group to project", text: "Failed to add group to project",
type: "error" type: "error"
}); });
} }
} };
return ( return (
<Modal <Modal
@@ -179,4 +169,4 @@ export const GroupModal = ({
</ModalContent> </ModalContent>
</Modal> </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();
@@ -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,
@@ -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 }) => ({
action,
subject: ProjectPermissionSub.Secrets, subject: ProjectPermissionSub.Secrets,
conditions conditions: {
})) environment: data.environmentSlug,
...(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 environment: currentWorkspace?.environments?.[0].slug as string
} }
} },
],
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,
@@ -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,8 +145,7 @@ 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")}
@@ -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 });
} }
}); });
} }