misc: setup prerequisites for terraform project group

This commit is contained in:
Sheen Capadngan
2024-09-16 02:12:24 +08:00
parent a6f4a95821
commit dbb8617180
4 changed files with 299 additions and 31 deletions

View File

@@ -28,14 +28,36 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug), projectSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.projectSlug),
groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug) groupSlug: z.string().trim().describe(PROJECTS.ADD_GROUP_TO_PROJECT.groupSlug)
}), }),
body: z.object({ body: z
role: z .object({
.string() role: z
.trim() .string()
.min(1) .trim()
.default(ProjectMembershipRole.NoAccess) .min(1)
.describe(PROJECTS.ADD_GROUP_TO_PROJECT.role) .default(ProjectMembershipRole.NoAccess)
}), .describe(PROJECTS.ADD_GROUP_TO_PROJECT.role),
roles: z
.array(
z.union([
z.object({
role: z.string(),
isTemporary: z.literal(false).default(false)
}),
z.object({
role: z.string(),
isTemporary: z.literal(true),
temporaryMode: z.nativeEnum(ProjectUserMembershipTemporaryMode),
temporaryRange: z.string().refine((val) => ms(val) > 0, "Temporary range must be a positive number"),
temporaryAccessStartTime: z.string().datetime()
})
])
)
.optional()
})
.refine((data) => data.role || data.roles, {
message: "Either role or roles must be present",
path: ["role", "roles"]
}),
response: { response: {
200: z.object({ 200: z.object({
groupMembership: GroupProjectMembershipsSchema groupMembership: GroupProjectMembershipsSchema
@@ -50,8 +72,9 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
groupSlug: req.params.groupSlug, groupSlug: req.params.groupSlug,
projectSlug: req.params.projectSlug, projectSlug: req.params.projectSlug,
role: req.body.role roles: req.body.roles || [{ role: req.body.role }]
}); });
return { groupMembership }; return { groupMembership };
} }
}); });
@@ -198,4 +221,58 @@ export const registerGroupProjectRouter = async (server: FastifyZodProvider) =>
return { groupMemberships }; return { groupMemberships };
} }
}); });
server.route({
method: "GET",
url: "/:projectSlug/groups/:groupSlug",
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Return project group",
security: [
{
bearerAuth: []
}
],
params: z.object({
projectSlug: z.string().trim(),
groupSlug: z.string().trim()
}),
response: {
200: z.object({
groupMembership: z.object({
id: z.string(),
groupId: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
roles: z.array(
z.object({
id: z.string(),
role: z.string(),
customRoleId: z.string().optional().nullable(),
customRoleName: z.string().optional().nullable(),
customRoleSlug: z.string().optional().nullable(),
isTemporary: z.boolean(),
temporaryMode: z.string().optional().nullable(),
temporaryRange: z.string().nullable().optional(),
temporaryAccessStartTime: z.date().nullable().optional(),
temporaryAccessEndTime: z.date().nullable().optional()
})
),
group: GroupsSchema.pick({ name: true, id: true, slug: true })
})
})
}
},
handler: async (req) => {
const groupMembership = await server.services.groupProject.getGroupInProject({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.params
});
return { groupMembership };
}
});
}; };

View File

