mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 15:27:27 +00:00
Merge pull request #4655 from Infisical/ENG-3913-add-mysql-pam
[ENG-3913] Add mysql pam
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
CreateMySQLAccountSchema,
|
||||||
|
SanitizedMySQLAccountWithResourceSchema,
|
||||||
|
UpdateMySQLAccountSchema
|
||||||
|
} 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 {
|
import {
|
||||||
CreatePostgresAccountSchema,
|
CreatePostgresAccountSchema,
|
||||||
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
|
|||||||
createAccountSchema: CreatePostgresAccountSchema,
|
createAccountSchema: CreatePostgresAccountSchema,
|
||||||
updateAccountSchema: UpdatePostgresAccountSchema
|
updateAccountSchema: UpdatePostgresAccountSchema
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||||
|
registerPamResourceEndpoints({
|
||||||
|
server,
|
||||||
|
resourceType: PamResource.MySQL,
|
||||||
|
accountResponseSchema: SanitizedMySQLAccountWithResourceSchema,
|
||||||
|
createAccountSchema: CreateMySQLAccountSchema,
|
||||||
|
updateAccountSchema: UpdateMySQLAccountSchema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { PamFoldersSchema } from "@app/db/schemas";
|
import { PamFoldersSchema } 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 { 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 { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
@@ -10,8 +11,10 @@ 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";
|
||||||
|
|
||||||
// Use z.union([...]) when more resources are added
|
const SanitizedAccountSchema = z.union([
|
||||||
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
|
SanitizedPostgresAccountWithResourceSchema,
|
||||||
|
SanitizedMySQLAccountWithResourceSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -4,6 +4,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 {
|
||||||
|
CreateMySQLResourceSchema,
|
||||||
|
MySQLResourceSchema,
|
||||||
|
UpdateMySQLResourceSchema
|
||||||
|
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
|
|
||||||
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
import { registerPamResourceEndpoints } from "./pam-resource-endpoints";
|
||||||
|
|
||||||
@@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
|
|||||||
createResourceSchema: CreatePostgresResourceSchema,
|
createResourceSchema: CreatePostgresResourceSchema,
|
||||||
updateResourceSchema: UpdatePostgresResourceSchema
|
updateResourceSchema: UpdatePostgresResourceSchema
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
|
||||||
|
registerPamResourceEndpoints({
|
||||||
|
server,
|
||||||
|
resourceType: PamResource.MySQL,
|
||||||
|
resourceResponseSchema: MySQLResourceSchema,
|
||||||
|
createResourceSchema: CreateMySQLResourceSchema,
|
||||||
|
updateResourceSchema: UpdateMySQLResourceSchema
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import {
|
||||||
|
MySQLResourceListItemSchema,
|
||||||
|
SanitizedMySQLResourceSchema
|
||||||
|
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
|
||||||
import {
|
import {
|
||||||
PostgresResourceListItemSchema,
|
PostgresResourceListItemSchema,
|
||||||
SanitizedPostgresResourceSchema
|
SanitizedPostgresResourceSchema
|
||||||
@@ -9,10 +13,12 @@ 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";
|
||||||
|
|
||||||
// Use z.union([...]) when more resources are added
|
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
|
||||||
const SanitizedResourceSchema = SanitizedPostgresResourceSchema;
|
|
||||||
|
|
||||||
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
|
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
|
||||||
|
PostgresResourceListItemSchema,
|
||||||
|
MySQLResourceListItemSchema
|
||||||
|
]);
|
||||||
|
|
||||||
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { PamSessionsSchema } from "@app/db/schemas";
|
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 { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
|
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-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";
|
||||||
|
|
||||||
// Use z.union([]) once there's multiple
|
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
|
||||||
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { MySQLResourceListItemSchema } from "./mysql-resource-schemas";
|
||||||
|
|
||||||
|
export const getMySQLResourceListItem = () => {
|
||||||
|
return {
|
||||||
|
name: MySQLResourceListItemSchema.shape.name.value,
|
||||||
|
resource: MySQLResourceListItemSchema.shape.resource.value
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { PamResource } from "../pam-resource-enums";
|
||||||
|
import {
|
||||||
|
BaseCreatePamAccountSchema,
|
||||||
|
BaseCreatePamResourceSchema,
|
||||||
|
BasePamAccountSchema,
|
||||||
|
BasePamAccountSchemaWithResource,
|
||||||
|
BasePamResourceSchema,
|
||||||
|
BaseUpdatePamAccountSchema,
|
||||||
|
BaseUpdatePamResourceSchema
|
||||||
|
} from "../pam-resource-schemas";
|
||||||
|
import {
|
||||||
|
BaseSqlAccountCredentialsSchema,
|
||||||
|
BaseSqlResourceConnectionDetailsSchema
|
||||||
|
} from "../shared/sql/sql-resource-schemas";
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({
|
||||||
|
// MySQL db in many cases the db will not be provided when making connection
|
||||||
|
database: z.string().trim()
|
||||||
|
});
|
||||||
|
export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
|
||||||
|
|
||||||
|
const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) });
|
||||||
|
|
||||||
|
export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({
|
||||||
|
username: true
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MySQLResourceListItemSchema = z.object({
|
||||||
|
name: z.literal("MySQL"),
|
||||||
|
resource: z.literal(PamResource.MySQL)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema,
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
|
||||||
|
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
|
||||||
|
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export const MySQLAccountSchema = BasePamAccountSchema.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
|
||||||
|
credentials: MySQLAccountCredentialsSchema.pick({
|
||||||
|
username: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sessions
|
||||||
|
export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
MySQLAccountCredentialsSchema,
|
||||||
|
MySQLAccountSchema,
|
||||||
|
MySQLResourceConnectionDetailsSchema,
|
||||||
|
MySQLResourceSchema
|
||||||
|
} from "./mysql-resource-schemas";
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export type TMySQLResource = z.infer<typeof MySQLResourceSchema>;
|
||||||
|
export type TMySQLResourceConnectionDetails = z.infer<typeof MySQLResourceConnectionDetailsSchema>;
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export type TMySQLAccount = z.infer<typeof MySQLAccountSchema>;
|
||||||
|
export type TMySQLAccountCredentials = z.infer<typeof MySQLAccountCredentialsSchema>;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export enum PamResource {
|
export enum PamResource {
|
||||||
Postgres = "postgres"
|
Postgres = "postgres",
|
||||||
|
MySQL = "mysql"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
|
|
||||||
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
|
||||||
|
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
|
||||||
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
|
||||||
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
|
||||||
|
|
||||||
export const listResourceOptions = () => {
|
export const listResourceOptions = () => {
|
||||||
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Resource
|
// Resource
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
|
||||||
|
import {
|
||||||
|
TMySQLAccount,
|
||||||
|
TMySQLAccountCredentials,
|
||||||
|
TMySQLResource,
|
||||||
|
TMySQLResourceConnectionDetails
|
||||||
|
} from "./mysql/mysql-resource-types";
|
||||||
import { PamResource } from "./pam-resource-enums";
|
import { PamResource } from "./pam-resource-enums";
|
||||||
import {
|
import {
|
||||||
TPostgresAccount,
|
TPostgresAccount,
|
||||||
@@ -8,12 +14,13 @@ import {
|
|||||||
} from "./postgres/postgres-resource-types";
|
} from "./postgres/postgres-resource-types";
|
||||||
|
|
||||||
// Resource types
|
// Resource types
|
||||||
export type TPamResource = TPostgresResource;
|
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||||
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||||
|
|
||||||
// Account types
|
// Account types
|
||||||
export type TPamAccount = TPostgresAccount;
|
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||||
export type TPamAccountCredentials = TPostgresAccountCredentials;
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
|
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||||
|
|
||||||
// Resource DTOs
|
// Resource DTOs
|
||||||
export type TCreateResourceDTO = Pick<
|
export type TCreateResourceDTO = Pick<
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import knex, { Knex } from "knex";
|
import knex from "knex";
|
||||||
|
import mysql, { Connection } from "mysql2/promise";
|
||||||
|
import * as pg from "pg";
|
||||||
import tls, { PeerCertificate } from "tls";
|
import tls, { PeerCertificate } from "tls";
|
||||||
|
|
||||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||||
@@ -20,30 +22,160 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
|||||||
|
|
||||||
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
|
||||||
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
|
||||||
|
const SIMPLE_QUERY = "select 1";
|
||||||
|
|
||||||
const SQL_CONNECTION_CLIENT_MAP = {
|
export interface SqlResourceConnection {
|
||||||
[PamResource.Postgres]: "pg"
|
/**
|
||||||
};
|
* Check and see if the connection is good or not.
|
||||||
|
*
|
||||||
|
* @param connectOnly when true, if we only want to know that making the connection is possible or not,
|
||||||
|
* we don't care about authentication failures
|
||||||
|
* @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked
|
||||||
|
*/
|
||||||
|
validate: (connectOnly: boolean) => Promise<void>;
|
||||||
|
|
||||||
const getConnectionConfig = (
|
/**
|
||||||
resourceType: PamResource,
|
* Rotate password and return the new credentials.
|
||||||
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
|
*
|
||||||
) => {
|
* @param currentCredentials the current credentials to rotate
|
||||||
switch (resourceType) {
|
*
|
||||||
|
* @returns Promise to be resolved with the new credentials
|
||||||
|
*/
|
||||||
|
rotateCredentials: (currentCredentials: TSqlAccountCredentials) => Promise<TSqlAccountCredentials>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the connection.
|
||||||
|
*
|
||||||
|
* @returns Promise for closing the connection
|
||||||
|
*/
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeSqlConnection = (
|
||||||
|
proxyPort: number,
|
||||||
|
config: {
|
||||||
|
connectionDetails: TSqlResourceConnectionDetails;
|
||||||
|
resourceType: PamResource;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
}
|
||||||
|
): SqlResourceConnection => {
|
||||||
|
const { connectionDetails, resourceType, username, password } = config;
|
||||||
|
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
|
||||||
|
const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback
|
||||||
|
const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback
|
||||||
|
switch (config.resourceType) {
|
||||||
case PamResource.Postgres: {
|
case PamResource.Postgres: {
|
||||||
|
const client = knex({
|
||||||
|
client: "pg",
|
||||||
|
connection: {
|
||||||
|
host: "localhost",
|
||||||
|
port: proxyPort,
|
||||||
|
user: actualUsername,
|
||||||
|
password: actualPassword,
|
||||||
|
database: connectionDetails.database,
|
||||||
|
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||||
|
ssl: sslEnabled
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: sslRejectUnauthorized,
|
||||||
|
ca: sslCertificate,
|
||||||
|
servername: host,
|
||||||
|
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
||||||
|
// but validate the certificate against the actual hostname
|
||||||
|
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
||||||
|
return tls.checkServerIdentity(host, cert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: false
|
||||||
|
}
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
ssl: sslEnabled
|
validate: async (connectOnly) => {
|
||||||
? {
|
try {
|
||||||
rejectUnauthorized: sslRejectUnauthorized,
|
await client.raw(SIMPLE_QUERY);
|
||||||
ca: sslCertificate,
|
} catch (error) {
|
||||||
servername: host,
|
if (error instanceof pg.DatabaseError) {
|
||||||
// When using proxy, we need to bypass hostname validation since we connect to localhost
|
// Hacky way to know if we successfully hit the database.
|
||||||
// but validate the certificate against the actual hostname
|
// TODO: potentially two approaches to solve the problem.
|
||||||
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
|
// 1. change the work flow, add account first then resource
|
||||||
return tls.checkServerIdentity(host, cert);
|
// 2. modify relay to add a new endpoint for returning if the target host is healthy or not
|
||||||
|
// (like being able to do an auth handshake regardless pass or not)
|
||||||
|
if (
|
||||||
|
connectOnly &&
|
||||||
|
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
|
||||||
|
error.message.includes("no pg_hba.conf entry for host"))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: false
|
throw new BadRequestError({
|
||||||
|
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rotateCredentials: async (currentCredentials) => {
|
||||||
|
const newPassword = alphaNumericNanoId(32);
|
||||||
|
// Note: The generated random password is not really going to make SQL Injection possible.
|
||||||
|
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
|
||||||
|
// parameters binding is not supported. But just in case if the this code got copied
|
||||||
|
// around and repurposed, let's just do some naive escaping regardless
|
||||||
|
await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, {
|
||||||
|
username: currentCredentials.username
|
||||||
|
});
|
||||||
|
return { username: currentCredentials.username, password: newPassword };
|
||||||
|
},
|
||||||
|
close: () => client.destroy()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case PamResource.MySQL: {
|
||||||
|
return {
|
||||||
|
validate: async (connectOnly) => {
|
||||||
|
let client: Connection | null = null;
|
||||||
|
try {
|
||||||
|
// Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex.
|
||||||
|
// mysql2 doesn't provide custom ssl verification function pass in.
|
||||||
|
// ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362
|
||||||
|
// and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug:
|
||||||
|
// ref: https://github.com/knex/knex/issues/5352
|
||||||
|
// It appears that using Knex causing more troubles than not, we are just checking the connections,
|
||||||
|
// so it's much easier to create raw connection with the driver lib directly
|
||||||
|
client = await mysql.createConnection({
|
||||||
|
host: "localhost",
|
||||||
|
port: proxyPort,
|
||||||
|
user: actualUsername, // Use provided username or fallback
|
||||||
|
password: actualPassword, // Use provided password or fallback
|
||||||
|
database: connectionDetails.database,
|
||||||
|
ssl: sslEnabled
|
||||||
|
? {
|
||||||
|
rejectUnauthorized: sslRejectUnauthorized,
|
||||||
|
ca: sslCertificate
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
await client.query(SIMPLE_QUERY);
|
||||||
|
} catch (error) {
|
||||||
|
if (connectOnly) {
|
||||||
|
// Hacky way to know if we successfully hit the database.
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: handle other errors, and throw standardlized errors providing user-friendly msg
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await client?.end();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rotateCredentials: async () => {
|
||||||
|
// TODO: the pwd rotation for MySQL is not supported yet
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unsupported operation"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
close: async () => {}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -62,10 +194,9 @@ export const executeWithGateway = async <T>(
|
|||||||
password?: string;
|
password?: string;
|
||||||
},
|
},
|
||||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
|
||||||
operation: (client: Knex) => Promise<T>
|
operation: (connection: SqlResourceConnection) => Promise<T>
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
const { connectionDetails, resourceType, gatewayId, username, password } = config;
|
const { connectionDetails, gatewayId } = config;
|
||||||
|
|
||||||
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
|
||||||
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
|
||||||
gatewayId,
|
gatewayId,
|
||||||
@@ -79,22 +210,11 @@ export const executeWithGateway = async <T>(
|
|||||||
|
|
||||||
return withGatewayV2Proxy(
|
return withGatewayV2Proxy(
|
||||||
async (proxyPort) => {
|
async (proxyPort) => {
|
||||||
const client = knex({
|
const connection = makeSqlConnection(proxyPort, config);
|
||||||
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
|
|
||||||
connection: {
|
|
||||||
database: connectionDetails.database,
|
|
||||||
port: proxyPort,
|
|
||||||
host: "localhost",
|
|
||||||
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
|
|
||||||
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
|
|
||||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
...getConnectionConfig(resourceType, connectionDetails)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
try {
|
try {
|
||||||
return await operation(client);
|
return await operation(connection);
|
||||||
} finally {
|
} finally {
|
||||||
await client.destroy();
|
await connection.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -115,25 +235,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
const validateConnection = async () => {
|
const validateConnection = async () => {
|
||||||
try {
|
try {
|
||||||
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
|
||||||
await client.raw("Select 1");
|
await client.validate(true);
|
||||||
});
|
});
|
||||||
return connectionDetails;
|
return connectionDetails;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Hacky way to know if we successfully hit the database
|
if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
|
||||||
if (error instanceof BadRequestError) {
|
throw new BadRequestError({
|
||||||
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
|
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
||||||
return connectionDetails;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message.includes("no pg_hba.conf entry for host")) {
|
|
||||||
return connectionDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message === "Connection terminated unexpectedly") {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Connection terminated unexpectedly. Verify that host and port are correct"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -156,11 +265,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
},
|
},
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
async (client) => {
|
async (client) => {
|
||||||
await client.raw("Select 1");
|
await client.validate(false);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return credentials;
|
return credentials;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// TODO: extract these logic into each SQL connection
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
if (error.message === `password authentication failed for user "${credentials.username}"`) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -186,9 +296,7 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
currentCredentials
|
currentCredentials
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const newPassword = alphaNumericNanoId(32);
|
return await executeWithGateway(
|
||||||
|
|
||||||
await executeWithGateway(
|
|
||||||
{
|
{
|
||||||
connectionDetails,
|
connectionDetails,
|
||||||
gatewayId,
|
gatewayId,
|
||||||
@@ -197,20 +305,8 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
|
|||||||
password: rotationAccountCredentials.password
|
password: rotationAccountCredentials.password
|
||||||
},
|
},
|
||||||
gatewayV2Service,
|
gatewayV2Service,
|
||||||
async (client) => {
|
(client) => client.rotateCredentials(currentCredentials)
|
||||||
switch (resourceType) {
|
|
||||||
case PamResource.Postgres:
|
|
||||||
await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: `Password rotation for ${resourceType as PamResource} is not supported.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return { username: currentCredentials.username, password: newPassword };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
|
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types";
|
||||||
import {
|
import {
|
||||||
TPostgresAccountCredentials,
|
TPostgresAccountCredentials,
|
||||||
TPostgresResourceConnectionDetails
|
TPostgresResourceConnectionDetails
|
||||||
} from "../../postgres/postgres-resource-types";
|
} from "../../postgres/postgres-resource-types";
|
||||||
|
|
||||||
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
|
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
|
||||||
export type TSqlAccountCredentials = TPostgresAccountCredentials;
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
|
export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export enum PamResourceType {
|
export enum PamResourceType {
|
||||||
Postgres = "postgres",
|
Postgres = "postgres",
|
||||||
|
MySQL = "mysql",
|
||||||
RDP = "rdp",
|
RDP = "rdp",
|
||||||
SSH = "ssh",
|
SSH = "ssh",
|
||||||
Kubernetes = "kubernetes"
|
Kubernetes = "kubernetes"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const PAM_RESOURCE_TYPE_MAP: Record<
|
|||||||
{ name: string; image: string; size?: number }
|
{ name: string; image: string; size?: number }
|
||||||
> = {
|
> = {
|
||||||
[PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
|
[PamResourceType.Postgres]: { name: "PostgreSQL", image: "Postgres.png" },
|
||||||
|
[PamResourceType.MySQL]: { name: "MySQL", image: "MySql.png" },
|
||||||
[PamResourceType.RDP]: { name: "RDP", image: "RDP.png" },
|
[PamResourceType.RDP]: { name: "RDP", image: "RDP.png" },
|
||||||
[PamResourceType.SSH]: { name: "SSH", image: "SSH.png" },
|
[PamResourceType.SSH]: { name: "SSH", image: "SSH.png" },
|
||||||
[PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" }
|
[PamResourceType.Kubernetes]: { name: "Kubernetes", image: "Kubernetes.png" }
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { PamResourceType, PamSessionStatus } from "../enums";
|
import { PamResourceType, PamSessionStatus } from "../enums";
|
||||||
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
import { TPostgresAccount, TPostgresResource } from "./postgres-resource";
|
||||||
|
import { TMySQLAccount, TMySQLResource } from "./mysql-resource";
|
||||||
|
|
||||||
export * from "./postgres-resource";
|
export * from "./postgres-resource";
|
||||||
|
export * from "./mysql-resource";
|
||||||
|
|
||||||
export type TPamResource = TPostgresResource;
|
export type TPamResource = TPostgresResource | TMySQLResource;
|
||||||
|
|
||||||
export type TPamAccount = TPostgresAccount;
|
export type TPamAccount = TPostgresAccount | TMySQLAccount;
|
||||||
|
|
||||||
export type TPamFolder = {
|
export type TPamFolder = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { PamResourceType } from "../enums";
|
||||||
|
import { TBaseSqlConnectionDetails, TBaseSqlCredentials } from "./shared/sql-resource";
|
||||||
|
import { TBasePamAccount } from "./base-account";
|
||||||
|
import { TBasePamResource } from "./base-resource";
|
||||||
|
|
||||||
|
// Resources
|
||||||
|
export type TMySQLResource = TBasePamResource & { resourceType: PamResourceType.MySQL } & {
|
||||||
|
connectionDetails: TBaseSqlConnectionDetails;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accounts
|
||||||
|
export type TMySQLAccount = TBasePamAccount & {
|
||||||
|
credentials: TBaseSqlCredentials;
|
||||||
|
};
|
||||||
@@ -60,7 +60,9 @@ export const PamAccessAccountModal = ({ isOpen, onOpenChange, account }: Props)
|
|||||||
|
|
||||||
const command = useMemo(
|
const command = useMemo(
|
||||||
() =>
|
() =>
|
||||||
account && account.resource.resourceType === PamResourceType.Postgres
|
account &&
|
||||||
|
(account.resource.resourceType === PamResourceType.Postgres ||
|
||||||
|
account.resource.resourceType === PamResourceType.MySQL)
|
||||||
? `infisical pam db access-account ${account.id} --duration ${cliDuration}`
|
? `infisical pam db access-account ${account.id} --duration ${cliDuration}`
|
||||||
: "",
|
: "",
|
||||||
[account, cliDuration]
|
[account, cliDuration]
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
|
import { PamResourceType, TMySQLAccount } from "@app/hooks/api/pam";
|
||||||
|
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||||
|
|
||||||
|
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||||
|
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||||
|
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
account?: TMySQLAccount;
|
||||||
|
resourceId?: string;
|
||||||
|
resourceType?: PamResourceType;
|
||||||
|
onSubmit: (formData: FormData) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = genericAccountFieldsSchema.extend({
|
||||||
|
credentials: BaseSqlAccountSchema,
|
||||||
|
// 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 MySQLAccountForm = ({ account, onSubmit }: Props) => {
|
||||||
|
const isUpdate = Boolean(account);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: account
|
||||||
|
? {
|
||||||
|
...account,
|
||||||
|
credentials: {
|
||||||
|
...account.credentials,
|
||||||
|
password: UNCHANGED_PASSWORD_SENTINEL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
handleSubmit(onSubmit)(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GenericAccountFields />
|
||||||
|
<SqlAccountFields 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { DiscriminativePick } from "@app/types";
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
import { PamAccountHeader } from "../PamAccountHeader";
|
import { PamAccountHeader } from "../PamAccountHeader";
|
||||||
|
import { MySQLAccountForm } from "./MySQLAccountForm";
|
||||||
import { PostgresAccountForm } from "./PostgresAccountForm";
|
import { PostgresAccountForm } from "./PostgresAccountForm";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
@@ -35,10 +36,7 @@ const CreateForm = ({
|
|||||||
const createPamAccount = useCreatePamAccount();
|
const createPamAccount = useCreatePamAccount();
|
||||||
|
|
||||||
const onSubmit = async (
|
const onSubmit = async (
|
||||||
formData: DiscriminativePick<
|
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
|
||||||
TPamAccount,
|
|
||||||
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
|
|
||||||
>
|
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const account = await createPamAccount.mutateAsync({
|
const account = await createPamAccount.mutateAsync({
|
||||||
@@ -72,6 +70,10 @@ const CreateForm = ({
|
|||||||
resourceType={resourceType}
|
resourceType={resourceType}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case PamResourceType.MySQL:
|
||||||
|
return (
|
||||||
|
<MySQLAccountForm onSubmit={onSubmit} resourceId={resourceId} resourceType={resourceType} />
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||||
}
|
}
|
||||||
@@ -81,10 +83,7 @@ const UpdateForm = ({ account, onComplete }: UpdateFormProps) => {
|
|||||||
const updatePamAccount = useUpdatePamAccount();
|
const updatePamAccount = useUpdatePamAccount();
|
||||||
|
|
||||||
const onSubmit = async (
|
const onSubmit = async (
|
||||||
formData: DiscriminativePick<
|
formData: DiscriminativePick<TPamAccount, "name" | "description" | "credentials">
|
||||||
TPamAccount,
|
|
||||||
"name" | "description" | "credentials" | "rotationEnabled" | "rotationIntervalSeconds"
|
|
||||||
>
|
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const updatedAccount = await updatePamAccount.mutateAsync({
|
const updatedAccount = await updatePamAccount.mutateAsync({
|
||||||
@@ -110,6 +109,8 @@ 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} onSubmit={onSubmit} />;
|
||||||
|
case PamResourceType.MySQL:
|
||||||
|
return <MySQLAccountForm account={account} onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
|
throw new Error(`Unhandled resource: ${account.resource.resourceType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -1,16 +1,21 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { Button, ModalClose } from "@app/components/v2";
|
import { Button, ModalClose } from "@app/components/v2";
|
||||||
import { PamResourceType, TPostgresAccount, useGetPamResourceById } from "@app/hooks/api/pam";
|
import {
|
||||||
|
PamResourceType,
|
||||||
|
TPostgresAccount,
|
||||||
|
TPostgresResource,
|
||||||
|
useGetPamResourceById
|
||||||
|
} from "@app/hooks/api/pam";
|
||||||
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
import { UNCHANGED_PASSWORD_SENTINEL } from "@app/hooks/api/pam/constants";
|
||||||
|
|
||||||
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
|
||||||
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
|
||||||
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
import { GenericAccountFields, genericAccountFieldsSchema } from "./GenericAccountFields";
|
||||||
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
|
import { RotateAccountFields, rotateAccountFieldsSchema } from "./RotateAccountFields";
|
||||||
|
import { BaseSqlAccountSchema } from "./shared/sql-account-schemas";
|
||||||
|
import { SqlAccountFields } from "./shared/SqlAccountFields";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
account?: TPostgresAccount;
|
account?: TPostgresAccount;
|
||||||
@@ -56,7 +61,9 @@ export const PostgresAccountForm = ({ account, resourceId, resourceType, onSubmi
|
|||||||
if (account) {
|
if (account) {
|
||||||
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
|
setRotationCredentialsConfigured(account.resource.rotationCredentialsConfigured);
|
||||||
} else {
|
} else {
|
||||||
setRotationCredentialsConfigured(!!resource?.rotationAccountCredentials);
|
setRotationCredentialsConfigured(
|
||||||
|
!!(resource as TPostgresResource)?.rotationAccountCredentials
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [account, resource]);
|
}, [account, resource]);
|
||||||
|
|
||||||
|
|||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
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, TMySQLResource } from "@app/hooks/api/pam";
|
||||||
|
|
||||||
|
import { BaseSqlResourceSchema } from "./shared/sql-resource-schemas";
|
||||||
|
import { SqlResourceFields } from "./shared/SqlResourceFields";
|
||||||
|
import { GenericResourceFields, genericResourceFieldsSchema } from "./GenericResourceFields";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
resource?: TMySQLResource;
|
||||||
|
onSubmit: (formData: FormData) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = genericResourceFieldsSchema.extend({
|
||||||
|
resourceType: z.literal(PamResourceType.MySQL),
|
||||||
|
connectionDetails: BaseSqlResourceSchema.extend({
|
||||||
|
database: z.string().trim().optional().default("")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
export const MySQLResourceForm = ({ resource, onSubmit }: Props) => {
|
||||||
|
const isUpdate = Boolean(resource);
|
||||||
|
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||||
|
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: resource ?? {
|
||||||
|
resourceType: PamResourceType.MySQL,
|
||||||
|
connectionDetails: {
|
||||||
|
host: "",
|
||||||
|
port: 3306,
|
||||||
|
database: "",
|
||||||
|
sslEnabled: true,
|
||||||
|
sslRejectUnauthorized: true,
|
||||||
|
sslCertificate: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isDirty }
|
||||||
|
} = form;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
setSelectedTabIndex(0);
|
||||||
|
handleSubmit(onSubmit)(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<GenericResourceFields />
|
||||||
|
<SqlResourceFields
|
||||||
|
selectedTabIndex={selectedTabIndex}
|
||||||
|
setSelectedTabIndex={setSelectedTabIndex}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+6
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
import { DiscriminativePick } from "@app/types";
|
import { DiscriminativePick } from "@app/types";
|
||||||
|
|
||||||
import { PamResourceHeader } from "../PamResourceHeader";
|
import { PamResourceHeader } from "../PamResourceHeader";
|
||||||
|
import { MySQLResourceForm } from "./MySQLResourceForm";
|
||||||
import { PostgresResourceForm } from "./PostgresResourceForm";
|
import { PostgresResourceForm } from "./PostgresResourceForm";
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
@@ -57,6 +58,8 @@ const CreateForm = ({ resourceType, onComplete, projectId }: CreateFormProps) =>
|
|||||||
switch (resourceType) {
|
switch (resourceType) {
|
||||||
case PamResourceType.Postgres:
|
case PamResourceType.Postgres:
|
||||||
return <PostgresResourceForm onSubmit={onSubmit} />;
|
return <PostgresResourceForm onSubmit={onSubmit} />;
|
||||||
|
case PamResourceType.MySQL:
|
||||||
|
return <MySQLResourceForm onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${resourceType}`);
|
throw new Error(`Unhandled resource: ${resourceType}`);
|
||||||
}
|
}
|
||||||
@@ -92,8 +95,10 @@ const UpdateForm = ({ resource, onComplete }: UpdateFormProps) => {
|
|||||||
switch (resource.resourceType) {
|
switch (resource.resourceType) {
|
||||||
case PamResourceType.Postgres:
|
case PamResourceType.Postgres:
|
||||||
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
|
return <PostgresResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||||
|
case PamResourceType.MySQL:
|
||||||
|
return <MySQLResourceForm resource={resource} onSubmit={onSubmit} />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled resource: ${resource.resourceType}`);
|
throw new Error(`Unhandled resource: ${(resource as any).resourceType}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ export const SqlResourceFields = ({ setSelectedTabIndex, selectedTabIndex }: Pro
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
isError={Boolean(error?.message)}
|
isError={Boolean(error?.message)}
|
||||||
className={sslEnabled ? "" : "opacity-50"}
|
className={sslEnabled ? "" : "opacity-50"}
|
||||||
label="SSL Certificate"
|
label="Trusted CA SSL Certificate"
|
||||||
isOptional
|
isOptional
|
||||||
>
|
>
|
||||||
<TextArea className="h-14 resize-none!" {...field} isDisabled={!sslEnabled} />
|
<TextArea className="h-14 resize-none!" {...field} isDisabled={!sslEnabled} />
|
||||||
|
|||||||
Reference in New Issue
Block a user