Finish basic CRUD groups

This commit is contained in:
Tuan Dang
2024-03-19 10:20:51 -07:00
parent cd192ee228
commit efc186ae6c
41 changed files with 1066 additions and 11 deletions
+96
View File
@@ -0,0 +1,96 @@
import { z } from "zod";
import { GroupsSchema, OrgMembershipRole } from "@app/db/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerGroupRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
organizationId: z.string().trim(),
name: z.string().trim().min(1),
slug: z.string().trim().min(1),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess) // TODO: add describe
}),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.createGroup({
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.body.organizationId,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return group;
}
});
server.route({
url: "/:currentSlug",
method: "PATCH",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
currentSlug: z.string().trim()
}),
body: z
.object({
name: z.string().trim().min(1),
slug: z.string().trim().min(1),
role: z.string().trim().min(1)
})
.partial(),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.updateGroup({
currentSlug: req.params.currentSlug,
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId as string, // note
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body
});
return group;
}
});
server.route({
url: "/:slug",
method: "DELETE",
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
params: z.object({
slug: z.string().trim()
}),
response: {
200: GroupsSchema
}
},
handler: async (req) => {
const group = await server.services.group.deleteGroup({
slug: req.params.slug,
actor: req.permission.type,
actorId: req.permission.id,
orgId: req.permission.orgId as string, // note
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return group;
}
});
};
+2
View File
@@ -1,3 +1,4 @@
import { registerGroupRouter } from "./group-router";
import { registerLdapRouter } from "./ldap-router";
import { registerLicenseRouter } from "./license-router";
import { registerOrgRoleRouter } from "./org-role-router";
@@ -40,4 +41,5 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(registerGroupRouter, { prefix: "/groups" });
};
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => {
const groupOrm = ormify(db, TableName.Groups);
return { ...groupOrm };
};
@@ -0,0 +1,141 @@
import { ForbiddenError } from "@casl/ability";
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGroupDALFactory } from "./group-dal";
import { TCreateGroupDTO, TDeleteGroupDTO, TUpdateGroupDTO } from "./group-types";
type TGroupServiceFactoryDep = {
groupDAL: TGroupDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TGroupServiceFactory = ReturnType<typeof groupServiceFactory>;
export const groupServiceFactory = ({ groupDAL, permissionService, licenseService }: TGroupServiceFactoryDep) => {
const createGroup = async ({
name,
slug,
role,
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId
}: TCreateGroupDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to create group due to plan restriction. Upgrade plan to create group."
});
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
role,
orgId
);
const isCustomRole = Boolean(customRole);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged group" });
const group = await groupDAL.create({
name,
slug,
orgId,
role: isCustomRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
});
return group;
};
const updateGroup = async ({
currentSlug,
name,
slug,
role,
actor,
actorId,
orgId,
actorAuthMethod,
actorOrgId
}: TUpdateGroupDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to update group due to plan restrictio Upgrade plan to update group."
});
const group = await groupDAL.findOne({ orgId, slug: currentSlug });
if (!group) throw new BadRequestError({ message: `Failed to find group with slug ${currentSlug}` });
let customRole: TOrgRoles | undefined;
if (role) {
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
role,
group.orgId
);
const isCustomRole = Boolean(customOrgRole);
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredNewRolePermission)
throw new BadRequestError({ message: "Failed to create a more privileged group" });
if (isCustomRole) customRole = customOrgRole;
}
const [updatedGroup] = await groupDAL.update(
{
orgId,
slug: currentSlug
},
{
name,
slug,
...(role
? {
role: customRole ? OrgMembershipRole.Custom : role,
roleId: customRole?.id
}
: {})
}
);
return updatedGroup;
};
const deleteGroup = async ({ slug, actor, actorId, orgId, actorAuthMethod, actorOrgId }: TDeleteGroupDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
const plan = await licenseService.getPlan(orgId);
if (!plan.groups)
throw new BadRequestError({
message: "Failed to delete group due to plan restriction. Upgrade plan to delete group."
});
const [group] = await groupDAL.delete({
orgId,
slug
});
return group;
};
return {
createGroup,
updateGroup,
deleteGroup
};
};
@@ -0,0 +1,20 @@
import { TOrgPermission } from "@app/lib/types";
export type TCreateGroupDTO = {
name: string;
slug: string;
role: string;
} & TOrgPermission;
export type TUpdateGroupDTO = {
currentSlug: string;
} & Partial<{
name: string;
slug: string;
role: string;
}> &
TOrgPermission;
export type TDeleteGroupDTO = {
slug: string;
} & TOrgPermission;
@@ -20,6 +20,7 @@ export const getDefaultOnPremFeatures = () => {
samlSSO: false,
scim: false,
ldap: false,
groups: true,
status: null,
trial_end: null,
has_used_trial: true,
@@ -26,6 +26,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
samlSSO: false,
scim: false,
ldap: false,
groups: true,
status: null,
trial_end: null,
has_used_trial: true,
@@ -42,6 +42,7 @@ export type TFeatureSet = {
samlSSO: false;
scim: false;
ldap: false;
groups: true;
status: null;
trial_end: null;
has_used_trial: true;
@@ -18,6 +18,7 @@ export enum OrgPermissionSubjects {
Sso = "sso",
Scim = "scim",
Ldap = "ldap",
Groups = "groups",
Billing = "billing",
SecretScanning = "secret-scanning",
Identity = "identity"
@@ -33,6 +34,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
| [OrgPermissionActions, OrgPermissionSubjects.Identity];
@@ -83,6 +85,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);