mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-18 15:04:59 +00:00
582 lines
17 KiB
TypeScript
582 lines
17 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-base-to-string */
|
|
import { z } from "zod";
|
|
|
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|
import { removeTrailingSlash } from "@app/lib/fn";
|
|
import { isValidFolderName } from "@app/lib/validator";
|
|
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
|
import { SecretNameSchema } from "@app/server/lib/schemas";
|
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
|
|
import { AuthMode } from "@app/services/auth/auth-type";
|
|
import { commitChangesResponseSchema, resourceChangeSchema } from "@app/services/folder-commit/folder-commit-schemas";
|
|
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
|
|
|
|
const commitHistoryItemSchema = z.object({
|
|
id: z.string(),
|
|
folderId: z.string(),
|
|
actorType: z.string(),
|
|
actorMetadata: z.unknown().optional(),
|
|
message: z.string().optional().nullable(),
|
|
commitId: z.string(),
|
|
createdAt: z.string().or(z.date()),
|
|
envId: z.string()
|
|
});
|
|
|
|
const folderStateSchema = z.array(
|
|
z.object({
|
|
type: z.string(),
|
|
id: z.string(),
|
|
versionId: z.string(),
|
|
secretKey: z.string().optional(),
|
|
secretVersion: z.number().optional(),
|
|
folderName: z.string().optional(),
|
|
folderVersion: z.number().optional()
|
|
})
|
|
);
|
|
|
|
export const registerPITRouter = async (server: FastifyZodProvider) => {
|
|
// Get commits count for a folder
|
|
server.route({
|
|
method: "GET",
|
|
url: "/commits/count",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
querystring: z.object({
|
|
environment: z.string().trim(),
|
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
|
projectId: z.string().trim()
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
count: z.number(),
|
|
folderId: z.string()
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.getCommitsCount({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.query.projectId,
|
|
environment: req.query.environment,
|
|
path: req.query.path
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.query.projectId,
|
|
event: {
|
|
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
|
|
metadata: {
|
|
environment: req.query.environment,
|
|
path: req.query.path,
|
|
commitCount: result.count.toString()
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Get all commits for a folder
|
|
server.route({
|
|
method: "GET",
|
|
url: "/commits",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
querystring: z.object({
|
|
environment: z.string().trim(),
|
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
|
projectId: z.string().trim(),
|
|
offset: z.coerce.number().min(0).default(0),
|
|
limit: z.coerce.number().min(1).max(100).default(20),
|
|
search: z.string().trim().optional(),
|
|
sort: z.enum(["asc", "desc"]).default("desc")
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
commits: commitHistoryItemSchema.array(),
|
|
total: z.number(),
|
|
hasMore: z.boolean()
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.getCommitsForFolder({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.query.projectId,
|
|
environment: req.query.environment,
|
|
path: req.query.path,
|
|
offset: req.query.offset,
|
|
limit: req.query.limit,
|
|
search: req.query.search,
|
|
sort: req.query.sort
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.query.projectId,
|
|
event: {
|
|
type: EventType.GET_PROJECT_PIT_COMMITS,
|
|
metadata: {
|
|
environment: req.query.environment,
|
|
path: req.query.path,
|
|
commitCount: result.commits.length.toString(),
|
|
offset: req.query.offset.toString(),
|
|
limit: req.query.limit.toString(),
|
|
search: req.query.search,
|
|
sort: req.query.sort
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Get commit changes for a specific commit
|
|
server.route({
|
|
method: "GET",
|
|
url: "/commits/:commitId/changes",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
params: z.object({
|
|
commitId: z.string().trim()
|
|
}),
|
|
querystring: z.object({
|
|
projectId: z.string().trim()
|
|
}),
|
|
response: {
|
|
200: commitChangesResponseSchema
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.getCommitChanges({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.query.projectId,
|
|
commitId: req.params.commitId
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.query.projectId,
|
|
event: {
|
|
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
|
|
metadata: {
|
|
commitId: req.params.commitId,
|
|
changesCount: (result.changes.changes?.length || 0).toString()
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Retrieve rollback changes for a commit
|
|
server.route({
|
|
method: "GET",
|
|
url: "/commits/:commitId/compare",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
params: z.object({
|
|
commitId: z.string().trim()
|
|
}),
|
|
querystring: z.object({
|
|
folderId: z.string().trim(),
|
|
environment: z.string().trim(),
|
|
deepRollback: booleanSchema.default(false),
|
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
|
projectId: z.string().trim()
|
|
}),
|
|
response: {
|
|
200: z.array(
|
|
z.object({
|
|
folderId: z.string(),
|
|
folderName: z.string(),
|
|
folderPath: z.string().optional(),
|
|
changes: z.array(resourceChangeSchema)
|
|
})
|
|
)
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.compareCommitChanges({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.query.projectId,
|
|
commitId: req.params.commitId,
|
|
folderId: req.query.folderId,
|
|
environment: req.query.environment,
|
|
deepRollback: req.query.deepRollback,
|
|
secretPath: req.query.secretPath
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.query.projectId,
|
|
event: {
|
|
type: EventType.PIT_COMPARE_FOLDER_STATES,
|
|
metadata: {
|
|
targetCommitId: req.params.commitId,
|
|
folderId: req.query.folderId,
|
|
deepRollback: req.query.deepRollback,
|
|
diffsCount: result.length.toString(),
|
|
environment: req.query.environment,
|
|
folderPath: req.query.secretPath
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Rollback to a previous commit
|
|
server.route({
|
|
method: "POST",
|
|
url: "/commits/:commitId/rollback",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
params: z.object({
|
|
commitId: z.string().trim()
|
|
}),
|
|
body: z.object({
|
|
folderId: z.string().trim(),
|
|
deepRollback: z.boolean().default(false),
|
|
message: z.string().max(256).trim().optional(),
|
|
environment: z.string().trim(),
|
|
projectId: z.string().trim()
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
success: z.boolean(),
|
|
secretChangesCount: z.number().optional(),
|
|
folderChangesCount: z.number().optional(),
|
|
totalChanges: z.number().optional()
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.rollbackToCommit({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.body.projectId,
|
|
commitId: req.params.commitId,
|
|
folderId: req.body.folderId,
|
|
deepRollback: req.body.deepRollback,
|
|
message: req.body.message,
|
|
environment: req.body.environment
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.body.projectId,
|
|
event: {
|
|
type: EventType.PIT_ROLLBACK_COMMIT,
|
|
metadata: {
|
|
targetCommitId: req.params.commitId,
|
|
environment: req.body.environment,
|
|
folderId: req.body.folderId,
|
|
deepRollback: req.body.deepRollback,
|
|
message: req.body.message || "Rollback to previous commit",
|
|
totalChanges: result.totalChanges?.toString() || "0"
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Revert commit
|
|
server.route({
|
|
method: "POST",
|
|
url: "/commits/:commitId/revert",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
params: z.object({
|
|
commitId: z.string().trim()
|
|
}),
|
|
body: z.object({
|
|
projectId: z.string().trim()
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
success: z.boolean(),
|
|
message: z.string(),
|
|
originalCommitId: z.string(),
|
|
revertCommitId: z.string().optional(),
|
|
changesReverted: z.number().optional()
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.revertCommit({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.body.projectId,
|
|
commitId: req.params.commitId
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.body.projectId,
|
|
event: {
|
|
type: EventType.PIT_REVERT_COMMIT,
|
|
metadata: {
|
|
commitId: req.params.commitId,
|
|
revertCommitId: result.revertCommitId,
|
|
changesReverted: result.changesReverted?.toString()
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
// Folder state at commit
|
|
server.route({
|
|
method: "GET",
|
|
url: "/commits/:commitId",
|
|
config: {
|
|
rateLimit: readLimit
|
|
},
|
|
schema: {
|
|
params: z.object({
|
|
commitId: z.string().trim()
|
|
}),
|
|
querystring: z.object({
|
|
folderId: z.string().trim(),
|
|
projectId: z.string().trim()
|
|
}),
|
|
response: {
|
|
200: folderStateSchema
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.getFolderStateAtCommit({
|
|
actor: req.permission?.type,
|
|
actorId: req.permission?.id,
|
|
actorOrgId: req.permission?.orgId,
|
|
actorAuthMethod: req.permission?.authMethod,
|
|
projectId: req.query.projectId,
|
|
commitId: req.params.commitId
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.query.projectId,
|
|
event: {
|
|
type: EventType.PIT_GET_FOLDER_STATE,
|
|
metadata: {
|
|
commitId: req.params.commitId,
|
|
folderId: req.query.folderId,
|
|
resourceCount: result.length.toString()
|
|
}
|
|
}
|
|
});
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
server.route({
|
|
method: "POST",
|
|
url: "/batch/commit",
|
|
config: {
|
|
rateLimit: secretsLimit
|
|
},
|
|
schema: {
|
|
hide: true,
|
|
description: "Commit changes",
|
|
security: [
|
|
{
|
|
bearerAuth: []
|
|
}
|
|
],
|
|
body: z.object({
|
|
projectId: z.string().trim(),
|
|
environment: z.string().trim(),
|
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
|
message: z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(255)
|
|
.refine((message) => message.trim() !== "", {
|
|
message: "Commit message cannot be empty"
|
|
}),
|
|
changes: z.object({
|
|
secrets: z.object({
|
|
create: z
|
|
.array(
|
|
z.object({
|
|
secretKey: SecretNameSchema,
|
|
secretValue: z.string().transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
|
|
secretComment: z.string().trim().optional().default(""),
|
|
skipMultilineEncoding: z.boolean().optional(),
|
|
metadata: z.record(z.string()).optional(),
|
|
secretMetadata: ResourceMetadataSchema.optional(),
|
|
tagIds: z.string().array().optional()
|
|
})
|
|
)
|
|
.optional(),
|
|
update: z
|
|
.array(
|
|
z.object({
|
|
secretKey: SecretNameSchema,
|
|
newSecretName: SecretNameSchema.optional(),
|
|
secretValue: z
|
|
.string()
|
|
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
|
|
.optional(),
|
|
secretComment: z.string().trim().optional().default(""),
|
|
skipMultilineEncoding: z.boolean().optional(),
|
|
metadata: z.record(z.string()).optional(),
|
|
secretMetadata: ResourceMetadataSchema.optional(),
|
|
tagIds: z.string().array().optional()
|
|
})
|
|
)
|
|
.optional(),
|
|
delete: z
|
|
.array(
|
|
z.object({
|
|
secretKey: SecretNameSchema
|
|
})
|
|
)
|
|
.optional()
|
|
}),
|
|
folders: z.object({
|
|
create: z
|
|
.array(
|
|
z.object({
|
|
folderName: z
|
|
.string()
|
|
.trim()
|
|
.refine((name) => isValidFolderName(name), {
|
|
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
|
|
}),
|
|
description: z.string().optional()
|
|
})
|
|
)
|
|
.optional(),
|
|
update: z
|
|
.array(
|
|
z.object({
|
|
folderName: z
|
|
.string()
|
|
.trim()
|
|
.refine((name) => isValidFolderName(name), {
|
|
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
|
|
}),
|
|
description: z.string().nullable().optional(),
|
|
id: z.string()
|
|
})
|
|
)
|
|
.optional(),
|
|
delete: z
|
|
.array(
|
|
z.object({
|
|
folderName: z
|
|
.string()
|
|
.trim()
|
|
.refine((name) => isValidFolderName(name), {
|
|
message: "Invalid folder name. Only alphanumeric characters, dashes, and underscores are allowed."
|
|
}),
|
|
id: z.string()
|
|
})
|
|
)
|
|
.optional()
|
|
})
|
|
})
|
|
}),
|
|
response: {
|
|
200: z.object({
|
|
message: z.string()
|
|
})
|
|
}
|
|
},
|
|
onRequest: verifyAuth([AuthMode.JWT]),
|
|
handler: async (req) => {
|
|
const result = await server.services.pit.processNewCommitRaw({
|
|
actorId: req.permission.id,
|
|
actor: req.permission.type,
|
|
actorOrgId: req.permission.orgId,
|
|
actorAuthMethod: req.permission.authMethod,
|
|
projectId: req.body.projectId,
|
|
environment: req.body.environment,
|
|
secretPath: req.body.secretPath,
|
|
message: req.body.message,
|
|
changes: {
|
|
secrets: req.body.changes.secrets,
|
|
folders: req.body.changes.folders
|
|
}
|
|
});
|
|
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
projectId: req.body.projectId,
|
|
event: {
|
|
type: EventType.PIT_PROCESS_NEW_COMMIT_RAW,
|
|
metadata: {
|
|
commitId: result.commitId,
|
|
approvalId: result.approvalId,
|
|
projectId: req.body.projectId,
|
|
environment: req.body.environment,
|
|
secretPath: req.body.secretPath,
|
|
message: req.body.message
|
|
}
|
|
}
|
|
});
|
|
|
|
for await (const event of result.secretMutationEvents) {
|
|
await server.services.auditLog.createAuditLog({
|
|
...req.auditLogInfo,
|
|
orgId: req.permission.orgId,
|
|
projectId: req.body.projectId,
|
|
event
|
|
});
|
|
}
|
|
|
|
return { message: "success" };
|
|
}
|
|
});
|
|
};
|