@@ -10,6 +10,109 @@ export type TGroupProjectDALFactory = ReturnType<typeof groupProjectDALFactory>;
export const groupProjectDALFactory = (db: TDbClient) => { export const groupProjectDALFactory = (db: TDbClient) => {
const groupProjectOrm = ormify(db, TableName.GroupProjectMembership); const groupProjectOrm = ormify(db, TableName.GroupProjectMembership);
const findByGroupSlugAndProject = async (
{ groupSlug, projectId }: { groupSlug: string; projectId: string },
tx?: Knex
) => {
try {
// this is a workaround so that the order of group roles is preserved as a necessary
// requirement for our terraform provider
/* Approach:
1. First we query the group project membership along with the attached roles
2. Then we have a separate query to fetch the custom role details which we manually augment below
*/
const rawGroupResult = await (tx || db.replicaNode())(TableName.GroupProjectMembership)
.where(`${TableName.GroupProjectMembership}.projectId`, projectId)
.where(`${TableName.Groups}.slug`, "=", groupSlug)
.join(TableName.Groups, `${TableName.GroupProjectMembership}.groupId`, `${TableName.Groups}.id`)
.join(
TableName.GroupProjectMembershipRole,
`${TableName.GroupProjectMembershipRole}.projectMembershipId`,
`${TableName.GroupProjectMembership}.id`
)
.select(
db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("createdAt").withSchema(TableName.GroupProjectMembership),
db.ref("updatedAt").withSchema(TableName.GroupProjectMembership),
db.ref("id").as("groupId").withSchema(TableName.Groups),
db.ref("name").as("groupName").withSchema(TableName.Groups),
db.ref("slug").as("groupSlug").withSchema(TableName.Groups),
db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("role").withSchema(TableName.GroupProjectMembershipRole),
db.ref("id").withSchema(TableName.GroupProjectMembershipRole).as("membershipRoleId"),
db.ref("customRoleId").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryMode").withSchema(TableName.GroupProjectMembershipRole),
db.ref("isTemporary").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole)
);
// IMPORTANT NOTE: we do this separately because this breaks the order of the group project roles
const customRoleDetails = await (tx || db.replicaNode())(TableName.ProjectRoles)
.whereIn("id", rawGroupResult.map((entry) => entry.customRoleId) as string[])
.select("id", "slug", "name");
const groupProjectMembershipRoleToCustomRole: Record<string, { name: string; slug: string }> = {};
customRoleDetails.forEach(
// eslint-disable-next-line no-return-assign
(entry) =>
(groupProjectMembershipRoleToCustomRole[entry.id] = {
name: entry.name,
slug: entry.slug
})
);
const members = sqlNestRelationships({
data: rawGroupResult,
parentMapper: ({ groupId, groupName, id, createdAt, updatedAt }) => ({
id,
groupId,
createdAt,
updatedAt,
group: {
id: groupId,
name: groupName,
slug: groupSlug
}
}),
key: "id",
childrenMapper: [
{
label: "roles" as const,
key: "membershipRoleId",
mapper: ({
role,
customRoleId,
membershipRoleId,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
}) => ({
id: membershipRoleId,
role,
customRoleId,
customRoleName: customRoleId && groupProjectMembershipRoleToCustomRole[customRoleId]?.name,
customRoleSlug: customRoleId && groupProjectMembershipRoleToCustomRole[customRoleId]?.slug,
temporaryRange,
temporaryMode,
temporaryAccessEndTime,
temporaryAccessStartTime,
isTemporary
})
}
]
});
return members[0];
} catch (error) {
throw new DatabaseError({ error, name: "FindByGroupSlugAndProject" });
}
};
const findByProjectId = async (projectId: string, tx?: Knex) => { const findByProjectId = async (projectId: string, tx?: Knex) => {
try { try {
const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership) const docs = await (tx || db.replicaNode())(TableName.GroupProjectMembership)
@@ -221,5 +324,5 @@ export const groupProjectDALFactory = (db: TDbClient) => {
return members; return members;
}; };
return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers }; return { ...groupProjectOrm, findByProjectId, findByUserId, findAllProjectGroupMembers, findByGroupSlugAndProject };
}; };

View File

