mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 22:29:15 +00:00
303 lines
9.7 KiB
TypeScript
303 lines
9.7 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
|
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
|
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
|
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
|
import { ProjectTemplates } from "@app/lib/api-docs";
|
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
|
import { slugSchema } from "@app/server/lib/schemas";
|
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
|
import { AuthMode } from "@app/services/auth/auth-type";
|
|
|
|
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
|
|
|
|
const isReservedRoleSlug = (slug: string) =>
|
|
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
|
|
|
const isReservedRoleName = (name: string) =>
|
|
["custom", "admin", "viewer", "developer", "no access"].includes(name.toLowerCase());
|
|
|
|
const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
|
|
roles: z
|
|
.object({
|
|
name: z.string().trim().min(1),
|
|
slug: slugSchema(),
|
|
permissions: UnpackedPermissionSchema.array()
|
|
})
|
|
.array(),
|
|
environments: z
|
|
.object({
|
|
name: z.string().trim().min(1),
|
|
slug: slugSchema(),
|
|
position: z.number().min(1)
|
|
})
|
|
.array()
|
|
});
|
|
|
|
const ProjectTemplateRolesSchema = z
|
|
.object({
|
|
name: z.string().trim().min(1),
|
|
slug: slugSchema(),
|
|
permissions: ProjectPermissionV2Schema.array()
|
|
})
|
|
.array()
|
|
.superRefine((roles, ctx) => {
|
|
if (!roles.length) return;
|
|
|
|
if (Buffer.byteLength(JSON.stringify(roles)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
|
|
|
if (new Set(roles.map((v) => v.slug)).size !== roles.length)
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role slugs must be unique" });
|
|
|
|
if (new Set(roles.map((v) => v.name)).size !== roles.length)
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Role names must be unique" });
|
|
|
|
roles.forEach((role) => {
|
|
if (isReservedRoleSlug(role.slug))
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role slug "${role.slug}" is reserved` });
|
|
|
|
if (isReservedRoleName(role.name))
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Role name "${role.name}" is reserved` });
|
|
});
|
|
});
|
|
|
|
const ProjectTemplateEnvironmentsSchema = z
|
|
.object({
|
|
name: z.string().trim().min(1),
|
|
slug: slugSchema(),
|
|
position: z.number().min(1)
|
|
})
|
|
.array()
|
|
.min(1)
|
|
.superRefine((environments, ctx) => {
|
|
if (Buffer.byteLength(JSON.stringify(environments)) > MAX_JSON_SIZE_LIMIT_IN_BYTES)
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Size limit exceeded" });
|
|
|
|
if (new Set(environments.map((v) => v.name)).size !== environments.length)
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment names must be unique" });
|
|
|
|
if (new Set(environments.map((v) => v.slug)).size !== environments.length)
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Environment slugs must be unique" });
|
|
|
|
if (
|
|
environments.some((env) => env.position < 1 || env.position > environments.length) ||
|
|
new Set(environments.map((env) => env.position)).size !== environments.length
|
|
)
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "One or more of the positions specified is invalid. Positions must be sequential starting from 1."
|
|
});
|
|
});
|
|
|
|
export const registerProjectTemplateRouter = async (server: FastifyZodProvider) => {
|
|
server.route({
|
|
method: "GET",
|
|
url: "/",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
description: "List project templates for the current organization.",
|
|
response: {
|
|
200: z.object({
|
|
projectTemplates: SanitizedProjectTemplateSchema.array()
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
handler: async (req) => {
|
|
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
|
|
|
|
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
orgId: req.permission.orgId,
|
|
event: {
|
|
type: EventType.GET_PROJECT_TEMPLATES,
|
|
metadata: {
|
|
count: auditTemplates.length,
|
|
templateIds: auditTemplates.map((template) => template.id)
|
|
}
|
|
}
|
|
});
|
|
|
|
return { projectTemplates };
|
|
}
|
|
});
|
|
|
|
server.route({
|
|
method: "GET",
|
|
url: "/:templateId",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
description: "Get a project template by ID.",
|
|
params: z.object({
|
|
templateId: z.string().uuid()
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
projectTemplate: SanitizedProjectTemplateSchema
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
handler: async (req) => {
|
|
const projectTemplate = await server.services.projectTemplate.findProjectTemplateById(
|
|
req.params.templateId,
|
|
req.permission
|
|
);
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
orgId: req.permission.orgId,
|
|
event: {
|
|
type: EventType.GET_PROJECT_TEMPLATE,
|
|
metadata: {
|
|
templateId: req.params.templateId
|
|
}
|
|
}
|
|
});
|
|
|
|
return { projectTemplate };
|
|
}
|
|
});
|
|
|
|
server.route({
|
|
method: "POST",
|
|
url: "/",
|
|
config: {
|
|
rateLimit: writeLimit
|
|
},
|
|
schema: {
|
|
description: "Create a project template.",
|
|
body: z.object({
|
|
name: slugSchema({ field: "name" })
|
|
.refine((val) => !isInfisicalProjectTemplate(val), {
|
|
message: `The requested project template name is reserved.`
|
|
})
|
|
.describe(ProjectTemplates.CREATE.name),
|
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
|
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
|
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
|
ProjectTemplates.CREATE.environments
|
|
)
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
projectTemplate: SanitizedProjectTemplateSchema
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
handler: async (req) => {
|
|
const projectTemplate = await server.services.projectTemplate.createProjectTemplate(req.body, req.permission);
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
orgId: req.permission.orgId,
|
|
event: {
|
|
type: EventType.CREATE_PROJECT_TEMPLATE,
|
|
metadata: req.body
|
|
}
|
|
});
|
|
|
|
return { projectTemplate };
|
|
}
|
|
});
|
|
|
|
server.route({
|
|
method: "PATCH",
|
|
url: "/:templateId",
|
|
config: {
|
|
rateLimit: writeLimit
|
|
},
|
|
schema: {
|
|
description: "Update a project template.",
|
|
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
|
|
body: z.object({
|
|
name: slugSchema({ field: "name" })
|
|
.refine((val) => !isInfisicalProjectTemplate(val), {
|
|
message: `The requested project template name is reserved.`
|
|
})
|
|
.optional()
|
|
.describe(ProjectTemplates.UPDATE.name),
|
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.UPDATE.description),
|
|
roles: ProjectTemplateRolesSchema.optional().describe(ProjectTemplates.UPDATE.roles),
|
|
environments: ProjectTemplateEnvironmentsSchema.optional().describe(ProjectTemplates.UPDATE.environments)
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
projectTemplate: SanitizedProjectTemplateSchema
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
handler: async (req) => {
|
|
const projectTemplate = await server.services.projectTemplate.updateProjectTemplateById(
|
|
req.params.templateId,
|
|
req.body,
|
|
req.permission
|
|
);
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
orgId: req.permission.orgId,
|
|
event: {
|
|
type: EventType.UPDATE_PROJECT_TEMPLATE,
|
|
metadata: {
|
|
templateId: req.params.templateId,
|
|
...req.body
|
|
}
|
|
}
|
|
});
|
|
|
|
return { projectTemplate };
|
|
}
|
|
});
|
|
|
|
server.route({
|
|
method: "DELETE",
|
|
url: "/:templateId",
|
|
config: {
|
|
rateLimit: writeLimit
|
|
},
|
|
schema: {
|
|
description: "Delete a project template.",
|
|
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.DELETE.templateId) }),
|
|
|
|
response: {
|
|
200: z.object({
|
|
projectTemplate: SanitizedProjectTemplateSchema
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
handler: async (req) => {
|
|
const projectTemplate = await server.services.projectTemplate.deleteProjectTemplateById(
|
|
req.params.templateId,
|
|
req.permission
|
|
);
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
orgId: req.permission.orgId,
|
|
event: {
|
|
type: EventType.DELETE_PROJECT_TEMPLATE,
|
|
metadata: {
|
|
templateId: req.params.templateId
|
|
}
|
|
}
|
|
});
|
|
|
|
return { projectTemplate };
|
|
}
|
|
});
|
|
};
|