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

This commit is contained in:
=
2024-05-26 22:47:19 +05:30
parent 719f3beab0
commit ee5130f56c
18 changed files with 397 additions and 304 deletions

View File

@@ -5,10 +5,15 @@ import { z } from "zod";
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 { BadRequestError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
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";
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
@@ -39,7 +44,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.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: {
200: z.object({
@@ -49,6 +59,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
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({
actorId: req.permission.id,
actor: req.permission.type,
@@ -57,7 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: false,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: JSON.stringify(packRules(permission))
});
return { privilege };
}
@@ -90,7 +112,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.optional()
.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
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
@@ -111,6 +138,19 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
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({
actorId: req.permission.id,
actor: req.permission.type,
@@ -119,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
isTemporary: true,
permissions: JSON.stringify(packRules(req.body.permissions))
permissions: JSON.stringify(packRules(permission))
});
return { privilege };
}
@@ -156,13 +196,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
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),
temporaryMode: z
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
temporaryRange: z
.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),
temporaryAccessStartTime: z
.string()
@@ -179,7 +222,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
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({
actorId: req.permission.id,
actor: req.permission.type,
@@ -190,7 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: req.body.projectSlug,
data: {
...updatedInfo,
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
permissions: permission ? JSON.stringify(packRules(permission)) : undefined
}
});
return { privilege };

View File

@@ -521,7 +521,8 @@ export const registerRoutes = async (
permissionService,
projectRoleDAL,
projectUserMembershipRoleDAL,
identityProjectMembershipRoleDAL
identityProjectMembershipRoleDAL,
projectDAL
});
const snapshotService = secretSnapshotServiceFactory({

View File

@@ -4,6 +4,7 @@ import {
DynamicSecretsSchema,
IdentityProjectAdditionalPrivilegeSchema,
IntegrationAuthsSchema,
ProjectRolesSchema,
SecretApprovalPoliciesSchema,
UsersSchema
} from "@app/db/schemas";
@@ -88,10 +89,38 @@ export const ProjectPermissionSchema = z.object({
.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({
permissions: UnpackedPermissionSchema.array()
});
export const SanitizedRoleSchema = ProjectRolesSchema.extend({
permissions: UnpackedPermissionSchema.array()
});
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
inputIV: true,
inputTag: true,

View File

@@ -12,21 +12,30 @@ export type TIdentityProjectPrivilege = {
updatedAt: Date;
permissions?: TProjectPermission[];
} & (
| {
| {
isTemporary: true;
temporaryMode: string;
temporaryRange: string;
temporaryAccessStartTime: string;
temporaryAccessEndTime?: string;
}
| {
| {
isTemporary: false;
temporaryMode?: null;
temporaryRange?: null;
temporaryAccessStartTime?: null;
temporaryAccessEndTime?: null;
}
);
);
export type TProjectSpecificPrivilegePermission = {
conditions: {
environment: string;
secretPath?: { $glob: string };
};
actions: string[];
subject: string;
};
export type TCreateIdentityProjectPrivilegeDTO = {
identityId: string;
@@ -36,14 +45,16 @@ export type TCreateIdentityProjectPrivilegeDTO = {
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
temporaryRange?: string;
temporaryAccessStartTime?: string;
permissions: TProjectPermission[];
privilegePermission: TProjectSpecificPrivilegePermission;
};
export type TUpdateIdentityProjectPrivlegeDTO = {
projectSlug: string;
identityId: string;
privilegeSlug: string;
privilegeDetails: Partial<Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">>;
privilegeDetails: Partial<
Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">
>;
};
export type TDeleteIdentityProjectPrivilegeDTO = {

View File

@@ -8,6 +8,7 @@ export {
} from "./mutation";
export {
useGetOrgRoles,
useGetProjectRoleBySlug,
useGetProjectRoles,
useGetUserOrgPermissions,
useGetUserProjectPermissions

View File

@@ -17,13 +17,10 @@ export const useCreateProjectRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ projectId, permissions, ...dto }: TCreateProjectRoleDTO) =>
apiRequest.post(`/api/v1/workspace/${projectId}/roles`, {
...dto,
permissions: permissions.length ? packRules(permissions) : []
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}
});
};
@@ -32,13 +29,10 @@ export const useUpdateProjectRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, projectId, permissions, ...dto }: TUpdateProjectRoleDTO) =>
apiRequest.patch(`/api/v1/workspace/${projectId}/roles/${id}`, {
...dto,
permissions: permissions?.length ? packRules(permissions) : []
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}
});
};
@@ -47,12 +41,10 @@ export const useDeleteProjectRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ projectId, id }: TDeleteProjectRoleDTO) =>
apiRequest.delete(`/api/v1/workspace/${projectId}/roles/${id}`, {
data: { projectId }
}),
onSuccess: (_, { projectId }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
onSuccess: (_, { projectSlug }) => {
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
}
});
};