@@ -7,7 +7,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { TGroupDALFactory } from "../../ee/services/group/group-dal"; import { TGroupDALFactory } from "../../ee/services/group/group-dal";
@@ -22,12 +22,16 @@ import { TGroupProjectMembershipRoleDALFactory } from "./group-project-membershi
import { import {
TCreateProjectGroupDTO, TCreateProjectGroupDTO,
TDeleteProjectGroupDTO, TDeleteProjectGroupDTO,
TGetGroupInProjectDTO,
TListProjectGroupDTO, TListProjectGroupDTO,
TUpdateProjectGroupDTO TUpdateProjectGroupDTO
} from "./group-project-types"; } from "./group-project-types";
type TGroupProjectServiceFactoryDep = { type TGroupProjectServiceFactoryDep = {
groupProjectDAL: Pick<TGroupProjectDALFactory, "findOne" | "transaction" | "create" | "delete" | "findByProjectId">; groupProjectDAL: Pick<
TGroupProjectDALFactory,
"findOne" | "transaction" | "create" | "delete" | "findByProjectId" | "findByGroupSlugAndProject"
>;
groupProjectMembershipRoleDAL: Pick< groupProjectMembershipRoleDAL: Pick<
TGroupProjectMembershipRoleDALFactory, TGroupProjectMembershipRoleDALFactory,
"create" | "transaction" | "insertMany" | "delete" "create" | "transaction" | "insertMany" | "delete"
@@ -61,7 +65,7 @@ export const groupProjectServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectSlug, projectSlug,
role roles
}: TCreateProjectGroupDTO) => { }: TCreateProjectGroupDTO) => {
const project = await projectDAL.findOne({ const project = await projectDAL.findOne({
slug: projectSlug slug: projectSlug
@@ -88,16 +92,35 @@ export const groupProjectServiceFactory = ({
message: `Group with slug ${groupSlug} already exists in project with id ${project.id}` message: `Group with slug ${groupSlug} already exists in project with id ${project.id}`
}); });
const { permission: rolePermission, role: customRole } = await permissionService.getProjectPermissionByRole( for await (const { role: requestedRoleChange } of roles) {
role, const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
project.id requestedRoleChange,
project.id
);
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
if (!hasRequiredPriviledges) {
throw new ForbiddenRequestError({ message: "Failed to assign group to a more privileged role" });
}
}
// validate custom roles input
const customInputRoles = roles.filter(
({ role }) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
); );
const hasPrivilege = isAtLeastAsPrivileged(permission, rolePermission); const hasCustomRole = Boolean(customInputRoles.length);
if (!hasPrivilege) const customRoles = hasCustomRole
throw new ForbiddenRequestError({ ? await projectRoleDAL.find({
message: "Failed to add group to project with more privileged role" projectId: project.id,
}); $in: { slug: customInputRoles.map(({ role }) => role) }
const isCustomRole = Boolean(customRole); })
: [];
if (customRoles.length !== customInputRoles.length) {
throw new NotFoundError({ message: "Custom role not found" });
}
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
const projectGroup = await groupProjectDAL.transaction(async (tx) => { const projectGroup = await groupProjectDAL.transaction(async (tx) => {
const groupProjectMembership = await groupProjectDAL.create( const groupProjectMembership = await groupProjectDAL.create(
@@ -108,14 +131,31 @@ export const groupProjectServiceFactory = ({
tx tx
); );
await groupProjectMembershipRoleDAL.create( const sanitizedProjectMembershipRoles = roles.map((inputRole) => {
{ const isCustomRole = Boolean(customRolesGroupBySlug?.[inputRole.role]?.[0]);
if (!inputRole.isTemporary) {
return {
projectMembershipId: groupProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null
};
}
// check cron or relative here later for now its just relative
const relativeTimeInMs = ms(inputRole.temporaryRange);
return {
projectMembershipId: groupProjectMembership.id, projectMembershipId: groupProjectMembership.id,
role: isCustomRole ? ProjectMembershipRole.Custom : role, role: isCustomRole ? ProjectMembershipRole.Custom : inputRole.role,
customRoleId: customRole?.id customRoleId: customRolesGroupBySlug[inputRole.role] ? customRolesGroupBySlug[inputRole.role][0].id : null,
}, isTemporary: true,
tx temporaryMode: ProjectUserMembershipTemporaryMode.Relative,
); temporaryRange: inputRole.temporaryRange,
temporaryAccessStartTime: new Date(inputRole.temporaryAccessStartTime),
temporaryAccessEndTime: new Date(new Date(inputRole.temporaryAccessStartTime).getTime() + relativeTimeInMs)
};
});
await groupProjectMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
// share project key with users in group that have not // share project key with users in group that have not
// individually been added to the project and that are not part of // individually been added to the project and that are not part of
@@ -336,10 +376,44 @@ export const groupProjectServiceFactory = ({
return groupMemberships; return groupMemberships;
}; };
const getGroupInProject = async ({
projectSlug,
actor,
actorId,
actorAuthMethod,
actorOrgId,
groupSlug
}: TGetGroupInProjectDTO) => {
const project = await projectDAL.findOne({
slug: projectSlug
});
if (!project) {
throw new NotFoundError({ message: `Failed to find project with slug ${projectSlug}` });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
project.id,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
const groupMembership = await groupProjectDAL.findByGroupSlugAndProject({
groupSlug,
projectId: project.id
});
return groupMembership;
};
return { return {
addGroupToProject, addGroupToProject,
updateGroupInProject, updateGroupInProject,
removeGroupFromProject, removeGroupFromProject,
listGroupsInProject listGroupsInProject,
getGroupInProject
}; };
}; };

View File

@@ -4,7 +4,19 @@ import { ProjectUserMembershipTemporaryMode } from "../project-membership/projec
export type TCreateProjectGroupDTO = { export type TCreateProjectGroupDTO = {
groupSlug: string; groupSlug: string;
role: string; roles: (
| {
role: string;
isTemporary?: false;
}
| {
role: string;
isTemporary: true;
temporaryMode: ProjectUserMembershipTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}
)[];
} & TProjectSlugPermission; } & TProjectSlugPermission;
export type TUpdateProjectGroupDTO = { export type TUpdateProjectGroupDTO = {
@@ -29,3 +41,5 @@ export type TDeleteProjectGroupDTO = {
} & TProjectSlugPermission; } & TProjectSlugPermission;
export type TListProjectGroupDTO = TProjectSlugPermission; export type TListProjectGroupDTO = TProjectSlugPermission;
export type TGetGroupInProjectDTO = TProjectSlugPermission & { groupSlug: string };