mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 02:27:37 +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.PkiAlerts);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateAuthorities);
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
|
||||||
@@ -1031,7 +1030,6 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
|
||||||
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateAuthorities);
|
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ProjectMembershipsSchema,
|
ProjectMembershipsSchema,
|
||||||
ProjectRolesSchema,
|
ProjectRolesSchema,
|
||||||
ProjectSlackConfigsSchema,
|
ProjectSlackConfigsSchema,
|
||||||
|
ProjectSshConfigsSchema,
|
||||||
ProjectType,
|
ProjectType,
|
||||||
SecretFoldersSchema,
|
SecretFoldersSchema,
|
||||||
SortDirection,
|
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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/slack-config",
|
url: "/:workspaceId/slack-config",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ import {
|
|||||||
TGetProjectDTO,
|
TGetProjectDTO,
|
||||||
TGetProjectKmsKey,
|
TGetProjectKmsKey,
|
||||||
TGetProjectSlackConfig,
|
TGetProjectSlackConfig,
|
||||||
|
TGetProjectSshConfig,
|
||||||
TListProjectAlertsDTO,
|
TListProjectAlertsDTO,
|
||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertificateTemplatesDTO,
|
TListProjectCertificateTemplatesDTO,
|
||||||
@@ -92,6 +93,7 @@ import {
|
|||||||
TUpdateProjectKmsDTO,
|
TUpdateProjectKmsDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectSlackConfig,
|
TUpdateProjectSlackConfig,
|
||||||
|
TUpdateProjectSshConfig,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
TUpgradeProjectDTO
|
TUpgradeProjectDTO
|
||||||
} from "./project-types";
|
} from "./project-types";
|
||||||
@@ -104,7 +106,7 @@ export const DEFAULT_PROJECT_ENVS = [
|
|||||||
|
|
||||||
type TProjectServiceFactoryDep = {
|
type TProjectServiceFactoryDep = {
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "create">;
|
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
|
||||||
projectQueue: TProjectQueueFactory;
|
projectQueue: TProjectQueueFactory;
|
||||||
userDAL: TUserDALFactory;
|
userDAL: TUserDALFactory;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
@@ -129,7 +131,7 @@ type TProjectServiceFactoryDep = {
|
|||||||
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
certificateTemplateDAL: Pick<TCertificateTemplateDALFactory, "getCertTemplatesByProjectId">;
|
||||||
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
pkiAlertDAL: Pick<TPkiAlertDALFactory, "find">;
|
||||||
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
pkiCollectionDAL: Pick<TPkiCollectionDALFactory, "find">;
|
||||||
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "create" | "transaction">;
|
sshCertificateAuthorityDAL: Pick<TSshCertificateAuthorityDALFactory, "find" | "findOne" | "create" | "transaction">;
|
||||||
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
sshCertificateAuthoritySecretDAL: Pick<TSshCertificateAuthoritySecretDALFactory, "create">;
|
||||||
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
sshCertificateDAL: Pick<TSshCertificateDALFactory, "find" | "countSshCertificatesInProject">;
|
||||||
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
sshCertificateTemplateDAL: Pick<TSshCertificateTemplateDALFactory, "find">;
|
||||||
@@ -1327,6 +1329,129 @@ export const projectServiceFactory = ({
|
|||||||
return { secretManagerKmsKey: kmsKey };
|
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 ({
|
const getProjectSlackConfig = async ({
|
||||||
actorId,
|
actorId,
|
||||||
actor,
|
actor,
|
||||||
@@ -1548,6 +1673,8 @@ export const projectServiceFactory = ({
|
|||||||
getProjectKmsBackup,
|
getProjectKmsBackup,
|
||||||
loadProjectKmsBackup,
|
loadProjectKmsBackup,
|
||||||
getProjectKmsKeys,
|
getProjectKmsKeys,
|
||||||
|
getProjectSshConfig,
|
||||||
|
updateProjectSshConfig,
|
||||||
getProjectSlackConfig,
|
getProjectSlackConfig,
|
||||||
updateProjectSlackConfig,
|
updateProjectSlackConfig,
|
||||||
requestProjectAccess,
|
requestProjectAccess,
|
||||||
|
|||||||
@@ -159,6 +159,13 @@ export type TListProjectSshCertificatesDTO = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TUpdateProjectSshConfig = {
|
||||||
|
defaultUserSshCaId?: string;
|
||||||
|
defaultHostSshCaId?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export type TGetProjectSshConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectSlackConfig = TProjectPermission;
|
export type TGetProjectSlackConfig = TProjectPermission;
|
||||||
|
|
||||||
export type TUpdateProjectSlackConfig = {
|
export type TUpdateProjectSlackConfig = {
|
||||||
|
|||||||
+147
-47
@@ -177,7 +177,6 @@ func issueCredentials(cmd *cobra.Command, args []string) {
|
|||||||
infisicalToken = token.Token
|
infisicalToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -411,7 +410,6 @@ func signKey(cmd *cobra.Command, args []string) {
|
|||||||
infisicalToken = token.Token
|
infisicalToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -610,25 +608,82 @@ func signKey(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sshConnect(cmd *cobra.Command, args []string) {
|
func sshConnect(cmd *cobra.Command, args []string) {
|
||||||
util.RequireLogin()
|
token, err := util.GetInfisicalToken(cmd)
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to authenticate")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var infisicalToken string
|
||||||
|
|
||||||
if loggedInUserDetails.LoginExpired {
|
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
|
||||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
infisicalToken = token.Token
|
||||||
|
} else {
|
||||||
|
util.RequireLogin()
|
||||||
|
|
||||||
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
if loggedInUserDetails.LoginExpired {
|
||||||
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
|
}
|
||||||
|
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
|
||||||
}
|
}
|
||||||
|
|
||||||
infisicalToken := loggedInUserDetails.UserCredentials.JTWToken
|
|
||||||
|
|
||||||
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile")
|
writeHostCaToFile, err := cmd.Flags().GetBool("writeHostCaToFile")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse --writeHostCaToFile flag")
|
util.HandleError(err, "Unable to parse --writeHostCaToFile flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
outFilePath, err := cmd.Flags().GetString("outFilePath")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname, _ := cmd.Flags().GetString("hostname")
|
||||||
|
loginUser, _ := cmd.Flags().GetString("loginUser")
|
||||||
|
|
||||||
|
var outputDir, privateKeyPath, publicKeyPath, signedKeyPath string
|
||||||
|
if outFilePath != "" {
|
||||||
|
if strings.HasPrefix(outFilePath, "~") {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to resolve home directory")
|
||||||
|
}
|
||||||
|
outFilePath = strings.Replace(outFilePath, "~", homeDir, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(outFilePath, "-cert.pub") {
|
||||||
|
signedKeyPath = outFilePath
|
||||||
|
baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub")
|
||||||
|
outputDir = filepath.Dir(outFilePath)
|
||||||
|
privateKeyPath = filepath.Join(outputDir, baseName)
|
||||||
|
publicKeyPath = filepath.Join(outputDir, baseName+".pub")
|
||||||
|
} else {
|
||||||
|
outputDir = outFilePath
|
||||||
|
info, err := os.Stat(outputDir)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(outputDir, 0755)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to create output directory")
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
util.HandleError(err, "Failed to access output directory")
|
||||||
|
} else if !info.IsDir() {
|
||||||
|
util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory")
|
||||||
|
}
|
||||||
|
fileName := "id_ed25519"
|
||||||
|
privateKeyPath = filepath.Join(outputDir, fileName)
|
||||||
|
publicKeyPath = filepath.Join(outputDir, fileName+".pub")
|
||||||
|
signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub")
|
||||||
|
}
|
||||||
|
|
||||||
|
if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" {
|
||||||
|
util.PrintErrorMessageAndExit("Failed to resolve file paths for writing credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to get custom headers")
|
util.HandleError(err, "Unable to get custom headers")
|
||||||
@@ -651,43 +706,68 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
util.PrintErrorMessageAndExit("You do not have access to any SSH hosts")
|
util.PrintErrorMessageAndExit("You do not have access to any SSH hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prompt to select host
|
var selectedHost = hosts[0]
|
||||||
hostNames := make([]string, len(hosts))
|
if hostname != "" {
|
||||||
for i, h := range hosts {
|
foundHost := false
|
||||||
hostNames[i] = h.Hostname
|
for _, h := range hosts {
|
||||||
|
if h.Hostname == hostname {
|
||||||
|
selectedHost = h
|
||||||
|
foundHost = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHost {
|
||||||
|
util.PrintErrorMessageAndExit("Specified --hostname not found or not accessible")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hostNames := make([]string, len(hosts))
|
||||||
|
for i, h := range hosts {
|
||||||
|
hostNames[i] = h.Hostname
|
||||||
|
}
|
||||||
|
hostPrompt := promptui.Select{
|
||||||
|
Label: "Select an SSH Host",
|
||||||
|
Items: hostNames,
|
||||||
|
Size: 10,
|
||||||
|
}
|
||||||
|
hostIdx, _, err := hostPrompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Prompt failed")
|
||||||
|
}
|
||||||
|
selectedHost = hosts[hostIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
hostPrompt := promptui.Select{
|
var selectedLoginUser string
|
||||||
Label: "Select an SSH Host",
|
if loginUser != "" {
|
||||||
Items: hostNames,
|
foundLoginUser := false
|
||||||
Size: 10,
|
for _, m := range selectedHost.LoginMappings {
|
||||||
|
if m.LoginUser == loginUser {
|
||||||
|
selectedLoginUser = loginUser
|
||||||
|
foundLoginUser = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundLoginUser {
|
||||||
|
util.PrintErrorMessageAndExit("Specified --loginUser not valid for selected host")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(selectedHost.LoginMappings) == 0 {
|
||||||
|
util.PrintErrorMessageAndExit("No login users available for selected host")
|
||||||
|
}
|
||||||
|
loginUsers := make([]string, len(selectedHost.LoginMappings))
|
||||||
|
for i, m := range selectedHost.LoginMappings {
|
||||||
|
loginUsers[i] = m.LoginUser
|
||||||
|
}
|
||||||
|
loginPrompt := promptui.Select{
|
||||||
|
Label: "Select Login User",
|
||||||
|
Items: loginUsers,
|
||||||
|
Size: 5,
|
||||||
|
}
|
||||||
|
loginIdx, _, err := loginPrompt.Run()
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Prompt failed")
|
||||||
|
}
|
||||||
|
selectedLoginUser = selectedHost.LoginMappings[loginIdx].LoginUser
|
||||||
}
|
}
|
||||||
hostIdx, _, err := hostPrompt.Run()
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err, "Prompt failed")
|
|
||||||
}
|
|
||||||
selectedHost := hosts[hostIdx]
|
|
||||||
|
|
||||||
// Prompt to select login user
|
|
||||||
if len(selectedHost.LoginMappings) == 0 {
|
|
||||||
util.PrintErrorMessageAndExit("No login users available for selected host")
|
|
||||||
}
|
|
||||||
|
|
||||||
loginUsers := make([]string, len(selectedHost.LoginMappings))
|
|
||||||
for i, m := range selectedHost.LoginMappings {
|
|
||||||
loginUsers[i] = m.LoginUser
|
|
||||||
}
|
|
||||||
|
|
||||||
loginPrompt := promptui.Select{
|
|
||||||
Label: "Select Login User",
|
|
||||||
Items: loginUsers,
|
|
||||||
Size: 5,
|
|
||||||
}
|
|
||||||
loginIdx, _, err := loginPrompt.Run()
|
|
||||||
if err != nil {
|
|
||||||
util.HandleError(err, "Prompt failed")
|
|
||||||
}
|
|
||||||
selectedLoginUser := selectedHost.LoginMappings[loginIdx].LoginUser
|
|
||||||
|
|
||||||
// Issue SSH creds for host
|
// Issue SSH creds for host
|
||||||
creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{
|
creds, err := infisicalClient.Ssh().IssueSshHostUserCert(selectedHost.ID, infisicalSdk.IssueSshHostUserCertOptions{
|
||||||
@@ -731,10 +811,27 @@ func sshConnect(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Failed to write Host CA to known_hosts")
|
util.HandleError(err, "Failed to write Host CA to known_hosts")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("📁 Wrote Host CA entry to %s\n", knownHostsPath)
|
fmt.Printf("Successfully wrote Host CA entry to %s\n", knownHostsPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if outFilePath != "" {
|
||||||
|
err = os.WriteFile(privateKeyPath, []byte(creds.PrivateKey), 0600)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to write private key")
|
||||||
|
}
|
||||||
|
err = os.WriteFile(publicKeyPath, []byte(creds.PublicKey), 0644)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to write public key")
|
||||||
|
}
|
||||||
|
err = os.WriteFile(signedKeyPath, []byte(creds.SignedKey), 0644)
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Failed to write signed cert")
|
||||||
|
}
|
||||||
|
fmt.Printf("Successfully wrote credentials to %s, %s, and %s\n", privateKeyPath, publicKeyPath, signedKeyPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Load credentials into SSH agent
|
// Load credentials into SSH agent
|
||||||
err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
|
err = addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -769,7 +866,6 @@ func sshAddHost(cmd *cobra.Command, args []string) {
|
|||||||
infisicalToken = token.Token
|
infisicalToken = token.Token
|
||||||
} else {
|
} else {
|
||||||
util.RequireLogin()
|
util.RequireLogin()
|
||||||
util.RequireLocalWorkspaceFile()
|
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1006,7 +1102,11 @@ func init() {
|
|||||||
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
|
||||||
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
sshCmd.AddCommand(sshIssueCredentialsCmd)
|
||||||
|
|
||||||
|
sshConnectCmd.Flags().String("token", "", "Use a machine identity access token")
|
||||||
sshConnectCmd.Flags().Bool("writeHostCaToFile", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist")
|
sshConnectCmd.Flags().Bool("writeHostCaToFile", true, "Write Host CA public key to ~/.ssh/known_hosts as a separate entry if doesn't already exist")
|
||||||
|
sshConnectCmd.Flags().String("hostname", "", "Hostname of the SSH host to connect to")
|
||||||
|
sshConnectCmd.Flags().String("loginUser", "", "Login user for the SSH connection")
|
||||||
|
sshConnectCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be added to the SSH agent and used to establish an interactive SSH connection")
|
||||||
sshCmd.AddCommand(sshConnectCmd)
|
sshCmd.AddCommand(sshConnectCmd)
|
||||||
|
|
||||||
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
|
sshAddHostCmd.Flags().String("token", "", "Use a machine identity access token")
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ Infisical SSH can be configured to provide users on your team short-lived, secur
|
|||||||
and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
|
and improves upon traditional SSH key-based authentication by mitigating private key compromise, static key management,
|
||||||
unauthorized access, and SSH key sprawl.
|
unauthorized access, and SSH key sprawl.
|
||||||
|
|
||||||
The following entities and concepts are important to understand when using Infisical SSH:
|
The following entities are important to understand when configuring and using Infisical SSH:
|
||||||
|
|
||||||
- Administrator: An individual on your team who is responsible for configuring Infisical SSH.
|
- Administrator: An individual on your team who is responsible for configuring Infisical SSH.
|
||||||
- Users: Other individuals on your team that need access to the remote host.
|
- Users: Other individuals that gain access to remote hosts through Infisical SSH.
|
||||||
- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH.
|
- Host: A remote machine (e.g. EC2 instance, GCP VM, Azure VM, on-prem Linux server, Raspberry Pi, VMware VM, etc.) that users need SSH access to that is registered with Infisical SSH.
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ export {
|
|||||||
useLeaveProject,
|
useLeaveProject,
|
||||||
useMigrateProjectToV3,
|
useMigrateProjectToV3,
|
||||||
useRequestProjectAccess,
|
useRequestProjectAccess,
|
||||||
useUpdateGroupWorkspaceRole
|
useUpdateGroupWorkspaceRole,
|
||||||
|
useUpdateProjectSshConfig
|
||||||
} from "./mutations";
|
} from "./mutations";
|
||||||
export {
|
export {
|
||||||
useAddIdentityToWorkspace,
|
useAddIdentityToWorkspace,
|
||||||
@@ -14,6 +15,7 @@ export {
|
|||||||
useDeleteUserFromWorkspace,
|
useDeleteUserFromWorkspace,
|
||||||
useDeleteWorkspace,
|
useDeleteWorkspace,
|
||||||
useDeleteWsEnvironment,
|
useDeleteWsEnvironment,
|
||||||
|
useGetProjectSshConfig,
|
||||||
useGetUpgradeProjectStatus,
|
useGetUpgradeProjectStatus,
|
||||||
useGetUserWorkspaceMemberships,
|
useGetUserWorkspaceMemberships,
|
||||||
useGetUserWorkspaces,
|
useGetUserWorkspaces,
|
||||||
|
|||||||
@@ -4,7 +4,11 @@ import { apiRequest } from "@app/config/request";
|
|||||||
|
|
||||||
import { userKeys } from "../users/query-keys";
|
import { userKeys } from "../users/query-keys";
|
||||||
import { workspaceKeys } from "./query-keys";
|
import { workspaceKeys } from "./query-keys";
|
||||||
import { TUpdateWorkspaceGroupRoleDTO } from "./types";
|
import {
|
||||||
|
TProjectSshConfig,
|
||||||
|
TUpdateProjectSshConfigDTO,
|
||||||
|
TUpdateWorkspaceGroupRoleDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useAddGroupToWorkspace = () => {
|
export const useAddGroupToWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -117,3 +121,20 @@ export const useRequestProjectAccess = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useUpdateProjectSshConfig = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TProjectSshConfig, object, TUpdateProjectSshConfigDTO>({
|
||||||
|
mutationFn: ({ projectId, defaultUserSshCaId, defaultHostSshCaId }) => {
|
||||||
|
return apiRequest.patch(`/api/v1/workspace/${projectId}/ssh-config`, {
|
||||||
|
defaultUserSshCaId,
|
||||||
|
defaultHostSshCaId
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: workspaceKeys.getProjectSshConfig(projectId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
TListProjectIdentitiesDTO,
|
TListProjectIdentitiesDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
ToggleDeleteProjectProtectionDTO,
|
ToggleDeleteProjectProtectionDTO,
|
||||||
|
TProjectSshConfig,
|
||||||
TSearchProjectsDTO,
|
TSearchProjectsDTO,
|
||||||
TUpdateWorkspaceIdentityRoleDTO,
|
TUpdateWorkspaceIdentityRoleDTO,
|
||||||
TUpdateWorkspaceUserRoleDTO,
|
TUpdateWorkspaceUserRoleDTO,
|
||||||
@@ -887,3 +888,17 @@ export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: strin
|
|||||||
enabled: Boolean(workspaceId)
|
enabled: Boolean(workspaceId)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetProjectSshConfig = (projectId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: workspaceKeys.getProjectSshConfig(projectId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TProjectSshConfig>(
|
||||||
|
`/api/v1/workspace/${projectId}/ssh-config`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -69,5 +69,6 @@ export const workspaceKeys = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
}) => [...workspaceKeys.allWorkspaceSshCertificates(projectId), { offset, limit }] as const,
|
}) => [...workspaceKeys.allWorkspaceSshCertificates(projectId), { offset, limit }] as const,
|
||||||
getWorkspaceSshCertificateTemplates: (projectId: string) =>
|
getWorkspaceSshCertificateTemplates: (projectId: string) =>
|
||||||
[{ projectId }, "workspace-ssh-certificate-templates"] as const
|
[{ projectId }, "workspace-ssh-certificate-templates"] as const,
|
||||||
|
getProjectSshConfig: (projectId: string) => [{ projectId }, "project-ssh-config"] as const
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -184,3 +184,18 @@ export type TSearchProjectsDTO = {
|
|||||||
orderBy?: ProjectIdentityOrderBy;
|
orderBy?: ProjectIdentityOrderBy;
|
||||||
orderDirection?: OrderByDirection;
|
orderDirection?: OrderByDirection;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TProjectSshConfig = {
|
||||||
|
id: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
projectId: string;
|
||||||
|
defaultUserSshCaId: string | null;
|
||||||
|
defaultHostSshCaId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateProjectSshConfigDTO = {
|
||||||
|
projectId: string;
|
||||||
|
defaultUserSshCaId?: string;
|
||||||
|
defaultHostSshCaId?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
|
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
BreadcrumbContainer,
|
BreadcrumbContainer,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -11,7 +12,12 @@ import {
|
|||||||
MenuItem,
|
MenuItem,
|
||||||
TBreadcrumbFormat
|
TBreadcrumbFormat
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useSubscription, useWorkspace } from "@app/context";
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useSubscription,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetAccessRequestsCount,
|
useGetAccessRequestsCount,
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
@@ -159,22 +165,31 @@ export const ProjectLayout = () => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link> */}
|
</Link> */}
|
||||||
{/* <Link
|
<ProjectPermissionCan
|
||||||
to={`/${ProjectType.SSH}/$projectId/cas` as const}
|
I={ProjectPermissionActions.Read}
|
||||||
params={{
|
a={ProjectPermissionSub.SshCertificateAuthorities}
|
||||||
projectId: currentWorkspace.id
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
{(isAllowed) =>
|
||||||
<MenuItem
|
isAllowed && (
|
||||||
isSelected={isActive}
|
<Link
|
||||||
icon="certificate-authority"
|
to={`/${ProjectType.SSH}/$projectId/cas` as const}
|
||||||
iconMode="reverse"
|
params={{
|
||||||
>
|
projectId: currentWorkspace.id
|
||||||
Certificate Authorities
|
}}
|
||||||
</MenuItem>
|
>
|
||||||
)}
|
{({ isActive }) => (
|
||||||
</Link> */}
|
<MenuItem
|
||||||
|
isSelected={isActive}
|
||||||
|
icon="certificate-authority"
|
||||||
|
iconMode="reverse"
|
||||||
|
>
|
||||||
|
Certificate Authorities
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isSecretManager && (
|
{isSecretManager && (
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
import { ProjectGeneralTab } from "./components/ProjectGeneralTab";
|
||||||
|
import { ProjectSshTab } from "./components/ProjectSshTab";
|
||||||
const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }];
|
|
||||||
|
|
||||||
export const SettingsPage = () => {
|
export const SettingsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -17,19 +18,28 @@ export const SettingsPage = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="w-full max-w-7xl">
|
<div className="w-full max-w-7xl">
|
||||||
<PageHeader title={t("settings.project.title")} />
|
<PageHeader title={t("settings.project.title")} />
|
||||||
<Tabs defaultValue={tabs[0].key}>
|
<Tabs defaultValue="tab-project-general">
|
||||||
<TabList>
|
<TabList>
|
||||||
{tabs.map((tab) => (
|
<Tab value="tab-project-general">General</Tab>
|
||||||
<Tab value={tab.key} key={tab.key}>
|
<ProjectPermissionCan
|
||||||
{tab.name}
|
I={ProjectPermissionActions.Edit}
|
||||||
</Tab>
|
a={ProjectPermissionSub.Project}
|
||||||
))}
|
>
|
||||||
|
{(isAllowed) => isAllowed && <Tab value="tab-project-ssh">SSH Settings</Tab>}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</TabList>
|
</TabList>
|
||||||
{tabs.map(({ key, Component }) => (
|
<TabPanel value="tab-project-general">
|
||||||
<TabPanel value={key} key={key}>
|
<ProjectGeneralTab />
|
||||||
<Component />
|
</TabPanel>
|
||||||
</TabPanel>
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
||||||
))}
|
{(isAllowed) =>
|
||||||
|
isAllowed && (
|
||||||
|
<TabPanel value="tab-project-ssh">
|
||||||
|
<ProjectSshTab />
|
||||||
|
</TabPanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ProjectPermissionCan>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ProjectSshConfigCasSection } from "./components";
|
||||||
|
|
||||||
|
export const ProjectSshTab = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ProjectSshConfigCasSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetProjectSshConfig,
|
||||||
|
useListWorkspaceSshCas,
|
||||||
|
useUpdateProjectSshConfig
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
defaultUserSshCaId: z.string().optional(),
|
||||||
|
defaultHostSshCaId: z.string().optional()
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const ProjectSshConfigCasSection = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: sshConfig } = useGetProjectSshConfig(currentWorkspace.id);
|
||||||
|
const { data: sshCas } = useListWorkspaceSshCas(currentWorkspace.id);
|
||||||
|
const { mutate: updateProjectSshConfig } = useUpdateProjectSshConfig();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (sshConfig) {
|
||||||
|
reset({
|
||||||
|
defaultUserSshCaId: sshConfig.defaultUserSshCaId || undefined,
|
||||||
|
defaultHostSshCaId: sshConfig.defaultHostSshCaId || undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [sshConfig]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ defaultUserSshCaId, defaultHostSshCaId }: FormData) => {
|
||||||
|
try {
|
||||||
|
await updateProjectSshConfig({
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
defaultUserSshCaId: defaultUserSshCaId || undefined,
|
||||||
|
defaultHostSshCaId: defaultHostSshCaId || undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated SSH project settings",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update SSH project settings",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<p className="mb-8 text-xl font-semibold">Certificate Authorities</p>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="defaultUserSshCaId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Default User CA"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="min-w-[20rem]"
|
||||||
|
>
|
||||||
|
{sshCas?.map(({ id, friendlyName }) => (
|
||||||
|
<SelectItem value={String(id || "")} key={friendlyName}>
|
||||||
|
{friendlyName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="defaultHostSshCaId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Default Host CA"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="min-w-[20rem]"
|
||||||
|
>
|
||||||
|
{sshCas?.map(({ id, friendlyName }) => (
|
||||||
|
<SelectItem value={String(id || "")} key={friendlyName}>
|
||||||
|
{friendlyName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isAllowed}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectSshConfigCasSection } from "./ProjectSshConfigCasSection";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectSshTab } from "./ProjectSshTab";
|
||||||
Reference in New Issue
Block a user