View File

@@ -14,7 +14,6 @@ import {
TGetUserProjectPermissionDTO,
TOrgRole,
TPermission,
TProjectPermission,
TProjectRole
} from "./types";
@@ -37,7 +36,9 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
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,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
["user-permissions", { orgId }] as const,
@@ -46,20 +47,29 @@ export const roleQueryKeys = {
};
const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{
data: { roles: Array<Omit<TProjectRole, "permissions"> & { permissions: unknown }> };
}>(`/api/v1/workspace/${projectId}/roles`);
return data.data.roles.map(({ permissions, ...el }) => ({
...el,
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
}));
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v1/workspace/${projectId}/roles`
);
return data.roles;
};
export const useGetProjectRoles = (projectId: string) =>
export const useGetProjectRoles = (projectSlug: string) =>
useQuery({
queryKey: roleQueryKeys.getProjectRoles(projectId),
queryFn: () => getProjectRoles(projectId),
enabled: Boolean(projectId)
queryKey: roleQueryKeys.getProjectRoles(projectSlug),
queryFn: () => getProjectRoles(projectSlug),
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) => {

View File

@@ -71,7 +71,7 @@ export type TDeleteOrgRoleDTO = {
};
export type TCreateProjectRoleDTO = {
projectId: string;
projectSlug: string;
name: string;
description?: string;
slug: string;
@@ -79,11 +79,11 @@ export type TCreateProjectRoleDTO = {
};
export type TUpdateProjectRoleDTO = {
projectId: string;
projectSlug: string;
id: string;
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
export type TDeleteProjectRoleDTO = {
projectId: string;
projectSlug: string;
id: string;
};

View File

@@ -5,25 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Modal,
ModalContent,
Select,
SelectItem} from "@app/components/v2";
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useAddGroupToWorkspace,
useGetOrganizationGroups,
useGetProjectRoles,
useListWorkspaceGroups,
import {
useAddGroupToWorkspace,
useGetOrganizationGroups,
useGetProjectRoles,
useListWorkspaceGroups
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
slug: z.string(),
role: z.string()
slug: z.string(),
role: z.string()
});
export type FormData = z.infer<typeof schema>;
@@ -33,150 +27,146 @@ type Props = {
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
};
export const GroupModal = ({
popUp,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
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();
const orgId = currentOrg?.id || "";
const projectSlug = currentWorkspace?.slug || "";
groupMemberships?.forEach((groupMembership) => {
wsGroupIds.set(groupMembership.group.id, true);
});
const { data: groups } = useGetOrganizationGroups(orgId);
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
return (groups || []).filter(({ id }) => !wsGroupIds.has(id));
}, [groups, groupMemberships]);
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
const { data: roles } = useGetProjectRoles(projectSlug);
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
const filteredGroupMembershipOrgs = useMemo(() => {
const wsGroupIds = new Map();
groupMemberships?.forEach((groupMembership) => {
wsGroupIds.set(groupMembership.group.id, true);
});
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) => {
try {
await addGroupToWorkspaceMutateAsync({
projectSlug: currentWorkspace?.slug || "",
groupSlug: slug,
role: role || undefined
});
reset();
handlePopUpToggle("group", false);
createNotification({
text: "Successfully added group to project",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed to add group to project",
type: "error"
});
}
reset();
handlePopUpToggle("group", false);
createNotification({
text: "Successfully added group to project",
type: "success"
});
} catch (err) {
createNotification({
text: "Failed to add group to project",
type: "error"
});
}
return (
<Modal
isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen);
reset();
}}
>
<ModalContent title="Add Group to Project">
{filteredGroupMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="slug"
defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
<SelectItem value={slug} key={`org-group-${id}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Role"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.group?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div className="text-sm">
All groups in your organization have already been added to this project.
</div>
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
<Button variant="outline_bg">Create a new group</Button>
</Link>
</div>
)}
</ModalContent>
</Modal>
);
}
};
return (
<Modal
isOpen={popUp?.group?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("group", isOpen);
reset();
}}
>
<ModalContent title="Add Group to Project">
{filteredGroupMembershipOrgs.length ? (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="slug"
defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
<SelectItem value={slug} key={`org-group-${id}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="role"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Role"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(roles || []).map(({ name, slug }) => (
<SelectItem value={slug} key={`st-role-${slug}`}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.group?.data ? "Update" : "Create"}
</Button>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</div>
</form>
) : (
<div className="flex flex-col space-y-4">
<div className="text-sm">
All groups in your organization have already been added to this project.
</div>
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
<Button variant="outline_bg">Create a new group</Button>
</Link>
</div>
)}
</ModalContent>
</Modal>
);
};

View File

@@ -201,11 +201,7 @@ export type TMemberRolesProp = {
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
export const GroupRoles = ({
roles = [],
disableEdit = false,
groupSlug
}: TMemberRolesProp) => {
export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemberRolesProp) => {
const { currentWorkspace } = useWorkspace();
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
const [searchRoles, setSearchRoles] = useState("");
@@ -220,9 +216,9 @@ export const GroupRoles = ({
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 updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
@@ -317,7 +313,7 @@ export const GroupRoles = ({
icon={faClock}
className={twMerge(
new Date() > new Date(temporaryAccessEndTime as string) &&
"text-red-600"
"text-red-600"
)}
/>
</Tooltip>
@@ -390,14 +386,14 @@ export const GroupRoles = ({
defaultValue={
userProjectRoleDetails?.isTemporary
? {
isTemporary: true,
temporaryAccessStartTime:
userProjectRoleDetails.temporaryAccessStartTime as string,
temporaryRange:
userProjectRoleDetails.temporaryRange as string,
temporaryAccessEndTime:
userProjectRoleDetails.temporaryAccessEndTime
}
isTemporary: true,
temporaryAccessStartTime:
userProjectRoleDetails.temporaryAccessStartTime as string,
temporaryRange:
userProjectRoleDetails.temporaryRange as string,
temporaryAccessEndTime:
userProjectRoleDetails.temporaryAccessEndTime
}
: false
}
render={({ field }) => (

View File

@@ -30,17 +30,17 @@ type Props = {
};
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const { currentWorkspace } = useWorkspace();
const orgId = currentOrg?.id || "";
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
const { data: roles } = useGetProjectRoles(workspaceId);
const { data: roles } = useGetProjectRoles(projectSlug);
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();

View File

@@ -65,7 +65,8 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
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 isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit,
@@ -79,14 +80,14 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary
? {
isTemporary: true,
temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime
}
isTemporary: true,
temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime
}
: {
isTemporary: dto.isTemporary
}
isTemporary: dto.isTemporary
}
}))
}
});
@@ -191,9 +192,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
? isExpired
? "Timed Access Expired"
: `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss"
)}`
new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss"
)}`
: "Non expiry access"
}
>
@@ -212,9 +213,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
? isExpired
? "Access Expired"
: formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date()
)
new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date()
)
: "Permanent"}
</Button>
</Tooltip>
@@ -338,7 +339,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
type="submit"
className={twMerge(
"transition-all",
"opacity-0 cursor-default",
"cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100"
)}
isDisabled={!roleForm.formState.isDirty}

