mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 15:40:47 +00:00
Make infisical ssh v2 work in non-interactive mode, allow reassignment of default ssh cas
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectType, TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasDefaultUserCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultUserSshCaId");
|
||||
const hasDefaultHostCaCol = await knex.schema.hasColumn(TableName.ProjectSshConfig, "defaultHostSshCaId");
|
||||
|
||||
if (hasDefaultUserCaCol && hasDefaultHostCaCol) {
|
||||
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
|
||||
t.dropForeign(["defaultUserSshCaId"]);
|
||||
t.dropForeign(["defaultHostSshCaId"]);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.ProjectSshConfig, (t) => {
|
||||
// allow nullable (does not wipe existing values)
|
||||
t.uuid("defaultUserSshCaId").nullable().alter();
|
||||
t.uuid("defaultHostSshCaId").nullable().alter();
|
||||
|
||||
// re-add with SET NULL behavior (previously CASCADE)
|
||||
t.foreign("defaultUserSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||
t.foreign("defaultHostSshCaId").references("id").inTable(TableName.SshCertificateAuthority).onDelete("SET NULL");
|
||||
});
|
||||
}
|
||||
|
||||
// (dangtony98): backfill by adding null defaults CAs for all existing Infisical SSH projects
|
||||
// that do not have an associated ProjectSshConfig record introduced in Infisical SSH V2.
|
||||
|
||||
const allProjects = await knex(TableName.Project).where("type", ProjectType.SSH).select("id");
|
||||
|
||||
const projectsWithConfig = await knex(TableName.ProjectSshConfig).select("projectId");
|
||||
const projectIdsWithConfig = new Set(projectsWithConfig.map((config) => config.projectId));
|
||||
|
||||
const projectsNeedingConfig = allProjects.filter((project) => !projectIdsWithConfig.has(project.id));
|
||||
|
||||
if (projectsNeedingConfig.length > 0) {
|
||||
const configsToInsert = projectsNeedingConfig.map((project) => ({
|
||||
projectId: project.id,
|
||||
defaultUserSshCaId: null,
|
||||
defaultHostSshCaId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}));
|
||||
|
||||
await knex.batchInsert(TableName.ProjectSshConfig, configsToInsert);
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
||||
@@ -965,7 +965,6 @@ const buildMemberPermissionRules = () => {
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||
@@ -1031,7 +1030,6 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ProjectMembershipsSchema,
|
||||
ProjectRolesSchema,
|
||||
ProjectSlackConfigsSchema,
|
||||
ProjectSshConfigsSchema,
|
||||
ProjectType,
|
||||
SecretFoldersSchema,
|
||||
SortDirection,
|
||||
@@ -612,6 +613,83 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/ssh-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSshConfigsSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
projectId: true,
|
||||
defaultUserSshCaId: true,
|
||||
defaultHostSshCaId: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshConfig = await server.services.project.getProjectSshConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
// TODO: consider adding audit logs
|
||||
|
||||
return sshConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:workspaceId/ssh-config",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
defaultUserSshCaId: z.string().optional(),
|
||||
defaultHostSshCaId: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: ProjectSshConfigsSchema.pick({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
projectId: true,
|
||||
defaultUserSshCaId: true,
|
||||
defaultHostSshCaId: true
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const sshConfig = await server.services.project.updateProjectSshConfig({
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.body
|
||||
});
|
||||
|
||||
return sshConfig;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/slack-config",
|
||||
|
||||
@@ -73,6 +73,7 @@ import {
|
||||
TGetProjectDTO,
|
||||
TGetProjectKmsKey,
|
||||
TGetProjectSlackConfig,
|
||||
TGetProjectSshConfig,
|
||||
TListProjectAlertsDTO,
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertificateTemplatesDTO,
|
||||
@@ -92,6 +93,7 @@ import {
|
||||
TUpdateProjectKmsDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectSlackConfig,
|
||||
TUpdateProjectSshConfig,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
TUpgradeProjectDTO
|
||||
} from "./project-types";
|
||||
@@ -104,7 +106,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
||||
|
||||
type TProjectServiceFactoryDep = {
|
||||
projectDAL: TProjectDALFactory;
|
||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "create">;
|
||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
|
||||
projectQueue: TProjectQueueFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
@@ -129,7 +131,7 @@ type TProjectServiceFactoryDep = {
|
||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "create" | "transaction">;
|
||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "findOne" | "create" | "transaction">;
|
||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
||||
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
||||
@@ -1327,6 +1329,129 @@ export const projectServiceFactory = ({
|
||||
return { secretManagerKmsKey: kmsKey };
|
||||
};
|
||||
|
||||
const getProjectSshConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId
|
||||
}: TGetProjectSshConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
|
||||
const projectSshConfig = await projectSshConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
if (!projectSshConfig) {
|
||||
throw new NotFoundError({
|
||||
message: `Project SSH config with ID '${project.id}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
return projectSshConfig;
|
||||
};
|
||||
|
||||
const updateProjectSshConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
defaultUserSshCaId,
|
||||
defaultHostSshCaId
|
||||
}: TUpdateProjectSshConfig) => {
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: `Project with ID '${projectId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SSH
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
let projectSshConfig = await projectSshConfigDAL.findOne({
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
if (!projectSshConfig) {
|
||||
throw new NotFoundError({
|
||||
message: `Project SSH config with ID '${project.id}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
projectSshConfig = await projectSshConfigDAL.transaction(async (tx) => {
|
||||
if (defaultUserSshCaId) {
|
||||
const userSshCa = await sshCertificateAuthorityDAL.findOne(
|
||||
{
|
||||
id: defaultUserSshCaId,
|
||||
projectId: project.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!userSshCa) {
|
||||
throw new NotFoundError({
|
||||
message: "User SSH CA must exist and belong to this project"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultHostSshCaId) {
|
||||
const hostSshCa = await sshCertificateAuthorityDAL.findOne(
|
||||
{
|
||||
id: defaultHostSshCaId,
|
||||
projectId: project.id
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!hostSshCa) {
|
||||
throw new NotFoundError({
|
||||
message: "Host SSH CA must exist and belong to this project"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updatedProjectSshConfig = await projectSshConfigDAL.updateById(
|
||||
projectSshConfig.id,
|
||||
{
|
||||
defaultUserSshCaId,
|
||||
defaultHostSshCaId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return updatedProjectSshConfig;
|
||||
});
|
||||
|
||||
return projectSshConfig;
|
||||
};
|
||||
|
||||
const getProjectSlackConfig = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -1548,6 +1673,8 @@ export const projectServiceFactory = ({
|
||||
getProjectKmsBackup,
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys,
|
||||
getProjectSshConfig,
|
||||
updateProjectSshConfig,
|
||||
getProjectSlackConfig,
|
||||
updateProjectSlackConfig,
|
||||
requestProjectAccess,
|
||||
|
||||
@@ -159,6 +159,13 @@ export type TListProjectSshCertificatesDTO = {
|
||||
limit: number;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TUpdateProjectSshConfig = {
|
||||
defaultUserSshCaId?: string;
|
||||
defaultHostSshCaId?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectSshConfig = TProjectPermission;
|
||||
|
||||
export type TGetProjectSlackConfig = TProjectPermission;
|
||||
|
||||
export type TUpdateProjectSlackConfig = {
|
||||
|
||||
Reference in New Issue
Block a user