mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: ssh pam draft
This commit is contained in:
@@ -9,6 +9,11 @@ import {
|
|||||||
SanitizedPostgresAccountWithResourceSchema,
|
SanitizedPostgresAccountWithResourceSchema,
|
||||||
UpdatePostgresAccountSchema
|
UpdatePostgresAccountSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
|
import {
|
||||||
|
CreateSSHAccountSchema,
|
||||||
|
SanitizedSSHAccountWithResourceSchema,
|
||||||
|
UpdateSSHAccountSchema
|
||||||
|
} from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
|
||||||
|
|
||||||
import { registerPamResourceEndpoints } from "./pam-account-endpoints";
|
import { registerPamResourceEndpoints } from "./pam-account-endpoints";
|
||||||
|
|
||||||
@@ -30,5 +35,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
|||||||
createAccountSchema: CreateMySQLAccountSchema,
|
createAccountSchema: CreateMySQLAccountSchema,
|
||||||
updateAccountSchema: UpdateMySQLAccountSchema
|
updateAccountSchema: UpdateMySQLAccountSchema
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[PamResource.SSH]: async (server: FastifyZodProvider) => {
|
||||||
|
registerPamResourceEndpoints({
|
||||||
|
server,
|
||||||
|
resourceType: PamResource.SSH,
|
||||||
|
accountResponseSchema: SanitizedSSHAccountWithResourceSchema,
|
||||||
|
createAccountSchema: CreateSSHAccountSchema,
|
||||||
|
updateAccountSchema: UpdateSSHAccountSchema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
|||||||
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
|
||||||
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
|
import { SanitizedSSHAccountWithResourceSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -12,6 +13,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
|||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
const SanitizedAccountSchema = z.union([
|
const SanitizedAccountSchema = z.union([
|
||||||
|
SanitizedSSHAccountWithResourceSchema, // ORDER MATTERS
|
||||||
SanitizedPostgresAccountWithResourceSchema,
|
SanitizedPostgresAccountWithResourceSchema,
|
||||||
SanitizedMySQLAccountWithResourceSchema
|
SanitizedMySQLAccountWithResourceSchema
|
||||||
]);
|
]);
|
||||||
@@ -93,7 +95,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
|||||||
gatewayClientPrivateKey: z.string(),
|
gatewayClientPrivateKey: z.string(),
|
||||||
gatewayServerCertificateChain: z.string(),
|
gatewayServerCertificateChain: z.string(),
|
||||||
relayHost: z.string(),
|
relayHost: z.string(),
|
||||||
metadata: z.record(z.string(), z.string()).optional()
|
metadata: z.record(z.string(), z.string().optional()).optional()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ import {
|
|||||||
SanitizedPostgresResourceSchema,
|
SanitizedPostgresResourceSchema,
|
||||||
UpdatePostgresResourceSchema
|
UpdatePostgresResourceSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
|
import {
|
||||||
|
CreateSSHResourceSchema,
|
||||||
|
SanitizedSSHResourceSchema,
|
||||||
|
UpdateSSHResourceSchema
|
||||||
|
} from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
|
||||||
|
|
||||||
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
||||||
|
|
||||||
@@ -30,5 +35,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
|||||||
createResourceSchema: CreateMySQLResourceSchema,
|
createResourceSchema: CreateMySQLResourceSchema,
|
||||||
updateResourceSchema: UpdateMySQLResourceSchema
|
updateResourceSchema: UpdateMySQLResourceSchema
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[PamResource.SSH]: async (server: FastifyZodProvider) => {
|
||||||
|
registerPamResourceEndpoints({
|
||||||
|
server,
|
||||||
|
resourceType: PamResource.SSH,
|
||||||
|
resourceResponseSchema: SanitizedSSHResourceSchema,
|
||||||
|
createResourceSchema: CreateSSHResourceSchema,
|
||||||
|
updateResourceSchema: UpdateSSHResourceSchema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,15 +9,24 @@ import {
|
|||||||
PostgresResourceListItemSchema,
|
PostgresResourceListItemSchema,
|
||||||
SanitizedPostgresResourceSchema
|
SanitizedPostgresResourceSchema
|
||||||
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
} from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
|
import {
|
||||||
|
SanitizedSSHResourceSchema,
|
||||||
|
SSHResourceListItemSchema
|
||||||
|
} from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
|
const SanitizedResourceSchema = z.union([
|
||||||
|
SanitizedPostgresResourceSchema,
|
||||||
|
SanitizedMySQLResourceSchema,
|
||||||
|
SanitizedSSHResourceSchema
|
||||||
|
]);
|
||||||
|
|
||||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||||
PostgresResourceListItemSchema,
|
PostgresResourceListItemSchema,
|
||||||
MySQLResourceListItemSchema
|
MySQLResourceListItemSchema,
|
||||||
|
SSHResourceListItemSchema
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||||
|
|||||||
@@ -4,12 +4,17 @@ import { PamSessionsSchema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
||||||
|
import { SSHSessionCredentialsSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas";
|
||||||
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
|
const SessionCredentialsSchema = z.union([
|
||||||
|
SSHSessionCredentialsSchema,
|
||||||
|
PostgresSessionCredentialsSchema,
|
||||||
|
MySQLSessionCredentialsSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
||||||
// Meant to be hit solely by gateway identities
|
// Meant to be hit solely by gateway identities
|
||||||
@@ -26,7 +31,7 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
|||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
credentials: SessionCredentialsSchema
|
credentials: z.any() // UNION DOES NOT WORK WITH ZOD SCHEMA
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -50,7 +55,7 @@ export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { credentials };
|
return { credentials: credentials as z.infer<typeof SessionCredentialsSchema> };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
|||||||
import { TLicenseServiceFactory } from "../license/license-service";
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
|
import { TPamFolderDALFactory } from "../pam-folder/pam-folder-dal";
|
||||||
import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns";
|
import { getFullPamFolderPath } from "../pam-folder/pam-folder-fns";
|
||||||
|
import { TMySQLResourceConnectionDetails } from "../pam-resource/mysql/mysql-resource-types";
|
||||||
import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal";
|
import { TPamResourceDALFactory } from "../pam-resource/pam-resource-dal";
|
||||||
import { PamResource } from "../pam-resource/pam-resource-enums";
|
import { PamResource } from "../pam-resource/pam-resource-enums";
|
||||||
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
|
import { TPamAccountCredentials } from "../pam-resource/pam-resource-types";
|
||||||
|
import { TPostgresResourceConnectionDetails } from "../pam-resource/postgres/postgres-resource-types";
|
||||||
import { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
|
import { TPamSessionDALFactory } from "../pam-session/pam-session-dal";
|
||||||
import { PamSessionStatus } from "../pam-session/pam-session-enums";
|
import { PamSessionStatus } from "../pam-session/pam-session-enums";
|
||||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
@@ -251,17 +253,17 @@ export const pamAccountServiceFactory = ({
|
|||||||
gatewayV2Service
|
gatewayV2Service
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logic to prevent overwriting unedited censored values
|
const decryptedCredentials = await decryptAccountCredentials({
|
||||||
const finalCredentials = { ...credentials };
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
if (credentials.password === "__INFISICAL_UNCHANGED__") {
|
projectId: account.projectId,
|
||||||
const decryptedCredentials = await decryptAccountCredentials({
|
kmsService
|
||||||
encryptedCredentials: account.encryptedCredentials,
|
});
|
||||||
projectId: account.projectId,
|
|
||||||
kmsService
|
|
||||||
});
|
|
||||||
|
|
||||||
finalCredentials.password = decryptedCredentials.password;
|
// Logic to prevent overwriting unedited censored values
|
||||||
}
|
const finalCredentials = await factory.handleOverwritePreventionForCensoredValues(
|
||||||
|
credentials,
|
||||||
|
decryptedCredentials
|
||||||
|
);
|
||||||
|
|
||||||
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
|
const validatedCredentials = await factory.validateAccountCredentials(finalCredentials);
|
||||||
const encryptedCredentials = await encryptAccountCredentials({
|
const encryptedCredentials = await encryptAccountCredentials({
|
||||||
@@ -486,11 +488,11 @@ export const pamAccountServiceFactory = ({
|
|||||||
case PamResource.Postgres:
|
case PamResource.Postgres:
|
||||||
case PamResource.MySQL:
|
case PamResource.MySQL:
|
||||||
{
|
{
|
||||||
const connectionCredentials = await decryptResourceConnectionDetails({
|
const connectionCredentials = (await decryptResourceConnectionDetails({
|
||||||
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
encryptedConnectionDetails: resource.encryptedConnectionDetails,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectId: account.projectId
|
projectId: account.projectId
|
||||||
});
|
})) as TMySQLResourceConnectionDetails | TPostgresResourceConnectionDetails;
|
||||||
|
|
||||||
const credentials = await decryptAccountCredentials({
|
const credentials = await decryptAccountCredentials({
|
||||||
encryptedCredentials: account.encryptedCredentials,
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
@@ -506,6 +508,21 @@ export const pamAccountServiceFactory = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case PamResource.SSH:
|
||||||
|
{
|
||||||
|
const credentials = await decryptAccountCredentials({
|
||||||
|
encryptedCredentials: account.encryptedCredentials,
|
||||||
|
kmsService,
|
||||||
|
projectId: account.projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
username: credentials.username,
|
||||||
|
accountName: account.name,
|
||||||
|
accountPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export enum PamResource {
|
export enum PamResource {
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
MySQL = "mysql"
|
MySQL = "mysql",
|
||||||
|
SSH = "ssh"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { PamResource } from "./pam-resource-enums";
|
import { PamResource } from "./pam-resource-enums";
|
||||||
import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types";
|
import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types";
|
||||||
import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
|
||||||
|
import { sshResourceFactory } from "./ssh/ssh-resource-factory";
|
||||||
|
|
||||||
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
|
||||||
|
|
||||||
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
|
||||||
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||||
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
|
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation,
|
||||||
|
[PamResource.SSH]: sshResourceFactory as TPamResourceFactoryImplementation
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -192,19 +192,18 @@ export const pamResourceServiceFactory = ({
|
|||||||
gatewayV2Service
|
gatewayV2Service
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logic to prevent overwriting unedited censored values
|
let finalCredentials = { ...rotationAccountCredentials };
|
||||||
const finalCredentials = { ...rotationAccountCredentials };
|
if (resource.encryptedRotationAccountCredentials) {
|
||||||
if (
|
|
||||||
resource.encryptedRotationAccountCredentials &&
|
|
||||||
rotationAccountCredentials.password === "__INFISICAL_UNCHANGED__"
|
|
||||||
) {
|
|
||||||
const decryptedCredentials = await decryptAccountCredentials({
|
const decryptedCredentials = await decryptAccountCredentials({
|
||||||
encryptedCredentials: resource.encryptedRotationAccountCredentials,
|
encryptedCredentials: resource.encryptedRotationAccountCredentials,
|
||||||
projectId: resource.projectId,
|
projectId: resource.projectId,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
finalCredentials.password = decryptedCredentials.password;
|
finalCredentials = await factory.handleOverwritePreventionForCensoredValues(
|
||||||
|
rotationAccountCredentials,
|
||||||
|
decryptedCredentials
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,15 +12,24 @@ import {
|
|||||||
TPostgresResource,
|
TPostgresResource,
|
||||||
TPostgresResourceConnectionDetails
|
TPostgresResourceConnectionDetails
|
||||||
} from "./postgres/postgres-resource-types";
|
} from "./postgres/postgres-resource-types";
|
||||||
|
import {
|
||||||
|
TSSHAccount,
|
||||||
|
TSSHAccountCredentials,
|
||||||
|
TSSHResource,
|
||||||
|
TSSHResourceConnectionDetails
|
||||||
|
} from "./ssh/ssh-resource-types";
|
||||||
|
|
||||||
// Resource types
|
// Resource types
|
||||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource;
|
||||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
export type TPamResourceConnectionDetails =
|
||||||
|
| TPostgresResourceConnectionDetails
|
||||||
|
| TMySQLResourceConnectionDetails
|
||||||
|
| TSSHResourceConnectionDetails;
|
||||||
|
|
||||||
// Account types
|
// Account types
|
||||||
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials | TSSHAccountCredentials;
|
||||||
|
|
||||||
// Resource DTOs
|
// Resource DTOs
|
||||||
export type TCreateResourceDTO = Pick<
|
export type TCreateResourceDTO = Pick<
|
||||||
@@ -51,4 +60,5 @@ export type TPamResourceFactory<T extends TPamResourceConnectionDetails, C exten
|
|||||||
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
validateConnection: TPamResourceFactoryValidateConnection<T>;
|
||||||
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<C>;
|
||||||
rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<C>;
|
rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<C>;
|
||||||
|
handleOverwritePreventionForCensoredValues: (updatedAccountCredentials: C, currentCredentials: C) => Promise<C>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -337,9 +337,24 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOverwritePreventionForCensoredValues = async (
|
||||||
|
updatedAccountCredentials: TSqlAccountCredentials,
|
||||||
|
currentCredentials: TSqlAccountCredentials
|
||||||
|
) => {
|
||||||
|
if (updatedAccountCredentials.password === "__INFISICAL_UNCHANGED__") {
|
||||||
|
return {
|
||||||
|
...updatedAccountCredentials,
|
||||||
|
password: currentCredentials.password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedAccountCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
validateConnection,
|
validateConnection,
|
||||||
validateAccountCredentials,
|
validateAccountCredentials,
|
||||||
rotateAccountCredentials
|
rotateAccountCredentials,
|
||||||
|
handleOverwritePreventionForCensoredValues
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export enum SSHAuthMethod {
|
||||||
|
Password = "password",
|
||||||
|
PublicKey = "public-key",
|
||||||
|
Certificate = "certificate"
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import {
|
||||||
|
TPamResourceFactory,
|
||||||
|
TPamResourceFactoryRotateAccountCredentials,
|
||||||
|
TPamResourceFactoryValidateAccountCredentials
|
||||||
|
} from "../pam-resource-types";
|
||||||
|
import { SSHAuthMethod } from "./ssh-resource-enums";
|
||||||
|
import { TSSHAccountCredentials, TSSHResourceConnectionDetails } from "./ssh-resource-types";
|
||||||
|
|
||||||
|
export const sshResourceFactory: TPamResourceFactory<TSSHResourceConnectionDetails, TSSHAccountCredentials> = (
|
||||||
|
resourceType,
|
||||||
|
connectionDetails,
|
||||||
|
gatewayId,
|
||||||
|
gatewayV2Service
|
||||||
|
) => {
|
||||||
|
const validateConnection = async () => {
|
||||||
|
return connectionDetails;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials<TSSHAccountCredentials> = async (
|
||||||
|
credentials
|
||||||
|
) => {
|
||||||
|
return credentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials<TSSHAccountCredentials> = async (
|
||||||
|
rotationAccountCredentials,
|
||||||
|
currentCredentials
|
||||||
|
) => {
|
||||||
|
return rotationAccountCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOverwritePreventionForCensoredValues = async (
|
||||||
|
updatedAccountCredentials: TSSHAccountCredentials,
|
||||||
|
currentCredentials: TSSHAccountCredentials
|
||||||
|
) => {
|
||||||
|
if (updatedAccountCredentials.authMethod !== currentCredentials.authMethod) {
|
||||||
|
return updatedAccountCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedAccountCredentials.authMethod === SSHAuthMethod.Password &&
|
||||||
|
currentCredentials.authMethod === SSHAuthMethod.Password
|
||||||
|
) {
|
||||||
|
if (updatedAccountCredentials.password === "__INFISICAL_UNCHANGED__") {
|
||||||
|
return {
|
||||||
|
...updatedAccountCredentials,
|
||||||
|
password: currentCredentials.password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedAccountCredentials.authMethod === SSHAuthMethod.PublicKey &&
|
||||||
|
currentCredentials.authMethod === SSHAuthMethod.PublicKey
|
||||||
|
) {
|
||||||
|
if (updatedAccountCredentials.privateKey === "__INFISICAL_UNCHANGED__") {
|
||||||
|
return {
|
||||||
|
...updatedAccountCredentials,
|
||||||
|
privateKey: currentCredentials.privateKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedAccountCredentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
validateConnection,
|
||||||
|
validateAccountCredentials,
|
||||||
|
rotateAccountCredentials,
|
||||||
|
handleOverwritePreventionForCensoredValues
|
||||||
|
};
|
||||||
|
};
|
||||||
117
backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts
Normal file
117
backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { PamResource } from "../pam-resource-enums";
|
||||||
|
import {
|
||||||
|
BaseCreatePamAccountSchema,
|
||||||
|
BaseCreatePamResourceSchema,
|
||||||
|
BasePamAccountSchema,
|
||||||
|
BasePamAccountSchemaWithResource,
|
||||||
|
BasePamResourceSchema,
|
||||||
|
BaseUpdatePamAccountSchema,
|
||||||
|
BaseUpdatePamResourceSchema
|
||||||
|
} from "../pam-resource-schemas";
|
||||||
|
import { SSHAuthMethod } from "./ssh-resource-enums";
|
||||||
|
|
||||||
|
export const BaseSSHResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.SSH) });
|
||||||
|
|
||||||
|
export const SSHResourceListItemSchema = z.object({
|
||||||
|
name: z.literal("SSH"),
|
||||||
|
resource: z.literal(PamResource.SSH)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHResourceConnectionDetailsSchema = z.object({
|
||||||
|
host: z.string().trim(),
|
||||||
|
port: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHPasswordCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Password),
|
||||||
|
username: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHPublicKeyCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.PublicKey),
|
||||||
|
username: z.string().trim(),
|
||||||
|
privateKey: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHCertificateCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Certificate),
|
||||||
|
username: z.string().trim()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHAccountCredentialsSchema = z.discriminatedUnion("authMethod", [
|
||||||
|
SSHPasswordCredentialsSchema,
|
||||||
|
SSHPublicKeyCredentialsSchema,
|
||||||
|
SSHCertificateCredentialsSchema
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const SSHResourceSchema = BaseSSHResourceSchema.extend({
|
||||||
|
connectionDetails: SSHResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedSSHResourceSchema = BaseSSHResourceSchema.extend({
|
||||||
|
connectionDetails: SSHResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: z
|
||||||
|
.discriminatedUnion("authMethod", [
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Password),
|
||||||
|
username: z.string()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.PublicKey),
|
||||||
|
username: z.string()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Certificate),
|
||||||
|
username: z.string()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateSSHResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||||
|
connectionDetails: SSHResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateSSHResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||||
|
connectionDetails: SSHResourceConnectionDetailsSchema.optional(),
|
||||||
|
rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export const SSHAccountSchema = BasePamAccountSchema.extend({
|
||||||
|
credentials: SSHAccountCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateSSHAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||||
|
credentials: SSHAccountCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateSSHAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||||
|
credentials: SSHAccountCredentialsSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedSSHAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||||
|
credentials: z.discriminatedUnion("authMethod", [
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Password),
|
||||||
|
username: z.string()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.PublicKey),
|
||||||
|
username: z.string()
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Certificate),
|
||||||
|
username: z.string()
|
||||||
|
})
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
export const SSHSessionCredentialsSchema = SSHResourceConnectionDetailsSchema.and(SSHAccountCredentialsSchema);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
SSHAccountCredentialsSchema,
|
||||||
|
SSHAccountSchema,
|
||||||
|
SSHResourceConnectionDetailsSchema,
|
||||||
|
SSHResourceSchema
|
||||||
|
} from "./ssh-resource-schemas";
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export type TSSHResource = z.infer<typeof SSHResourceSchema>;
|
||||||
|
export type TSSHResourceConnectionDetails = z.infer<typeof SSHResourceConnectionDetailsSchema>;
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export type TSSHAccount = z.infer<typeof SSHAccountSchema>;
|
||||||
|
export type TSSHAccountCredentials = z.infer<typeof SSHAccountCredentialsSchema>;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { PamResourceType, PamSessionStatus } from "../enums";
|
import { PamResourceType, PamSessionStatus } from "../enums";
|
||||||
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||||
|
import { TSSHAccount, TSSHResource } from "./ssh-resource";
|
||||||
|
|
||||||
export * from "./mysql-resource";
|
export * from "./mysql-resource";
|
||||||
export * from "./postgres-resource";
|
export * from "./postgres-resource";
|
||||||
|
export * from "./ssh-resource";
|
||||||
|
|
||||||
export type TPamResource = TPostgresResource | TMySQLResource;
|
export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource;
|
||||||
|
|
||||||
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount;
|
||||||
|
|
||||||
export type TPamFolder = {
|
export type TPamFolder = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
47
frontend/src/hooks/api/pam/types/ssh-resource.ts
Normal file
47
frontend/src/hooks/api/pam/types/ssh-resource.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { PamResourceType } from "../enums";
|
||||||
|
import { TBasePamAccount } from "./base-account";
|
||||||
|
import { TBasePamResource } from "./base-resource";
|
||||||
|
|
||||||
|
export enum SSHAuthMethod {
|
||||||
|
Password = "password",
|
||||||
|
PublicKey = "public-key",
|
||||||
|
Certificate = "certificate"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TSSHConnectionDetails = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSSHPasswordCredentials = {
|
||||||
|
authMethod: SSHAuthMethod.Password;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSSHPublicKeyCredentials = {
|
||||||
|
authMethod: SSHAuthMethod.PublicKey;
|
||||||
|
username: string;
|
||||||
|
privateKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSSHCertificateCredentials = {
|
||||||
|
authMethod: SSHAuthMethod.Certificate;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSSHCredentials =
|
||||||
|
| TSSHPasswordCredentials
|
||||||
|
| TSSHPublicKeyCredentials
|
||||||
|
| TSSHCertificateCredentials;
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export type TSSHResource = TBasePamResource & { resourceType: PamResourceType.SSH } & {
|
||||||
|
connectionDetails: TSSHConnectionDetails;
|
||||||
|
rotationAccountCredentials?: TSSHCredentials | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export type TSSHAccount = TBasePamAccount & {
|
||||||
|
credentials: TSSHCredentials;
|
||||||
|
};
|
||||||
@@ -58,15 +58,22 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
|||||||
return duration;
|
return duration;
|
||||||
}, [duration]);
|
}, [duration]);
|
||||||
|
|
||||||
const command = useMemo(
|
const command = useMemo(() => {
|
||||||
() =>
|
if (!account) return "";
|
||||||
account &&
|
|
||||||
(account.resource.resourceType === PamResourceType.Postgres ||
|
if (
|
||||||
account.resource.resourceType === PamResourceType.MySQL)
|
account.resource.resourceType === PamResourceType.Postgres ||
|
||||||
? `infisical pam db access-account ${account.id} --duration ${cliDuration}`
|
account.resource.resourceType === PamResourceType.MySQL
|
||||||
: "",
|
) {
|
||||||
[account, cliDuration]
|
return `infisical pam db access-account ${account.id} --duration ${cliDuration}`;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
if (account.resource.resourceType === PamResourceType.SSH) {
|
||||||
|
return `infisical pam ssh ${account.id} --duration ${cliDuration}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}, [account, cliDuration]);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { DiscriminativePick } from "@app/types";
|
|||||||
import { PamAccountHeader } from "../PamAccountHeader";
|
import { PamAccountHeader } from "../PamAccountHeader";
|
||||||
import { MySQLAccountForm } from "./MySQLAccountForm";
|
import { MySQLAccountForm } from "./MySQLAccountForm";
|
||||||
import { PostgresAccountForm } from "./PostgresAccountForm";
|
import { PostgresAccountForm } from "./PostgresAccountForm";
|
||||||
|
import { SSHAccountForm } from "./SSHAccountForm";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
onComplete: (account: TPamAccount) => void;
|
onComplete: (account: TPamAccount) => void;
|
||||||
@@ -65,6 +66,10 @@ const CreateForm = ({
|
|||||||
return (
|
return (
|
||||||
<MySQLAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
|
<MySQLAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
|
||||||
);
|
);
|
||||||
|
case PamResourceType.SSH:
|
||||||
|
return (
|
||||||
|
<SSHAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||||
}
|
}
|
||||||
@@ -90,9 +95,11 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
|||||||
|
|
||||||
switch (account.resource.resourceType) {
|
switch (account.resource.resourceType) {
|
||||||
case PamResourceType.Postgres:
|
case PamResourceType.Postgres:
|
||||||
return <PostgresAccountForm account={account} onSubmit={onSubmit} />;
|
return <PostgresAccountForm account={account as any} onSubmit={onSubmit} />;
|
||||||
case PamResourceType.MySQL:
|
case PamResourceType.MySQL:
|
||||||
return <MySQLAccountForm account={account} onSubmit={onSubmit} />;
|
return <MySQLAccountForm account={account as any} onSubmit={onSubmit} />;
|
||||||
|
case PamResourceType.SSH:
|
||||||
|
return <SSHAccountForm account={account as any} onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
|
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
|
import { PamResourceType, TSSHAccount } from "@app/hooks/api/pam";
|
||||||
|
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||||
|
import { SSHAuthMethod } from "@app/hooks/api/pam/types/ssh-resource";
|
||||||
|
|
||||||
|
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||||
|
import { BaseSshAccountSchema } from "./shared/ssh-account-schemas";
|
||||||
|
import { SshAccountFields } from "./shared/SshAccountFields";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
account?: TSSHAccount;
|
||||||
|
resourceId?: string;
|
||||||
|
resourceType?: PamResourceType;
|
||||||
|
onSubmit: (formData: FormData) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = genericAccountFieldsSchema.extend({
|
||||||
|
credentials: BaseSshAccountSchema,
|
||||||
|
// We don't support rotation for now, just feed a false value to
|
||||||
|
// make the schema happy
|
||||||
|
rotationEnabled: z.boolean().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const SSHAccountForm = ({ account, onSubmit }: Props) => {
|
||||||
|
const isUpdate = Boolean(account);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: account
|
||||||
|
? {
|
||||||
|
...account,
|
||||||
|
credentials:
|
||||||
|
account.credentials.authMethod === SSHAuthMethod.Password
|
||||||
|
? {
|
||||||
|
...account.credentials,
|
||||||
|
password: UNCHANGED_PASSWORD_SENTINEL
|
||||||
|
}
|
||||||
|
: account.credentials.authMethod === SSHAuthMethod.PublicKey
|
||||||
|
? {
|
||||||
|
...account.credentials,
|
||||||
|
privateKey: UNCHANGED_PASSWORD_SENTINEL
|
||||||
|
}
|
||||||
|
: account.credentials
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
credentials: {
|
||||||
|
authMethod: SSHAuthMethod.Password,
|
||||||
|
username: "",
|
||||||
|
password: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
handleSubmit(onSubmit)(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GenericAccountFields />
|
||||||
|
<SshAccountFields isUpdate={isUpdate} />
|
||||||
|
<div className="mt-6 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update Account" : "Create Account"}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
import { FormControl, Input, Select, SelectItem, TextArea } from "@app/components/v2";
|
||||||
|
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||||
|
import { SSHAuthMethod } from "@app/hooks/api/pam/types/ssh-resource";
|
||||||
|
|
||||||
|
export const SshAccountFields = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||||
|
const { control, setValue } = useFormContext();
|
||||||
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
|
||||||
|
const authMethod =
|
||||||
|
useWatch({ control, name: "credentials.authMethod" }) || SSHAuthMethod.Password;
|
||||||
|
const password = useWatch({ control, name: "credentials.password" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (password === UNCHANGED_PASSWORD_SENTINEL) {
|
||||||
|
setShowPassword(false);
|
||||||
|
}
|
||||||
|
}, [password]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
|
||||||
|
<Controller
|
||||||
|
name="credentials.authMethod"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-3"
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Authentication Method"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={value || SSHAuthMethod.Password}
|
||||||
|
onValueChange={(newAuthMethod) => {
|
||||||
|
onChange(newAuthMethod);
|
||||||
|
// Clear out credentials from other auth methods
|
||||||
|
setValue("credentials.password", undefined, { shouldDirty: true });
|
||||||
|
setValue("credentials.privateKey", undefined, { shouldDirty: true });
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<SelectItem value={SSHAuthMethod.Password}>Password</SelectItem>
|
||||||
|
<SelectItem value={SSHAuthMethod.PublicKey}>SSH Key</SelectItem>
|
||||||
|
<SelectItem value={SSHAuthMethod.Certificate}>Certificate</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="credentials.username"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-3"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Username"
|
||||||
|
>
|
||||||
|
<Input {...field} autoComplete="off" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{authMethod === SSHAuthMethod.Password && (
|
||||||
|
<Controller
|
||||||
|
name="credentials.password"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Password"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type={showPassword ? "text" : "password"}
|
||||||
|
autoComplete="new-password"
|
||||||
|
onFocus={() => {
|
||||||
|
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||||
|
field.onChange("");
|
||||||
|
}
|
||||||
|
setShowPassword(true);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (isUpdate && field.value === "") {
|
||||||
|
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||||
|
}
|
||||||
|
setShowPassword(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authMethod === SSHAuthMethod.PublicKey && (
|
||||||
|
<Controller
|
||||||
|
name="credentials.privateKey"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Private Key"
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
className="min-h-32 resize-y font-mono text-xs"
|
||||||
|
placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ... -----END OPENSSH PRIVATE KEY-----"
|
||||||
|
onFocus={() => {
|
||||||
|
if (isUpdate && field.value === UNCHANGED_PASSWORD_SENTINEL) {
|
||||||
|
field.onChange("");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
if (isUpdate && field.value === "") {
|
||||||
|
field.onChange(UNCHANGED_PASSWORD_SENTINEL);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{authMethod === SSHAuthMethod.Certificate && (
|
||||||
|
<p className="mb-0 text-xs text-mineshaft-400">
|
||||||
|
Certificate-based authentication will use the certificate configured on the SSH resource.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SSHAuthMethod } from "@app/hooks/api/pam/types/ssh-resource";
|
||||||
|
|
||||||
|
export const SSHPasswordCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Password),
|
||||||
|
username: z.string().trim().min(1, "Username is required"),
|
||||||
|
password: z.string().trim().min(1, "Password is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHPublicKeyCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.PublicKey),
|
||||||
|
username: z.string().trim().min(1, "Username is required"),
|
||||||
|
privateKey: z.string().trim().min(1, "Private key is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHCertificateCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Certificate),
|
||||||
|
username: z.string().trim().min(1, "Username is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseSshAccountSchema = z.discriminatedUnion("authMethod", [
|
||||||
|
SSHPasswordCredentialsSchema,
|
||||||
|
SSHPublicKeyCredentialsSchema,
|
||||||
|
SSHCertificateCredentialsSchema
|
||||||
|
]);
|
||||||
@@ -11,6 +11,7 @@ import { DiscriminativePick } from "@app/types";
|
|||||||
import { PamResourceHeader } from "../PamResourceHeader";
|
import { PamResourceHeader } from "../PamResourceHeader";
|
||||||
import { MySQLResourceForm } from "./MySQLResourceForm";
|
import { MySQLResourceForm } from "./MySQLResourceForm";
|
||||||
import { PostgresResourceForm } from "./PostgresResourceForm";
|
import { PostgresResourceForm } from "./PostgresResourceForm";
|
||||||
|
import { SSHResourceForm } from "./SSHResourceForm";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
onComplete: (resource: TPamResource) => void;
|
onComplete: (resource: TPamResource) => void;
|
||||||
@@ -51,6 +52,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) =>
|
|||||||
return <PostgresResourceForm onSubmit={onSubmit} />;
|
return <PostgresResourceForm onSubmit={onSubmit} />;
|
||||||
case PamResourceType.MySQL:
|
case PamResourceType.MySQL:
|
||||||
return <MySQLResourceForm onSubmit={onSubmit} />;
|
return <MySQLResourceForm onSubmit={onSubmit} />;
|
||||||
|
case PamResourceType.SSH:
|
||||||
|
return <SSHResourceForm onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||||
}
|
}
|
||||||
@@ -79,6 +82,8 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
|
|||||||
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
|
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||||
case PamResourceType.MySQL:
|
case PamResourceType.MySQL:
|
||||||
return <MySQLResourceForm resource={resource} onSubmit={onSubmit} />;
|
return <MySQLResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||||
|
case PamResourceType.SSH:
|
||||||
|
return <SSHResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${(resource as any).resourceType}`);
|
throw new Error(`Unhandled resource: ${(resource as any).resourceType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
|
import { PamResourceType, TSSHResource } from "@app/hooks/api/pam";
|
||||||
|
|
||||||
|
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
|
||||||
|
import { BaseSshConnectionDetailsSchema } from "./shared/ssh-resource-schemas";
|
||||||
|
import { SshResourceFields } from "./shared/SshResourceFields";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
resource?: TSSHResource;
|
||||||
|
onSubmit: (formData: FormData) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = genericResourceFieldsSchema.extend({
|
||||||
|
resourceType: z.literal(PamResourceType.SSH),
|
||||||
|
connectionDetails: BaseSshConnectionDetailsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const SSHResourceForm = ({ resource, onSubmit }: Props) => {
|
||||||
|
const isUpdate = Boolean(resource);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: resource ?? {
|
||||||
|
resourceType: PamResourceType.SSH,
|
||||||
|
connectionDetails: {
|
||||||
|
host: "",
|
||||||
|
port: 22
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<GenericResourceFields />
|
||||||
|
<SshResourceFields />
|
||||||
|
<div className="mt-6 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
colorSchema="secondary"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting || !isDirty}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update Details" : "Create Resource"}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { FormControl, Input } from "@app/components/v2";
|
||||||
|
|
||||||
|
export const SshResourceFields = () => {
|
||||||
|
const { control } = useFormContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-4 rounded-sm border border-mineshaft-600 bg-mineshaft-700/70 p-3">
|
||||||
|
<div className="mt-[0.675rem] flex items-start gap-2">
|
||||||
|
<Controller
|
||||||
|
name="connectionDetails.host"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="flex-1"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Host"
|
||||||
|
>
|
||||||
|
<Input placeholder="example.com or 192.168.1.1" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="connectionDetails.port"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="w-28"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
label="Port"
|
||||||
|
>
|
||||||
|
<Input type="number" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SSHAuthMethod } from "@app/hooks/api/pam/types/ssh-resource";
|
||||||
|
|
||||||
|
export const BaseSshConnectionDetailsSchema = z.object({
|
||||||
|
host: z.string().trim().min(1, "Host is required"),
|
||||||
|
port: z.number().int().min(1).max(65535)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHPasswordCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Password),
|
||||||
|
username: z.string().trim().min(1, "Username is required"),
|
||||||
|
password: z.string().trim().min(1, "Password is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHPublicKeyCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.PublicKey),
|
||||||
|
username: z.string().trim().min(1, "Username is required"),
|
||||||
|
privateKey: z.string().trim().min(1, "Private key is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SSHCertificateCredentialsSchema = z.object({
|
||||||
|
authMethod: z.literal(SSHAuthMethod.Certificate),
|
||||||
|
username: z.string().trim().min(1, "Username is required")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BaseSshAccountSchema = z.discriminatedUnion("authMethod", [
|
||||||
|
SSHPasswordCredentialsSchema,
|
||||||
|
SSHPublicKeyCredentialsSchema,
|
||||||
|
SSHCertificateCredentialsSchema
|
||||||
|
]);
|
||||||
@@ -78,7 +78,6 @@ export const ResourceTypeSelect = ({ onSelect }: Props) => {
|
|||||||
// We temporarily show a special license modal for these because we will have to write some code to complete the integration
|
// We temporarily show a special license modal for these because we will have to write some code to complete the integration
|
||||||
if (
|
if (
|
||||||
resource === PamResourceType.RDP ||
|
resource === PamResourceType.RDP ||
|
||||||
resource === PamResourceType.SSH ||
|
|
||||||
resource === PamResourceType.Kubernetes ||
|
resource === PamResourceType.Kubernetes ||
|
||||||
resource === PamResourceType.MCP ||
|
resource === PamResourceType.MCP ||
|
||||||
resource === PamResourceType.Redis ||
|
resource === PamResourceType.Redis ||
|
||||||
|
|||||||
Reference in New Issue
Block a user