View File

@@ -131,20 +131,17 @@ const SpecificPrivilegeSecretForm = ({
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
{ 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({
privilegeDetails: {
...data.temporaryAccess,
permissions: actions
.filter(({ allowed }) => allowed)
.map(({ action }) => ({
action,
subject: ProjectPermissionSub.Secrets,
conditions
}))
privilegePermission: {
actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action),
subject: ProjectPermissionSub.Secrets,
conditions: {
environment: data.environmentSlug,
...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {})
}
}
},
privilegeSlug: privilege.slug,
identityId,
@@ -474,15 +471,13 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
if (createIdentityPrivilege.isLoading) return;
try {
await createIdentityPrivilege.mutateAsync({
permissions: [
{
action: ProjectPermissionActions.Read,
subject: ProjectPermissionSub.Secrets,
conditions: {
environment: currentWorkspace?.environments?.[0].slug
}
privilegePermission: {
actions: [ProjectPermissionActions.Read],
subject: ProjectPermissionSub.Secrets,
conditions: {
environment: currentWorkspace?.environments?.[0].slug as string
}
],
},
identityId,
projectSlug
});

View File

@@ -65,7 +65,8 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace();
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 isMemberEditDisabled = permission.cannot(
ProjectPermissionActions.Edit,
@@ -79,14 +80,14 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
slug: customRoleSlug || role,
temporaryAccess: dto.isTemporary
? {
isTemporary: true,
temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime
}
isTemporary: true,
temporaryRange: dto.temporaryRange,
temporaryAccessEndTime: dto.temporaryAccessEndTime,
temporaryAccessStartTime: dto.temporaryAccessStartTime
}
: {
isTemporary: dto.isTemporary
}
isTemporary: dto.isTemporary
}
}))
}
});
@@ -191,9 +192,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
? isExpired
? "Timed Access Expired"
: `Until ${format(
new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss"
)}`
new Date(temporaryAccess.temporaryAccessEndTime || ""),
"yyyy-MM-dd HH:mm:ss"
)}`
: "Non expiry access"
}
>
@@ -212,9 +213,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
? isExpired
? "Access Expired"
: formatDistance(
new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date()
)
new Date(temporaryAccess.temporaryAccessEndTime || ""),
new Date()
)
: "Permanent"}
</Button>
</Tooltip>
@@ -335,7 +336,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
type="submit"
className={twMerge(
"transition-all",
"opacity-0 cursor-default",
"cursor-default opacity-0",
roleForm.formState.isDirty && "cursor-pointer opacity-100"
)}
isDisabled={!roleForm.formState.isDirty}

View File

@@ -3,7 +3,6 @@ import { motion } from "framer-motion";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { withProjectPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectRoleList } from "./components/ProjectRoleList";
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
@@ -21,7 +20,7 @@ export const ProjectRoleListTab = withProjectPermission(
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectRoleModifySection
role={popUp.editRole.data as TProjectRole}
roleSlug={popUp.editRole.data as string}
onGoBack={() => handlePopUpClose("editRole")}
/>
</motion.div>
@@ -33,7 +32,7 @@ export const ProjectRoleListTab = withProjectPermission(
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -30 }}
>
<ProjectRoleList onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
<ProjectRoleList onSelectRole={(slug) => handlePopUpOpen("editRole", slug)} />
</motion.div>
);
},

View File

@@ -24,7 +24,7 @@ import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
type Props = {
onSelectRole: (role?: TProjectRole) => void;
onSelectRole: (slug?: string) => void;
};
export const ProjectRoleList = ({ onSelectRole }: Props) => {
@@ -32,10 +32,9 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
console.log(roles);
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
const { mutateAsync: deleteRole } = useDeleteProjectRole();
@@ -43,7 +42,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
const { id } = popUp?.deleteRole?.data as TProjectRole;
try {
await deleteRole({
projectId: workspaceId,
projectSlug,
id
});
createNotification({ type: "success", text: "Successfully removed the role" });
@@ -109,7 +108,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
<IconButton
isDisabled={!isAllowed}
ariaLabel="edit"
onClick={() => onSelectRole(role)}
onClick={() => onSelectRole(role.slug)}
variant="plain"
>
<FontAwesomeIcon icon={faEdit} />
@@ -146,9 +145,8 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
</div>
<DeleteActionModal
isOpen={popUp.deleteRole.isOpen}
title={`Are you sure want to delete ${
(popUp?.deleteRole?.data as TProjectRole)?.name || " "
} role?`}
title={`Are you sure want to delete ${(popUp?.deleteRole?.data as TProjectRole)?.name || " "
} role?`}
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
onClose={() => handlePopUpClose("deleteRole")}
onDeleteApproved={handleRoleDelete}

View File

@@ -19,9 +19,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
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 { useCreateProjectRole, useUpdateProjectRole } from "@app/hooks/api";
import {
useCreateProjectRole,
useGetProjectRoleBySlug,
useUpdateProjectRole
} from "@app/hooks/api";
import { TProjectRole } from "@app/hooks/api/roles/types";
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
@@ -117,17 +121,20 @@ const SINGLE_PERMISSION_LIST = [
] as const;
type Props = {
role?: TProjectRole;
roleSlug?: string;
onGoBack: VoidFunction;
};
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(role?.slug || "");
const isNewRole = !role?.slug;
export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
const isNewRole = !roleSlug;
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?.id || "";
const projectSlug = currentWorkspace?.slug || "";
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
currentWorkspace?.slug || "",
roleSlug as string
);
const {
handleSubmit,
@@ -137,19 +144,21 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
getValues,
control
} = useForm<TFormSchema>({
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
values: roleDetails
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
: ({} as TProjectRole),
resolver: zodResolver(formSchema)
});
const { mutateAsync: createRole } = useCreateProjectRole();
const { mutateAsync: updateRole } = useUpdateProjectRole();
const handleRoleUpdate = async (el: TFormSchema) => {
if (!role?.id) return;
if (!roleDetails?.id) return;
try {
await updateRole({
id: role?.id,
projectId: workspaceId,
id: roleDetails?.id as string,
projectSlug,
...el,
permissions: formRolePermission2API(el.permissions)
});
@@ -169,7 +178,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
try {
await createRole({
projectId: workspaceId,
projectSlug,
...el,
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 (
<div>
<form onSubmit={handleSubmit(handleFormSubmit)}>

View File

@@ -95,10 +95,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
const formVal: Record<string, any> = {};
permissions.forEach((permission) => {
const {
subject: [subject],
action
} = permission;
const { subject: caslSub, action } = permission;
const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
if (!formVal?.[subject]) formVal[subject] = {};
if (subject === "secrets") {
@@ -123,7 +121,7 @@ const multiEnvForm2Api = (
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
// if any of them is set in all push it without any condition
PERMISSION_ACTIONS.forEach((action) => {
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] });
if (formVal?.all?.[action]) permissions.push({ action, subject });
});
if (!isFullAccess) {
@@ -144,7 +142,7 @@ const multiEnvForm2Api = (
if (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 {
Object.entries(actions).forEach(([action, isAllowed]) => {
if (isAllowed) {
permissions.push({ subject: [rule], action });
permissions.push({ subject: rule, action });
}
});
}