feat(rbac): implemented project based permission loading and role management

This commit is contained in:
Akhil Mohan
2023-09-08 21:22:36 +05:30
parent aac3168c80
commit 520a553ea1
63 changed files with 2237 additions and 488 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
export { useCreateRole, useDeleteRole, useUpdateRole } from "./mutation";
export { useGetRoles, useGetUserOrgPermissions } from "./queries";
export { useGetRoles, useGetUserOrgPermissions,useGetUserProjectPermissions } from "./queries";
+4 -4
View File
@@ -5,22 +5,22 @@ import { apiRequest } from "@app/config/request";
import { roleQueryKeys } from "./queries";
import { TCreateRoleDTO, TDeleteRoleDTO, TUpdateRoleDTO } from "./types";
export const useCreateRole = () => {
export const useCreateRole = <T extends string | undefined>() => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: TCreateRoleDTO) => apiRequest.post("/api/v1/roles", dto),
mutationFn: (dto: TCreateRoleDTO<T>) => apiRequest.post("/api/v1/roles", dto),
onSuccess: (_, { orgId, workspaceId }) => {
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
}
});
};
export const useUpdateRole = () => {
export const useUpdateRole = <T extends string | undefined>() => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, ...dto }: TUpdateRoleDTO) => apiRequest.patch(`/api/v1/roles/${id}`, dto),
mutationFn: ({ id, ...dto }: TUpdateRoleDTO<T>) => apiRequest.patch(`/api/v1/roles/${id}`, dto),
onSuccess: (_, { orgId, workspaceId }) => {
queryClient.invalidateQueries(roleQueryKeys.getRoles({ orgId, workspaceId }));
}
+39 -8
View File
@@ -4,22 +4,33 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { OrgPermissionSet } from "@app/context/OrgPermissionContext/types";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext/types";
import { TGetRolesDTO, TGetUserOrgPermissionsDTO, TRole } from "./types";
import {
TGetRolesDTO,
TGetUserOrgPermissionsDTO,
TGetUserProjectPermissionDTO,
TRole
} from "./types";
export const roleQueryKeys = {
getRoles: ({ orgId, workspaceId }: TGetRolesDTO) => ["roles", { orgId, workspaceId }] as const,
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
["user-permissions", { orgId }] as const
["user-permissions", { orgId }] as const,
getUserProjectPermissions: ({ workspaceId }: TGetUserProjectPermissionDTO) =>
["user-project-permissions", { workspaceId }] as const
};
const getRoles = async ({ orgId, workspaceId }: TGetRolesDTO) => {
const { data } = await apiRequest.get<{ data: { roles: TRole[] } }>("/api/v1/roles", {
params: {
workspaceId,
orgId
const { data } = await apiRequest.get<{ data: { roles: TRole<typeof workspaceId>[] } }>(
"/api/v1/roles",
{
params: {
workspaceId,
orgId
}
}
});
);
return data.data.roles;
};
@@ -34,7 +45,7 @@ export const useGetRoles = ({ orgId, workspaceId }: TGetRolesDTO) =>
const getUserOrgPermissions = async ({ orgId }: TGetUserOrgPermissionsDTO) => {
const { data } = await apiRequest.get<{
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
}>(`/api/v1/roles/${orgId}/permissions`, {});
}>(`/api/v1/roles/organization/${orgId}/permissions`, {});
return data.data.permissions;
};
@@ -50,3 +61,23 @@ export const useGetUserOrgPermissions = ({ orgId }: TGetUserOrgPermissionsDTO) =
return ability;
}
});
const getUserProjectPermissions = async ({ workspaceId }: TGetUserProjectPermissionDTO) => {
const { data } = await apiRequest.get<{
data: { permissions: PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[] };
}>(`/api/v1/roles/workspace/${workspaceId}/permissions`, {});
return data.data.permissions;
};
export const useGetUserProjectPermissions = ({ workspaceId }: TGetUserProjectPermissionDTO) =>
useQuery({
queryKey: roleQueryKeys.getUserProjectPermissions({ workspaceId }),
queryFn: () => getUserProjectPermissions({ workspaceId }),
enabled: Boolean(workspaceId),
select: (data) => {
const rule = unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(data);
const ability = createMongoAbility<ProjectPermissionSet>(rule);
return ability;
}
});
+35 -9
View File
@@ -3,14 +3,14 @@ export type TGetRolesDTO = {
workspaceId?: string;
};
export type TRole = {
export type TRole<T extends string | undefined> = {
_id: string;
organization: string;
workspace: string;
workspace: T;
name: string;
description: string;
slug: string;
permissions: TPermission[];
permissions: T extends string ? TProjectPermission[] : TPermission[];
createdAt: string;
updatedAt: string;
};
@@ -29,20 +29,42 @@ type TWorkspacePermission = {
subject: "workspace";
};
export type TCreateRoleDTO = {
export type TProjectPermission = TProjectGeneralPermission | TProjectWorkspacePermission;
type TProjectGeneralPermission = {
condition?: Record<string, any>;
action: "read" | "edit" | "create" | "delete";
subject:
| "member"
| "role"
| "settings"
| "secrets"
| "environments"
| "folders"
| "secret-imports"
| "service-tokens";
};
type TProjectWorkspacePermission = {
condition?: Record<string, any>;
action: "delete" | "edit";
subject: "workspace";
};
export type TCreateRoleDTO<T extends string | undefined> = {
orgId: string;
workspaceId?: string;
workspaceId?: T;
name: string;
description?: string;
slug: string;
permissions: TPermission[];
permissions: T extends string ? TProjectPermission[] : TPermission[];
};
export type TUpdateRoleDTO = {
export type TUpdateRoleDTO<T extends string | undefined> = {
orgId: string;
id: string;
workspaceId?: string;
} & Partial<Omit<TCreateRoleDTO, "orgId" | "workspaceId">>;
workspaceId?: T;
} & Partial<Omit<TCreateRoleDTO<T>, "orgId" | "workspaceId">>;
export type TDeleteRoleDTO = {
orgId: string;
@@ -53,3 +75,7 @@ export type TDeleteRoleDTO = {
export type TGetUserOrgPermissionsDTO = {
orgId: string;
};
export type TGetUserProjectPermissionDTO = {
workspaceId: string;
};
+56 -73
View File
@@ -8,6 +8,7 @@ import { apiRequest } from "@app/config/request";
import { setAuthToken } from "@app/reactQuery";
import { useUploadWsKey } from "../keys/queries";
import { workspaceKeys } from "../workspace/queries";
import {
AddUserToOrgDTO,
AddUserToWsDTO,
@@ -55,27 +56,27 @@ export const useRenameUser = () => {
return useMutation<{}, {}, RenameUserDTO>({
mutationFn: ({ newName }) =>
apiRequest.patch("/api/v2/users/me/name", { firstName: newName?.split(" ")[0], lastName: newName?.split(" ").slice(1).join(" ") }),
apiRequest.patch("/api/v2/users/me/name", {
firstName: newName?.split(" ")[0],
lastName: newName?.split(" ").slice(1).join(" ")
}),
onSuccess: () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
};
export const useUpdateUserAuthMethods = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
authMethods
}: {
authMethods: AuthMethod[];
}) => {
const { data: { user } } = await apiRequest.put("/api/v2/users/me/auth-methods", {
mutationFn: async ({ authMethods }: { authMethods: AuthMethod[] }) => {
const {
data: { user }
} = await apiRequest.put("/api/v2/users/me/auth-methods", {
authMethods
});
return user;
},
onSuccess: () => {
@@ -108,6 +109,7 @@ export const useGetOrgUsers = (orgId: string) =>
// mutation
export const useAddUserToWs = () => {
const uploadWsKey = useUploadWsKey();
const queryClient = useQueryClient();
return useMutation<{ data: AddUserToWsRes }, {}, AddUserToWsDTO>({
mutationFn: ({ email, workspaceId }) =>
@@ -136,18 +138,20 @@ export const useAddUserToWs = () => {
userId: data.invitee._id,
workspaceId
});
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(workspaceId));
}
});
};
export const useAddUserToOrg = () => {
const queryClient = useQueryClient();
type Response = {
type Response = {
data: {
message: string,
completeInviteLink: string | undefined
}
}
message: string;
completeInviteLink: string | undefined;
};
};
return useMutation<Response, {}, AddUserToOrgDTO>({
mutationFn: (dto) => {
@@ -164,7 +168,7 @@ export const useDeleteOrgMembership = () => {
return useMutation<{}, {}, DeletOrgMembershipDTO>({
mutationFn: ({ membershipId, orgId }) => {
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`)
return apiRequest.delete(`/api/v2/organizations/${orgId}/memberships/${membershipId}`);
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(orgId));
@@ -177,9 +181,12 @@ export const useUpdateOrgUserRole = () => {
return useMutation<{}, {}, UpdateOrgUserRoleDTO>({
mutationFn: ({ organizationId, membershipId, role }) => {
return apiRequest.patch(`/api/v2/organizations/${organizationId}/memberships/${membershipId}`, {
role
});
return apiRequest.patch(
`/api/v2/organizations/${organizationId}/memberships/${membershipId}`,
{
role
}
);
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries(userKeys.getOrgUsers(organizationId));
@@ -218,64 +225,49 @@ export const useLogoutUser = () =>
});
export const useGetMyIp = () => {
return useQuery({
return useQuery({
queryKey: userKeys.myIp,
queryFn: async () => {
const { data } = await apiRequest.get<{ ip: string; }>(
"/api/v1/users/me/ip"
);
const { data } = await apiRequest.get<{ ip: string }>("/api/v1/users/me/ip");
return data.ip;
},
enabled: true
});
}
});
};
export const useGetMyAPIKeys = () => {
return useQuery({
queryKey: userKeys.myAPIKeys,
queryFn: async () => {
const { data } = await apiRequest.get<APIKeyData[]>(
"/api/v2/users/me/api-keys"
);
const { data } = await apiRequest.get<APIKeyData[]>("/api/v2/users/me/api-keys");
return data;
},
enabled: true
});
}
};
export const useCreateAPIKey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
name,
expiresIn
}: {
name: string;
expiresIn: number;
}) => {
const { data } = await apiRequest.post<CreateAPIKeyRes>(
"/api/v2/users/me/api-keys",
{
name,
expiresIn
}
);
mutationFn: async ({ name, expiresIn }: { name: string; expiresIn: number }) => {
const { data } = await apiRequest.post<CreateAPIKeyRes>("/api/v2/users/me/api-keys", {
name,
expiresIn
});
return data;
},
onSuccess() {
queryClient.invalidateQueries(userKeys.myAPIKeys);
}
});
}
};
export const useDeleteAPIKey = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (apiKeyDataId: string) => {
const { data } = await apiRequest.delete(
`/api/v2/users/me/api-keys/${apiKeyDataId}`
);
const { data } = await apiRequest.delete(`/api/v2/users/me/api-keys/${apiKeyDataId}`);
return data;
},
@@ -283,29 +275,25 @@ export const useDeleteAPIKey = () => {
queryClient.invalidateQueries(userKeys.myAPIKeys);
}
});
}
};
export const useGetMySessions = () => {
return useQuery({
queryKey: userKeys.mySessions,
queryFn: async () => {
const { data } = await apiRequest.get<TokenVersion[]>(
"/api/v2/users/me/sessions"
);
const { data } = await apiRequest.get<TokenVersion[]>("/api/v2/users/me/sessions");
return data;
},
enabled: true
});
}
};
export const useRevokeMySessions = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.delete(
"/api/v2/users/me/sessions"
);
const { data } = await apiRequest.delete("/api/v2/users/me/sessions");
return data;
},
@@ -313,22 +301,17 @@ export const useRevokeMySessions = () => {
queryClient.invalidateQueries(userKeys.mySessions);
}
});
}
};
export const useUpdateMfaEnabled = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
isMfaEnabled
}: {
isMfaEnabled: boolean;
}) => {
const { data: { user } } = await apiRequest.patch(
"/api/v2/users/me/mfa",
{
isMfaEnabled
}
);
mutationFn: async ({ isMfaEnabled }: { isMfaEnabled: boolean }) => {
const {
data: { user }
} = await apiRequest.patch("/api/v2/users/me/mfa", {
isMfaEnabled
});
return user;
},
@@ -336,15 +319,15 @@ export const useUpdateMfaEnabled = () => {
queryClient.invalidateQueries(userKeys.getUser);
}
});
}
};
export const fetchMyOrganizationProjects = async (orgId: string) => {
const { data: { workspaces } } = await apiRequest.get(
`/api/v1/organization/${orgId}/my-workspaces`
);
const {
data: { workspaces }
} = await apiRequest.get(`/api/v1/organization/${orgId}/my-workspaces`);
return workspaces;
}
};
export const useGetMyOrganizationProjects = (orgId: string) => {
return useQuery({
@@ -354,4 +337,4 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
},
enabled: true
});
}
};
+7 -5
View File
@@ -1,12 +1,12 @@
import { UserWsKeyPair } from "../keys/types";
export enum AuthMethod {
EMAIL = "email",
GOOGLE = "google",
EMAIL = "email",
GOOGLE = "google",
GITHUB = "github",
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml"
}
export type User = {
@@ -48,6 +48,8 @@ export type OrgUser = {
customRole: string;
};
export type TWorkspaceUser = OrgUser;
export type AddUserToWsDTO = {
workspaceId: string;
email: string;
+25 -27
View File
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types";
import { EncryptedSecret } from "../secrets/types";
import { TWorkspaceUser } from "../users/types";
import {
CreateEnvironmentDTO,
CreateWorkspaceDTO,
@@ -173,16 +174,17 @@ export const createWorkspace = ({
workspaceName
}: CreateWorkspaceDTO): Promise<{ data: { workspace: Workspace } }> => {
return apiRequest.post("/api/v1/workspace", { workspaceName, organizationId });
}
};
export const useCreateWorkspace = () => {
const queryClient = useQueryClient();
return useMutation<{ data: { workspace: Workspace } }, {}, CreateWorkspaceDTO>({
mutationFn: async ({ organizationId, workspaceName }) => createWorkspace({
organizationId,
workspaceName
}),
mutationFn: async ({ organizationId, workspaceName }) =>
createWorkspace({
organizationId,
workspaceName
}),
onSuccess: () => {
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
}
@@ -296,32 +298,30 @@ export const useGetWorkspaceUsers = (workspaceId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceUsers(workspaceId),
queryFn: async () => {
const { data: { users } } = await apiRequest.get(
const {
data: { users }
} = await apiRequest.get<{ users: TWorkspaceUser[] }>(
`/api/v1/workspace/${workspaceId}/users`
);
return users;
},
enabled: true
});
}
};
export const useAddUserToWorkspace = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
email,
workspaceId
}: {
email: string;
workspaceId: string;
}) => {
const { data: { invitee, latestKey } } = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
return ({
mutationFn: async ({ email, workspaceId }: { email: string; workspaceId: string }) => {
const {
data: { invitee, latestKey }
} = await apiRequest.post(`/api/v1/workspace/${workspaceId}/invite-signup`, { email });
return {
invitee,
latestKey
});
};
},
onSuccess: (_, dto) => {
queryClient.invalidateQueries(workspaceKeys.getWorkspaceUsers(dto.workspaceId));
@@ -334,7 +334,9 @@ export const useDeleteUserFromWorkspace = () => {
return useMutation({
mutationFn: async (membershipId: string) => {
const { data: { deletedMembership } } = await apiRequest.delete(`/api/v1/membership/${membershipId}`);
const {
data: { deletedMembership }
} = await apiRequest.delete(`/api/v1/membership/${membershipId}`);
return deletedMembership;
},
onSuccess: (res) => {
@@ -346,14 +348,10 @@ export const useDeleteUserFromWorkspace = () => {
export const useUpdateUserWorkspaceRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
membershipId,
role
}: {
membershipId: string;
role: string;
}) => {
const { data: { membership } } = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, {
mutationFn: async ({ membershipId, role }: { membershipId: string; role: string }) => {
const {
data: { membership }
} = await apiRequest.post(`/api/v1/membership/${membershipId}/change-role`, {
role
});
return membership;