mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 18:41:21 +00:00
Merge branch 'main' into feat/vercelSecretSyncIntegration
This commit is contained in:
@@ -6,7 +6,9 @@ export enum AppConnection {
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Humanitec = "humanitec",
|
||||
Vercel = "vercel"
|
||||
Vercel = "vercel",
|
||||
Postgres = "postgres",
|
||||
MsSql = "mssql"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
import { TAppConnections } from "@app/db/schemas/app-connections";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/app-connection-service";
|
||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
getAwsConnectionListItem,
|
||||
validateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
validateDatabricksConnectionCredentials
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import {
|
||||
GcpConnectionMethod,
|
||||
getGcpConnectionListItem,
|
||||
validateGcpConnectionCredentials
|
||||
} from "@app/services/app-connection/gcp";
|
||||
import {
|
||||
getGitHubConnectionListItem,
|
||||
GitHubConnectionMethod,
|
||||
validateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
transferSqlConnectionCredentialsToPlatform,
|
||||
validateSqlConnectionCredentials
|
||||
} from "@app/services/app-connection/shared/sql";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
import { TAppConnectionServiceFactoryDep } from "./app-connection-service";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TAppConnectionCredentialsValidator,
|
||||
TAppConnectionTransitionCredentialsToPlatform
|
||||
} from "./app-connection-types";
|
||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
getAzureAppConfigurationConnectionListItem,
|
||||
@@ -35,11 +27,20 @@ import {
|
||||
getAzureKeyVaultConnectionListItem,
|
||||
validateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
validateDatabricksConnectionCredentials
|
||||
} from "./databricks";
|
||||
import { GcpConnectionMethod, getGcpConnectionListItem, validateGcpConnectionCredentials } from "./gcp";
|
||||
import { getGitHubConnectionListItem, GitHubConnectionMethod, validateGitHubConnectionCredentials } from "./github";
|
||||
import {
|
||||
getHumanitecConnectionListItem,
|
||||
HumanitecConnectionMethod,
|
||||
validateHumanitecConnectionCredentials
|
||||
} from "./humanitec";
|
||||
import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql";
|
||||
import { getPostgresConnectionListItem, PostgresConnectionMethod } from "./postgres";
|
||||
import { VercelConnectionMethod } from "./vercel";
|
||||
import { getVercelConnectionListItem, validateVercelConnectionCredentials } from "./vercel/vercel-connection-fns";
|
||||
|
||||
@@ -52,7 +53,9 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getVercelConnectionListItem()
|
||||
getVercelConnectionListItem(),
|
||||
getPostgresConnectionListItem(),
|
||||
getMsSqlConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
@@ -98,32 +101,23 @@ export const decryptAppConnectionCredentials = async ({
|
||||
return JSON.parse(decryptedPlainTextBlob.toString()) as TAppConnection["credentials"];
|
||||
};
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TAppConnectionCredentialsValidator> = {
|
||||
[AppConnection.AWS]: validateAwsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Databricks]: validateDatabricksConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GitHub]: validateGitHubConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.GCP]: validateGcpConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureKeyVault]: validateAzureKeyVaultConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureAppConfiguration]:
|
||||
validateAzureAppConfigurationConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Vercel]: validateVercelConnectionCredentials as TAppConnectionCredentialsValidator
|
||||
};
|
||||
|
||||
export const validateAppConnectionCredentials = async (
|
||||
appConnection: TAppConnectionConfig
|
||||
): Promise<TAppConnection["credentials"]> => {
|
||||
const { app } = appConnection;
|
||||
switch (app) {
|
||||
case AppConnection.AWS:
|
||||
return validateAwsConnectionCredentials(appConnection);
|
||||
case AppConnection.Databricks:
|
||||
return validateDatabricksConnectionCredentials(appConnection);
|
||||
case AppConnection.GitHub:
|
||||
return validateGitHubConnectionCredentials(appConnection);
|
||||
case AppConnection.GCP:
|
||||
return validateGcpConnectionCredentials(appConnection);
|
||||
case AppConnection.AzureKeyVault:
|
||||
return validateAzureKeyVaultConnectionCredentials(appConnection);
|
||||
case AppConnection.AzureAppConfiguration:
|
||||
return validateAzureAppConfigurationConnectionCredentials(appConnection);
|
||||
case AppConnection.Humanitec:
|
||||
return validateHumanitecConnectionCredentials(appConnection);
|
||||
case AppConnection.Vercel:
|
||||
return validateVercelConnectionCredentials(appConnection);
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection ${app}`);
|
||||
}
|
||||
};
|
||||
): Promise<TAppConnection["credentials"]> => VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection);
|
||||
|
||||
export const getAppConnectionMethodName = (method: TAppConnection["method"]) => {
|
||||
switch (method) {
|
||||
@@ -141,10 +135,13 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
return "Service Account Impersonation";
|
||||
case DatabricksConnectionMethod.ServicePrincipal:
|
||||
return "Service Principal";
|
||||
case HumanitecConnectionMethod.API_TOKEN:
|
||||
case HumanitecConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case VercelConnectionMethod.API_TOKEN:
|
||||
case VercelConnectionMethod.ApiToken:
|
||||
return "API Token";
|
||||
case PostgresConnectionMethod.UsernameAndPassword:
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
return "Username & Password";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
@@ -165,3 +162,25 @@ export const decryptAppConnection = async (
|
||||
credentialsHash: generateHash(appConnection.encryptedCredentials)
|
||||
} as TAppConnection;
|
||||
};
|
||||
|
||||
const platformManagedCredentialsNotSupported: TAppConnectionTransitionCredentialsToPlatform = ({ app }) => {
|
||||
throw new BadRequestError({
|
||||
message: `${APP_CONNECTION_NAME_MAP[app]} Connections do not support platform managed credentials.`
|
||||
});
|
||||
};
|
||||
|
||||
export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
AppConnection,
|
||||
TAppConnectionTransitionCredentialsToPlatform
|
||||
> = {
|
||||
[AppConnection.AWS]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Databricks]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GitHub]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.GCP]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.Vercel]: platformManagedCredentialsNotSupported
|
||||
};
|
||||
|
||||
@@ -8,5 +8,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.Vercel]: "Vercel"
|
||||
[AppConnection.Vercel]: "Vercel",
|
||||
[AppConnection.Postgres]: "PostgreSQL",
|
||||
[AppConnection.MsSql]: "Microsoft SQL Server"
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ import { z } from "zod";
|
||||
import { AppConnectionsSchema } from "@app/db/schemas/app-connections";
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import { TAppConnectionBaseConfig } from "@app/services/app-connection/app-connection-types";
|
||||
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
|
||||
@@ -14,7 +16,10 @@ export const BaseAppConnectionSchema = AppConnectionsSchema.omit({
|
||||
credentialsHash: z.string().optional()
|
||||
});
|
||||
|
||||
export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
export const GenericCreateAppConnectionFieldsSchema = (
|
||||
app: AppConnection,
|
||||
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
|
||||
) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(AppConnections.CREATE(app).name),
|
||||
description: z
|
||||
@@ -22,10 +27,16 @@ export const GenericCreateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.CREATE(app).description)
|
||||
.describe(AppConnections.CREATE(app).description),
|
||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||
? z.boolean().optional().default(false).describe(AppConnections.CREATE(app).isPlatformManagedCredentials)
|
||||
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
||||
});
|
||||
|
||||
export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
export const GenericUpdateAppConnectionFieldsSchema = (
|
||||
app: AppConnection,
|
||||
{ supportsPlatformManagedCredentials = false }: TAppConnectionBaseConfig = {}
|
||||
) =>
|
||||
z.object({
|
||||
name: slugSchema({ field: "name" }).describe(AppConnections.UPDATE(app).name).optional(),
|
||||
description: z
|
||||
@@ -33,5 +44,8 @@ export const GenericUpdateAppConnectionFieldsSchema = (app: AppConnection) =>
|
||||
.trim()
|
||||
.max(256, "Description cannot exceed 256 characters")
|
||||
.nullish()
|
||||
.describe(AppConnections.UPDATE(app).description)
|
||||
.describe(AppConnections.UPDATE(app).description),
|
||||
isPlatformManagedCredentials: supportsPlatformManagedCredentials
|
||||
? z.boolean().optional().describe(AppConnections.UPDATE(app).isPlatformManagedCredentials)
|
||||
: z.literal(false).optional().describe(`Not supported for ${APP_CONNECTION_NAME_MAP[app]} Connections.`)
|
||||
});
|
||||
|
||||
@@ -6,25 +6,27 @@ import { generateHash } from "@app/lib/crypto/encryption";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { DiscriminativePick, OrgServiceActor } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
decryptAppConnection,
|
||||
encryptAppConnectionCredentials,
|
||||
getAppConnectionMethodName,
|
||||
listAppConnectionOptions,
|
||||
TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM,
|
||||
validateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { APP_CONNECTION_NAME_MAP } from "@app/services/app-connection/app-connection-maps";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TCreateAppConnectionDTO,
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { AppConnection } from "./app-connection-enums";
|
||||
import { APP_CONNECTION_NAME_MAP } from "./app-connection-maps";
|
||||
import {
|
||||
TAppConnection,
|
||||
TAppConnectionConfig,
|
||||
TAppConnectionRaw,
|
||||
TCreateAppConnectionDTO,
|
||||
TUpdateAppConnectionDTO,
|
||||
TValidateAppConnectionCredentialsSchema
|
||||
} from "./app-connection-types";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
@@ -37,6 +39,8 @@ import { ValidateGitHubConnectionCredentialsSchema } from "./github";
|
||||
import { githubConnectionService } from "./github/github-connection-service";
|
||||
import { ValidateHumanitecConnectionCredentialsSchema } from "./humanitec";
|
||||
import { humanitecConnectionService } from "./humanitec/humanitec-connection-service";
|
||||
import { ValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import { ValidatePostgresConnectionCredentialsSchema } from "./postgres";
|
||||
import { ValidateVercelConnectionCredentialsSchema } from "./vercel";
|
||||
import { vercelConnectionService } from "./vercel/vercel-connection-service";
|
||||
|
||||
@@ -48,7 +52,7 @@ export type TAppConnectionServiceFactoryDep = {
|
||||
|
||||
export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServiceFactory>;
|
||||
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentialsSchema> = {
|
||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||
@@ -56,7 +60,9 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema
|
||||
[AppConnection.Vercel]: ValidateVercelConnectionCredentialsSchema,
|
||||
[AppConnection.Postgres]: ValidatePostgresConnectionCredentialsSchema,
|
||||
[AppConnection.MsSql]: ValidateMsSqlConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -166,20 +172,38 @@ export const appConnectionServiceFactory = ({
|
||||
orgId: actor.orgId
|
||||
} as TAppConnectionConfig);
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
try {
|
||||
const connection = await appConnectionDAL.create({
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
method,
|
||||
app,
|
||||
...params
|
||||
});
|
||||
const createConnection = async (connectionCredentials: TAppConnection["credentials"]) => {
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: connectionCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
return appConnectionDAL.create({
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
method,
|
||||
app,
|
||||
...params
|
||||
});
|
||||
};
|
||||
|
||||
let connection: TAppConnectionRaw;
|
||||
|
||||
if (params.isPlatformManagedCredentials) {
|
||||
connection = await TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM[app](
|
||||
{
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials: validatedCredentials,
|
||||
method
|
||||
} as TAppConnectionConfig,
|
||||
(platformCredentials) => createConnection(platformCredentials)
|
||||
);
|
||||
} else {
|
||||
connection = await createConnection(validatedCredentials);
|
||||
}
|
||||
|
||||
return {
|
||||
...connection,
|
||||
@@ -216,11 +240,18 @@ export const appConnectionServiceFactory = ({
|
||||
OrgPermissionSubjects.AppConnections
|
||||
);
|
||||
|
||||
let encryptedCredentials: undefined | Buffer;
|
||||
// prevent updating credentials or management status if platform managed
|
||||
if (appConnection.isPlatformManagedCredentials && (params.isPlatformManagedCredentials === false || credentials)) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot update credentials or management status for platform managed connections"
|
||||
});
|
||||
}
|
||||
|
||||
let updatedCredentials: undefined | TAppConnection["credentials"];
|
||||
|
||||
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||
|
||||
if (credentials) {
|
||||
const { app, method } = appConnection as DiscriminativePick<TAppConnectionConfig, "app" | "method">;
|
||||
|
||||
if (
|
||||
!VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[app].safeParse({
|
||||
method,
|
||||
@@ -233,29 +264,53 @@ export const appConnectionServiceFactory = ({
|
||||
} Connection with method ${getAppConnectionMethodName(method)}`
|
||||
});
|
||||
|
||||
const validatedCredentials = await validateAppConnectionCredentials({
|
||||
updatedCredentials = await validateAppConnectionCredentials({
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials,
|
||||
method
|
||||
} as TAppConnectionConfig);
|
||||
|
||||
if (!validatedCredentials)
|
||||
if (!updatedCredentials)
|
||||
throw new BadRequestError({ message: "Unable to validate connection - check credentials" });
|
||||
|
||||
encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: validatedCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedConnection = await appConnectionDAL.updateById(connectionId, {
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
...params
|
||||
});
|
||||
const updateConnection = async (connectionCredentials: TAppConnection["credentials"] | undefined) => {
|
||||
const encryptedCredentials = connectionCredentials
|
||||
? await encryptAppConnectionCredentials({
|
||||
credentials: connectionCredentials,
|
||||
orgId: actor.orgId,
|
||||
kmsService
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return appConnectionDAL.updateById(connectionId, {
|
||||
orgId: actor.orgId,
|
||||
encryptedCredentials,
|
||||
...params
|
||||
});
|
||||
};
|
||||
|
||||
let updatedConnection: TAppConnectionRaw;
|
||||
|
||||
if (params.isPlatformManagedCredentials) {
|
||||
if (!updatedCredentials)
|
||||
// prevent enabling platform managed credentials without re-confirming credentials
|
||||
throw new BadRequestError({ message: "Credentials required to transition to platform managed credentials" });
|
||||
|
||||
updatedConnection = await TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM[app](
|
||||
{
|
||||
app,
|
||||
orgId: actor.orgId,
|
||||
credentials: updatedCredentials,
|
||||
method
|
||||
} as TAppConnectionConfig,
|
||||
(platformCredentials) => updateConnection(platformCredentials)
|
||||
);
|
||||
} else {
|
||||
updatedConnection = await updateConnection(updatedCredentials);
|
||||
}
|
||||
|
||||
return await decryptAppConnection(updatedConnection, kmsService);
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,45 +1,58 @@
|
||||
import { AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { AWSRegion } from "./app-connection-enums";
|
||||
import {
|
||||
TAwsConnection,
|
||||
TAwsConnectionConfig,
|
||||
TAwsConnectionInput,
|
||||
TValidateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
TDatabricksConnectionInput,
|
||||
TValidateDatabricksConnectionCredentials
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import {
|
||||
TGitHubConnection,
|
||||
TGitHubConnectionConfig,
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
TValidateAwsConnectionCredentialsSchema
|
||||
} from "./aws";
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureAppConfigurationConnectionConfig,
|
||||
TAzureAppConfigurationConnectionInput,
|
||||
TValidateAzureAppConfigurationConnectionCredentials
|
||||
TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
} from "./azure-app-configuration";
|
||||
import {
|
||||
TAzureKeyVaultConnection,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
TAzureKeyVaultConnectionInput,
|
||||
TValidateAzureKeyVaultConnectionCredentials
|
||||
TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
} from "./azure-key-vault";
|
||||
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
TDatabricksConnectionInput,
|
||||
TValidateDatabricksConnectionCredentialsSchema
|
||||
} from "./databricks";
|
||||
import {
|
||||
TGcpConnection,
|
||||
TGcpConnectionConfig,
|
||||
TGcpConnectionInput,
|
||||
TValidateGcpConnectionCredentialsSchema
|
||||
} from "./gcp";
|
||||
import {
|
||||
TGitHubConnection,
|
||||
TGitHubConnectionConfig,
|
||||
TGitHubConnectionInput,
|
||||
TValidateGitHubConnectionCredentialsSchema
|
||||
} from "./github";
|
||||
import {
|
||||
THumanitecConnection,
|
||||
THumanitecConnectionConfig,
|
||||
THumanitecConnectionInput,
|
||||
TValidateHumanitecConnectionCredentials
|
||||
TValidateHumanitecConnectionCredentialsSchema
|
||||
} from "./humanitec";
|
||||
import { TMsSqlConnection, TMsSqlConnectionInput, TValidateMsSqlConnectionCredentialsSchema } from "./mssql";
|
||||
import {
|
||||
TValidateVercelConnectionCredentials,
|
||||
TPostgresConnection,
|
||||
TPostgresConnectionInput,
|
||||
TValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres";
|
||||
import {
|
||||
TValidateVercelConnectionCredentialsSchema,
|
||||
TVercelConnection,
|
||||
TVercelConnectionConfig,
|
||||
TVercelConnectionInput
|
||||
@@ -54,8 +67,14 @@ export type TAppConnection = { id: string } & (
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TVercelConnection
|
||||
| TPostgresConnection
|
||||
| TMsSqlConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionRaw = NonNullable<Awaited<ReturnType<TAppConnectionDALFactory["findById"]>>>;
|
||||
|
||||
export type TSqlConnection = TPostgresConnection | TMsSqlConnection;
|
||||
|
||||
export type TAppConnectionInput = { id: string } & (
|
||||
| TAwsConnectionInput
|
||||
| TGitHubConnectionInput
|
||||
@@ -65,11 +84,15 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TVercelConnectionInput
|
||||
| TPostgresConnectionInput
|
||||
| TMsSqlConnectionInput
|
||||
);
|
||||
|
||||
export type TSqlConnectionInput = TPostgresConnectionInput | TMsSqlConnectionInput;
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
"credentials" | "method" | "name" | "app" | "description"
|
||||
"credentials" | "method" | "name" | "app" | "description" | "isPlatformManagedCredentials"
|
||||
>;
|
||||
|
||||
export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "method" | "app">> & {
|
||||
@@ -84,20 +107,36 @@ export type TAppConnectionConfig =
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
| TVercelConnectionConfig;
|
||||
| TVercelConnectionConfig
|
||||
| TSqlConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentials =
|
||||
| TValidateAwsConnectionCredentials
|
||||
| TValidateGitHubConnectionCredentials
|
||||
| TValidateGcpConnectionCredentials
|
||||
| TValidateAzureKeyVaultConnectionCredentials
|
||||
| TValidateAzureAppConfigurationConnectionCredentials
|
||||
| TValidateDatabricksConnectionCredentials
|
||||
| TValidateHumanitecConnectionCredentials
|
||||
| TValidateVercelConnectionCredentials;
|
||||
export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAwsConnectionCredentialsSchema
|
||||
| TValidateGitHubConnectionCredentialsSchema
|
||||
| TValidateGcpConnectionCredentialsSchema
|
||||
| TValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
| TValidateDatabricksConnectionCredentialsSchema
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
| TValidateMsSqlConnectionCredentialsSchema
|
||||
| TValidateVercelConnectionCredentialsSchema;
|
||||
|
||||
export type TListAwsConnectionKmsKeys = {
|
||||
connectionId: string;
|
||||
region: AWSRegion;
|
||||
destination: SecretSync.AWSParameterStore | SecretSync.AWSSecretsManager;
|
||||
};
|
||||
|
||||
export type TAppConnectionCredentialsValidator = (
|
||||
appConnection: TAppConnectionConfig
|
||||
) => Promise<TAppConnection["credentials"]>;
|
||||
|
||||
export type TAppConnectionTransitionCredentialsToPlatform = (
|
||||
appConnection: TAppConnectionConfig,
|
||||
callback: (credentials: TAppConnection["credentials"]) => Promise<TAppConnectionRaw>
|
||||
) => Promise<TAppConnectionRaw>;
|
||||
|
||||
export type TAppConnectionBaseConfig = {
|
||||
supportsPlatformManagedCredentials?: boolean;
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
|
||||
resp = await sts.getCallerIdentity().promise();
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,11 @@ export const SanitizedAwsConnectionSchema = z.discriminatedUnion("method", [
|
||||
|
||||
export const ValidateAwsConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||
method: z.literal(AwsConnectionMethod.AssumeRole).describe(AppConnections.CREATE(AppConnection.AWS).method),
|
||||
credentials: AwsConnectionAssumeRoleCredentialsSchema.describe(AppConnections.CREATE(AppConnection.AWS).credentials)
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections?.CREATE(AppConnection.AWS).method),
|
||||
method: z.literal(AwsConnectionMethod.AccessKey).describe(AppConnections.CREATE(AppConnection.AWS).method),
|
||||
credentials: AwsConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AWS).credentials
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type TAwsConnectionInput = z.infer<typeof CreateAwsConnectionSchema> & {
|
||||
app: AppConnection.AWS;
|
||||
};
|
||||
|
||||
export type TValidateAwsConnectionCredentials = typeof ValidateAwsConnectionCredentialsSchema;
|
||||
export type TValidateAwsConnectionCredentialsSchema = typeof ValidateAwsConnectionCredentialsSchema;
|
||||
|
||||
export type TAwsConnectionConfig = DiscriminativePick<TAwsConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ export const validateAzureAppConfigurationConnectionCredentials = async (
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export type TAzureAppConfigurationConnectionInput = z.infer<typeof CreateAzureAp
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
};
|
||||
|
||||
export type TValidateAzureAppConfigurationConnectionCredentials =
|
||||
export type TValidateAzureAppConfigurationConnectionCredentialsSchema =
|
||||
typeof ValidateAzureAppConfigurationConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureAppConfigurationConnectionConfig = DiscriminativePick<
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureK
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ export type TAzureKeyVaultConnectionInput = z.infer<typeof CreateAzureKeyVaultCo
|
||||
app: AppConnection.AzureKeyVault;
|
||||
};
|
||||
|
||||
export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
|
||||
export type TValidateAzureKeyVaultConnectionCredentialsSchema = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureKeyVaultConnectionConfig = DiscriminativePick<
|
||||
TAzureKeyVaultConnectionInput,
|
||||
|
||||
@@ -86,7 +86,7 @@ export const validateDatabricksConnectionCredentials = async (appConnection: TDa
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnio
|
||||
z.object({
|
||||
method: z
|
||||
.literal(DatabricksConnectionMethod.ServicePrincipal)
|
||||
.describe(AppConnections?.CREATE(AppConnection.Databricks).method),
|
||||
.describe(AppConnections.CREATE(AppConnection.Databricks).method),
|
||||
credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Databricks).credentials
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type TDatabricksConnectionInput = z.infer<typeof CreateDatabricksConnecti
|
||||
app: AppConnection.Databricks;
|
||||
};
|
||||
|
||||
export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema;
|
||||
export type TValidateDatabricksConnectionCredentialsSchema = typeof ValidateDatabricksConnectionCredentialsSchema;
|
||||
|
||||
export type TDatabricksConnectionConfig = DiscriminativePick<
|
||||
TDatabricksConnection,
|
||||
|
||||
@@ -4,10 +4,10 @@ import { GetAccessTokenResponse } from "google-auth-library/build/src/auth/oauth
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { getAppConnectionMethodName } from "../app-connection-fns";
|
||||
import { GcpConnectionMethod } from "./gcp-connection-enums";
|
||||
import {
|
||||
GCPApp,
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ValidateGcpConnectionCredentialsSchema = z.discriminatedUnion("meth
|
||||
z.object({
|
||||
method: z
|
||||
.literal(GcpConnectionMethod.ServiceAccountImpersonation)
|
||||
.describe(AppConnections?.CREATE(AppConnection.GCP).method),
|
||||
.describe(AppConnections.CREATE(AppConnection.GCP).method),
|
||||
credentials: GcpConnectionServiceAccountImpersonationCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.GCP).credentials
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type TGcpConnectionInput = z.infer<typeof CreateGcpConnectionSchema> & {
|
||||
app: AppConnection.GCP;
|
||||
};
|
||||
|
||||
export type TValidateGcpConnectionCredentials = typeof ValidateGcpConnectionCredentialsSchema;
|
||||
export type TValidateGcpConnectionCredentialsSchema = typeof ValidateGcpConnectionCredentialsSchema;
|
||||
|
||||
export type TGcpConnectionConfig = DiscriminativePick<TGcpConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
|
||||
@@ -200,7 +200,7 @@ export const validateGitHubConnectionCredentials = async (config: TGitHubConnect
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
message: `Unable to validate connection: verify credentials`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ export type TGitHubConnectionInput = z.infer<typeof CreateGitHubConnectionSchema
|
||||
app: AppConnection.GitHub;
|
||||
};
|
||||
|
||||
export type TValidateGitHubConnectionCredentials = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||
export type TValidateGitHubConnectionCredentialsSchema = typeof ValidateGitHubConnectionCredentialsSchema;
|
||||
|
||||
export type TGitHubConnectionConfig = DiscriminativePick<TGitHubConnectionInput, "method" | "app" | "credentials">;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export enum HumanitecConnectionMethod {
|
||||
API_TOKEN = "api-token"
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const getHumanitecConnectionListItem = () => {
|
||||
return {
|
||||
name: "Humanitec" as const,
|
||||
app: AppConnection.Humanitec as const,
|
||||
methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.API_TOKEN]
|
||||
methods: Object.values(HumanitecConnectionMethod) as [HumanitecConnectionMethod.ApiToken]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -40,7 +40,7 @@ export const validateHumanitecConnectionCredentials = async (config: THumanitecC
|
||||
});
|
||||
}
|
||||
throw new BadRequestError({
|
||||
message: "Unable to validate connection - verify credentials"
|
||||
message: "Unable to validate connection: verify credentials"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ export const HumanitecConnectionAccessTokenCredentialsSchema = z.object({
|
||||
const BaseHumanitecConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Humanitec) });
|
||||
|
||||
export const HumanitecConnectionSchema = BaseHumanitecConnectionSchema.extend({
|
||||
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
|
||||
method: z.literal(HumanitecConnectionMethod.ApiToken),
|
||||
credentials: HumanitecConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseHumanitecConnectionSchema.extend({
|
||||
method: z.literal(HumanitecConnectionMethod.API_TOKEN),
|
||||
method: z.literal(HumanitecConnectionMethod.ApiToken),
|
||||
credentials: HumanitecConnectionAccessTokenCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
@@ -31,8 +31,8 @@ export const SanitizedHumanitecConnectionSchema = z.discriminatedUnion("method",
|
||||
export const ValidateHumanitecConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(HumanitecConnectionMethod.API_TOKEN)
|
||||
.describe(AppConnections?.CREATE(AppConnection.Humanitec).method),
|
||||
.literal(HumanitecConnectionMethod.ApiToken)
|
||||
.describe(AppConnections.CREATE(AppConnection.Humanitec).method),
|
||||
credentials: HumanitecConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Humanitec).credentials
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type THumanitecConnectionInput = z.infer<typeof CreateHumanitecConnection
|
||||
app: AppConnection.Humanitec;
|
||||
};
|
||||
|
||||
export type TValidateHumanitecConnectionCredentials = typeof ValidateHumanitecConnectionCredentialsSchema;
|
||||
export type TValidateHumanitecConnectionCredentialsSchema = typeof ValidateHumanitecConnectionCredentialsSchema;
|
||||
|
||||
export type THumanitecConnectionConfig = DiscriminativePick<
|
||||
THumanitecConnectionInput,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./mssql-connection-enums";
|
||||
export * from "./mssql-connection-fns";
|
||||
export * from "./mssql-connection-schemas";
|
||||
export * from "./mssql-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum MsSqlConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { MsSqlConnectionMethod } from "./mssql-connection-enums";
|
||||
|
||||
export const getMsSqlConnectionListItem = () => {
|
||||
return {
|
||||
name: "Microsoft SQL Server" as const,
|
||||
app: AppConnection.MsSql as const,
|
||||
methods: Object.values(MsSqlConnectionMethod) as [MsSqlConnectionMethod.UsernameAndPassword],
|
||||
supportsPlatformManagement: true as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
|
||||
import { MsSqlConnectionMethod } from "./mssql-connection-enums";
|
||||
|
||||
export const MsSqlConnectionAccessTokenCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema;
|
||||
|
||||
const BaseMsSqlConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.MsSql)
|
||||
});
|
||||
|
||||
export const MsSqlConnectionSchema = BaseMsSqlConnectionSchema.extend({
|
||||
method: z.literal(MsSqlConnectionMethod.UsernameAndPassword),
|
||||
credentials: MsSqlConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedMsSqlConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseMsSqlConnectionSchema.extend({
|
||||
method: z.literal(MsSqlConnectionMethod.UsernameAndPassword),
|
||||
credentials: MsSqlConnectionAccessTokenCredentialsSchema.pick({
|
||||
host: true,
|
||||
database: true,
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateMsSqlConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(MsSqlConnectionMethod.UsernameAndPassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.MsSql).method),
|
||||
credentials: MsSqlConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.MsSql).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateMsSqlConnectionSchema = ValidateMsSqlConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true })
|
||||
);
|
||||
|
||||
export const UpdateMsSqlConnectionSchema = z
|
||||
.object({
|
||||
credentials: MsSqlConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.MsSql).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.MsSql, { supportsPlatformManagedCredentials: true }));
|
||||
|
||||
export const MsSqlConnectionListItemSchema = z.object({
|
||||
name: z.literal("Microsoft SQL Server"),
|
||||
app: z.literal(AppConnection.MsSql),
|
||||
methods: z.nativeEnum(MsSqlConnectionMethod).array(),
|
||||
supportsPlatformManagement: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreateMsSqlConnectionSchema,
|
||||
MsSqlConnectionSchema,
|
||||
ValidateMsSqlConnectionCredentialsSchema
|
||||
} from "./mssql-connection-schemas";
|
||||
|
||||
export type TMsSqlConnection = z.infer<typeof MsSqlConnectionSchema>;
|
||||
|
||||
export type TMsSqlConnectionInput = z.infer<typeof CreateMsSqlConnectionSchema> & {
|
||||
app: AppConnection.MsSql;
|
||||
};
|
||||
|
||||
export type TValidateMsSqlConnectionCredentialsSchema = typeof ValidateMsSqlConnectionCredentialsSchema;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./postgres-connection-enums";
|
||||
export * from "./postgres-connection-fns";
|
||||
export * from "./postgres-connection-schemas";
|
||||
export * from "./postgres-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum PostgresConnectionMethod {
|
||||
UsernameAndPassword = "username-and-password"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import { PostgresConnectionMethod } from "./postgres-connection-enums";
|
||||
|
||||
export const getPostgresConnectionListItem = () => {
|
||||
return {
|
||||
name: "PostgreSQL" as const,
|
||||
app: AppConnection.Postgres as const,
|
||||
methods: Object.values(PostgresConnectionMethod) as [PostgresConnectionMethod.UsernameAndPassword],
|
||||
supportsPlatformManagement: true as const
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { BaseSqlUsernameAndPasswordConnectionSchema } from "../shared/sql";
|
||||
import { PostgresConnectionMethod } from "./postgres-connection-enums";
|
||||
|
||||
export const PostgresConnectionAccessTokenCredentialsSchema = BaseSqlUsernameAndPasswordConnectionSchema;
|
||||
|
||||
const BasePostgresConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Postgres) });
|
||||
|
||||
export const PostgresConnectionSchema = BasePostgresConnectionSchema.extend({
|
||||
method: z.literal(PostgresConnectionMethod.UsernameAndPassword),
|
||||
credentials: PostgresConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedPostgresConnectionSchema = z.discriminatedUnion("method", [
|
||||
BasePostgresConnectionSchema.extend({
|
||||
method: z.literal(PostgresConnectionMethod.UsernameAndPassword),
|
||||
credentials: PostgresConnectionAccessTokenCredentialsSchema.pick({
|
||||
host: true,
|
||||
database: true,
|
||||
port: true,
|
||||
username: true,
|
||||
sslEnabled: true,
|
||||
sslRejectUnauthorized: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidatePostgresConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(PostgresConnectionMethod.UsernameAndPassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.Postgres).method),
|
||||
credentials: PostgresConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Postgres).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreatePostgresConnectionSchema = ValidatePostgresConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true })
|
||||
);
|
||||
|
||||
export const UpdatePostgresConnectionSchema = z
|
||||
.object({
|
||||
credentials: PostgresConnectionAccessTokenCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Postgres).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Postgres, { supportsPlatformManagedCredentials: true }));
|
||||
|
||||
export const PostgresConnectionListItemSchema = z.object({
|
||||
name: z.literal("PostgreSQL"),
|
||||
app: z.literal(AppConnection.Postgres),
|
||||
methods: z.nativeEnum(PostgresConnectionMethod).array(),
|
||||
supportsPlatformManagement: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
CreatePostgresConnectionSchema,
|
||||
PostgresConnectionSchema,
|
||||
ValidatePostgresConnectionCredentialsSchema
|
||||
} from "./postgres-connection-schemas";
|
||||
|
||||
export type TPostgresConnection = z.infer<typeof PostgresConnectionSchema>;
|
||||
|
||||
export type TPostgresConnectionInput = z.infer<typeof CreatePostgresConnectionSchema> & {
|
||||
app: AppConnection.Postgres;
|
||||
};
|
||||
|
||||
export type TValidatePostgresConnectionCredentialsSchema = typeof ValidatePostgresConnectionCredentialsSchema;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./sql-connection-fns";
|
||||
export * from "./sql-connection-schemas";
|
||||
@@ -0,0 +1,139 @@
|
||||
import knex, { Knex } from "knex";
|
||||
|
||||
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
|
||||
import {
|
||||
TSqlCredentialsRotationGeneratedCredentials,
|
||||
TSqlCredentialsRotationWithConnection
|
||||
} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-types";
|
||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { TAppConnectionRaw, TSqlConnection } from "@app/services/app-connection/app-connection-types";
|
||||
import { TSqlConnectionConfig } from "@app/services/app-connection/shared/sql/sql-connection-types";
|
||||
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const SQL_CONNECTION_CLIENT_MAP = {
|
||||
[AppConnection.Postgres]: "pg",
|
||||
[AppConnection.MsSql]: "mssql"
|
||||
};
|
||||
|
||||
const getConnectionConfig = ({
|
||||
app,
|
||||
credentials: { host, sslCertificate, sslEnabled, sslRejectUnauthorized }
|
||||
}: Pick<TSqlConnection, "credentials" | "app">) => {
|
||||
switch (app) {
|
||||
case AppConnection.Postgres: {
|
||||
return {
|
||||
ssl: sslEnabled
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host
|
||||
}
|
||||
: false
|
||||
};
|
||||
}
|
||||
case AppConnection.MsSql: {
|
||||
return {
|
||||
options: sslEnabled
|
||||
? {
|
||||
trustServerCertificate: !sslRejectUnauthorized,
|
||||
encrypt: true,
|
||||
cryptoCredentialsDetails: sslCertificate ? { ca: sslCertificate } : {}
|
||||
}
|
||||
: { encrypt: false }
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unhandled SQL Connection Config: ${app as AppConnection}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection, "credentials" | "app">) => {
|
||||
const {
|
||||
app,
|
||||
credentials: { host: baseHost, database, port, password, username }
|
||||
} = appConnection;
|
||||
|
||||
const [host] = await verifyHostInputValidity(baseHost);
|
||||
|
||||
const client = knex({
|
||||
client: SQL_CONNECTION_CLIENT_MAP[app],
|
||||
connection: {
|
||||
database,
|
||||
port,
|
||||
host,
|
||||
user: username,
|
||||
password,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
...getConnectionConfig(appConnection)
|
||||
}
|
||||
});
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
export const validateSqlConnectionCredentials = async (config: TSqlConnectionConfig) => {
|
||||
const { credentials, app } = config;
|
||||
|
||||
const client = await getSqlConnectionClient({ app, credentials });
|
||||
|
||||
try {
|
||||
await client.raw(`Select 1`);
|
||||
|
||||
return credentials;
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
(error as Error)?.message?.replaceAll(credentials.password, "********************") ??
|
||||
"Unable to validate connection: verify credentials"
|
||||
});
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
export const SQL_CONNECTION_ALTER_LOGIN_STATEMENT: Record<
|
||||
TSqlCredentialsRotationWithConnection["connection"]["app"],
|
||||
(credentials: TSqlCredentialsRotationGeneratedCredentials[number]) => [string, Knex.RawBinding]
|
||||
> = {
|
||||
[AppConnection.Postgres]: ({ username, password }) => [`ALTER USER ?? WITH PASSWORD '${password}';`, [username]],
|
||||
[AppConnection.MsSql]: ({ username, password }) => [`ALTER LOGIN ?? WITH PASSWORD = '${password}';`, [username]]
|
||||
};
|
||||
|
||||
export const transferSqlConnectionCredentialsToPlatform = async (
|
||||
config: TSqlConnectionConfig,
|
||||
callback: (credentials: TSqlConnectionConfig["credentials"]) => Promise<TAppConnectionRaw>
|
||||
) => {
|
||||
const { credentials, app } = config;
|
||||
|
||||
const client = await getSqlConnectionClient({ app, credentials });
|
||||
|
||||
const newPassword = alphaNumericNanoId(32);
|
||||
|
||||
try {
|
||||
return await client.transaction(async (tx) => {
|
||||
await tx.raw(
|
||||
...SQL_CONNECTION_ALTER_LOGIN_STATEMENT[app]({ username: credentials.username, password: newPassword })
|
||||
);
|
||||
return callback({
|
||||
...credentials,
|
||||
password: newPassword
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
// update/create service function will handle
|
||||
if (error instanceof DatabaseError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
(error as Error)?.message?.replaceAll(newPassword, "********************") ??
|
||||
"Encountered an error transferring credentials to platform"
|
||||
});
|
||||
} finally {
|
||||
await client.destroy();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
|
||||
export const BaseSqlUsernameAndPasswordConnectionSchema = z.object({
|
||||
host: z.string().trim().min(1, "Host required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.host),
|
||||
port: z.coerce.number().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.port),
|
||||
database: z.string().trim().min(1, "Database required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.database),
|
||||
username: z.string().trim().min(1, "Username required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.username),
|
||||
password: z.string().trim().min(1, "Password required").describe(AppConnections.CREDENTIALS.SQL_CONNECTION.password),
|
||||
sslEnabled: z.boolean().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.sslEnabled),
|
||||
sslRejectUnauthorized: z.boolean().describe(AppConnections.CREDENTIALS.SQL_CONNECTION.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.SQL_CONNECTION.sslCertificate)
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { TSqlConnectionInput } from "@app/services/app-connection/app-connection-types";
|
||||
|
||||
export type TSqlConnectionConfig = DiscriminativePick<TSqlConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
export enum VercelConnectionMethod {
|
||||
API_TOKEN = "api-token"
|
||||
ApiToken = "api-token"
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const getVercelConnectionListItem = () => {
|
||||
return {
|
||||
name: "Vercel" as const,
|
||||
app: AppConnection.Vercel as const,
|
||||
methods: Object.values(VercelConnectionMethod) as [VercelConnectionMethod.API_TOKEN]
|
||||
methods: Object.values(VercelConnectionMethod) as [VercelConnectionMethod.ApiToken]
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -14,23 +14,26 @@ export const VercelConnectionAccessTokenCredentialsSchema = z.object({
|
||||
apiToken: z.string().trim().min(1, "API Token required")
|
||||
});
|
||||
|
||||
const BaseVercelConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Vercel) });
|
||||
const BaseVercelConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.Vercel),
|
||||
isPlatformManagedCredentials: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const VercelConnectionSchema = BaseVercelConnectionSchema.extend({
|
||||
method: z.literal(VercelConnectionMethod.API_TOKEN),
|
||||
method: z.literal(VercelConnectionMethod.ApiToken),
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedVercelConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseVercelConnectionSchema.extend({
|
||||
method: z.literal(VercelConnectionMethod.API_TOKEN),
|
||||
method: z.literal(VercelConnectionMethod.ApiToken),
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema.pick({})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateVercelConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(VercelConnectionMethod.API_TOKEN).describe(AppConnections?.CREATE(AppConnection.Vercel).method),
|
||||
method: z.literal(VercelConnectionMethod.ApiToken).describe(AppConnections.CREATE(AppConnection.Vercel).method),
|
||||
credentials: VercelConnectionAccessTokenCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Vercel).credentials
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ export type TVercelConnectionInput = z.infer<typeof CreateVercelConnectionSchema
|
||||
app: AppConnection.Vercel;
|
||||
};
|
||||
|
||||
export type TValidateVercelConnectionCredentials = typeof ValidateVercelConnectionCredentialsSchema;
|
||||
export type TValidateVercelConnectionCredentialsSchema = typeof ValidateVercelConnectionCredentialsSchema;
|
||||
|
||||
export type TVercelConnectionConfig = DiscriminativePick<TVercelConnectionInput, "method" | "app" | "credentials"> & {
|
||||
orgId: string;
|
||||
|
||||
@@ -12,17 +12,22 @@ import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
|
||||
|
||||
type TOrgAdminServiceFactoryDep = {
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findOne" | "create" | "transaction" | "delete">;
|
||||
projectDAL: Pick<TProjectDALFactory, "find" | "findById" | "findProjectGhostUser" | "findOne">;
|
||||
projectMembershipDAL: Pick<
|
||||
TProjectMembershipDALFactory,
|
||||
"findOne" | "create" | "transaction" | "delete" | "findAllProjectMembers"
|
||||
>;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
};
|
||||
|
||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||
@@ -34,7 +39,8 @@ export const orgAdminServiceFactory = ({
|
||||
projectKeyDAL,
|
||||
projectBotDAL,
|
||||
userDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
const listOrgProjects = async ({
|
||||
actor,
|
||||
@@ -89,7 +95,7 @@ export const orgAdminServiceFactory = ({
|
||||
OrgPermissionSubjects.AdminConsole
|
||||
);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId });
|
||||
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
|
||||
|
||||
if (project.version === ProjectVersion.V1) {
|
||||
@@ -184,6 +190,23 @@ export const orgAdminServiceFactory = ({
|
||||
);
|
||||
return newProjectMembership;
|
||||
});
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const filteredProjectMembers = projectMembers
|
||||
.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
)
|
||||
.map((el) => el.user.email!);
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: filteredProjectMembers,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
|
||||
}
|
||||
});
|
||||
return { isExistingMember: false, membership: updatedMembership };
|
||||
};
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ export const orgServiceFactory = ({
|
||||
|
||||
const findAllWorkspaces = async ({ actor, actorId, orgId, type }: TFindAllWorkspacesDTO) => {
|
||||
if (actor === ActorType.USER) {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, orgId, type || "all");
|
||||
const workspaces = await projectDAL.findUserProjects(actorId, orgId, type || "all");
|
||||
return workspaces;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,20 +6,23 @@ import {
|
||||
ProjectType,
|
||||
ProjectUpgradeStatus,
|
||||
ProjectVersion,
|
||||
SortDirection,
|
||||
TableName,
|
||||
TProjects,
|
||||
TProjectsUpdate
|
||||
} from "@app/db/schemas";
|
||||
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
|
||||
import { Filter, ProjectFilterType } from "./project-types";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
|
||||
|
||||
export type TProjectDALFactory = ReturnType<typeof projectDALFactory>;
|
||||
|
||||
export const projectDALFactory = (db: TDbClient) => {
|
||||
const projectOrm = ormify(db, TableName.Project);
|
||||
|
||||
const findAllProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
|
||||
const findUserProjects = async (userId: string, orgId: string, projectType: ProjectType | "all") => {
|
||||
try {
|
||||
const workspaces = await db
|
||||
.replicaNode()(TableName.ProjectMembership)
|
||||
@@ -352,9 +355,79 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const searchProjects = async (dto: {
|
||||
orgId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
type?: ProjectType;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
sortBy?: SearchProjectSortBy;
|
||||
sortDir?: SortDirection;
|
||||
}) => {
|
||||
const { limit = 20, offset = 0, sortBy = SearchProjectSortBy.NAME, sortDir = SortDirection.ASC } = dto;
|
||||
|
||||
const userMembershipSubquery = db(TableName.ProjectMembership).where({ userId: dto.actorId }).select("projectId");
|
||||
const groups = db(TableName.UserGroupMembership).where({ userId: dto.actorId }).select("groupId");
|
||||
const groupMembershipSubquery = db(TableName.GroupProjectMembership).whereIn("groupId", groups).select("projectId");
|
||||
|
||||
const identityMembershipSubQuery = db(TableName.IdentityProjectMembership)
|
||||
.where({ identityId: dto.actorId })
|
||||
.select("projectId");
|
||||
|
||||
// Get the SQL strings for the subqueries
|
||||
const userMembershipSql = userMembershipSubquery.toQuery();
|
||||
const groupMembershipSql = groupMembershipSubquery.toQuery();
|
||||
const identityMembershipSql = identityMembershipSubQuery.toQuery();
|
||||
|
||||
const query = db
|
||||
.replicaNode()(TableName.Project)
|
||||
.where(`${TableName.Project}.orgId`, dto.orgId)
|
||||
.select(selectAllTableCols(TableName.Project))
|
||||
.select(db.raw("COUNT(*) OVER() AS count"))
|
||||
.select<(TProjects & { isMember: boolean; count: number })[]>(
|
||||
dto.actor === ActorType.USER
|
||||
? db.raw(
|
||||
`
|
||||
CASE
|
||||
WHEN ${TableName.Project}.id IN (?) THEN TRUE
|
||||
WHEN ${TableName.Project}.id IN (?) THEN TRUE
|
||||
ELSE FALSE
|
||||
END as "isMember"
|
||||
`,
|
||||
[db.raw(userMembershipSql), db.raw(groupMembershipSql)]
|
||||
)
|
||||
: db.raw(
|
||||
`
|
||||
CASE
|
||||
WHEN ${TableName.Project}.id IN (?) THEN TRUE
|
||||
ELSE FALSE
|
||||
END as "isMember"
|
||||
`,
|
||||
[db.raw(identityMembershipSql)]
|
||||
)
|
||||
)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
if (sortBy === SearchProjectSortBy.NAME) {
|
||||
void query.orderBy([{ column: `${TableName.Project}.name`, order: sortDir }]);
|
||||
}
|
||||
|
||||
if (dto.type) {
|
||||
void query.where(`${TableName.Project}.type`, dto.type);
|
||||
}
|
||||
if (dto.name) {
|
||||
void query.whereILike(`${TableName.Project}.name`, `%${dto.name}%`);
|
||||
}
|
||||
const docs = await query;
|
||||
|
||||
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
||||
};
|
||||
|
||||
return {
|
||||
...projectOrm,
|
||||
findAllProjects,
|
||||
findUserProjects,
|
||||
setProjectUpgradeStatus,
|
||||
findAllProjectsByIdentity,
|
||||
findProjectGhostUser,
|
||||
@@ -363,6 +436,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
||||
findProjectBySlug,
|
||||
findProjectWithOrg,
|
||||
checkProjectUpgradeStatus,
|
||||
getProjectFromSplitId
|
||||
getProjectFromSplitId,
|
||||
searchProjects
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-cer
|
||||
import { TSshCertificateDALFactory } from "@app/ee/services/ssh-certificate/ssh-certificate-dal";
|
||||
import { TSshCertificateTemplateDALFactory } from "@app/ee/services/ssh-certificate-template/ssh-certificate-template-dal";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
@@ -57,6 +58,7 @@ import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secr
|
||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||
import { TProjectSlackConfigDALFactory } from "../slack/project-slack-config-dal";
|
||||
import { TSlackIntegrationDALFactory } from "../slack/slack-integration-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TProjectDALFactory } from "./project-dal";
|
||||
import { assignWorkspaceKeysToMembers, createProjectKey } from "./project-fns";
|
||||
@@ -76,6 +78,8 @@ import {
|
||||
TListProjectSshCertificatesDTO,
|
||||
TListProjectSshCertificateTemplatesDTO,
|
||||
TLoadProjectKmsBackupDTO,
|
||||
TProjectAccessRequestDTO,
|
||||
TSearchProjectsDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
TUpdateProjectDTO,
|
||||
@@ -106,7 +110,10 @@ type TProjectServiceFactoryDep = {
|
||||
identityProjectDAL: TIdentityProjectDALFactory;
|
||||
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
|
||||
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create" | "findProjectGhostUser" | "findOne" | "delete">;
|
||||
projectMembershipDAL: Pick<
|
||||
TProjectMembershipDALFactory,
|
||||
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
|
||||
>;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "delete">;
|
||||
projectSlackConfigDAL: Pick<TProjectSlackConfigDALFactory, "findOne" | "transaction" | "updateById" | "create">;
|
||||
slackIntegrationDAL: Pick<TSlackIntegrationDALFactory, "findById" | "findByIdWithWorkflowIntegrationDetails">;
|
||||
@@ -123,6 +130,7 @@ type TProjectServiceFactoryDep = {
|
||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
queueService: Pick<TQueueServiceFactory, "stopRepeatableJob">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
|
||||
orgDAL: Pick<TOrgDALFactory, "findOne">;
|
||||
keyStore: Pick<TKeyStoreFactory, "deleteItem">;
|
||||
@@ -177,7 +185,8 @@ export const projectServiceFactory = ({
|
||||
projectSlackConfigDAL,
|
||||
slackIntegrationDAL,
|
||||
projectTemplateService,
|
||||
groupProjectDAL
|
||||
groupProjectDAL,
|
||||
smtpService
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -506,7 +515,7 @@ export const projectServiceFactory = ({
|
||||
actorOrgId,
|
||||
type = ProjectType.SecretManager
|
||||
}: TListProjectsDTO) => {
|
||||
const workspaces = await projectDAL.findAllProjects(actorId, actorOrgId, type);
|
||||
const workspaces = await projectDAL.findUserProjects(actorId, actorOrgId, type);
|
||||
|
||||
if (includeRoles) {
|
||||
const { permission } = await permissionService.getUserOrgPermission(
|
||||
@@ -1339,6 +1348,85 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const searchProjects = async ({
|
||||
name,
|
||||
offset,
|
||||
permission,
|
||||
limit,
|
||||
type,
|
||||
orderBy,
|
||||
orderDirection
|
||||
}: TSearchProjectsDTO) => {
|
||||
// check user belong to org
|
||||
await permissionService.getOrgPermission(
|
||||
permission.type,
|
||||
permission.id,
|
||||
permission.orgId,
|
||||
permission.authMethod,
|
||||
permission.orgId
|
||||
);
|
||||
|
||||
return projectDAL.searchProjects({
|
||||
limit,
|
||||
offset,
|
||||
name,
|
||||
type,
|
||||
orgId: permission.orgId,
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
sortBy: orderBy,
|
||||
sortDir: orderDirection
|
||||
});
|
||||
};
|
||||
|
||||
const requestProjectAccess = async ({ permission, comment, projectId }: TProjectAccessRequestDTO) => {
|
||||
// check user belong to org
|
||||
await permissionService.getOrgPermission(
|
||||
permission.type,
|
||||
permission.id,
|
||||
permission.orgId,
|
||||
permission.authMethod,
|
||||
permission.orgId
|
||||
);
|
||||
|
||||
const projectMember = await permissionService
|
||||
.getProjectPermission({
|
||||
actor: permission.type,
|
||||
actorId: permission.id,
|
||||
projectId,
|
||||
actionProjectType: ActionProjectType.Any,
|
||||
actorAuthMethod: permission.authMethod,
|
||||
actorOrgId: permission.orgId
|
||||
})
|
||||
.catch(() => {
|
||||
return null;
|
||||
});
|
||||
if (projectMember) throw new BadRequestError({ message: "User already has access to the project" });
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const filteredProjectMembers = projectMembers
|
||||
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||
.map((el) => el.user.email!);
|
||||
const org = await orgDAL.findOne({ id: permission.orgId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const userDetails = await userDAL.findById(permission.id);
|
||||
const appCfg = getConfig();
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.ProjectAccessRequest,
|
||||
recipients: filteredProjectMembers,
|
||||
subjectLine: "Project Access Request",
|
||||
substitutions: {
|
||||
requesterName: `${userDetails.firstName} ${userDetails.lastName}`,
|
||||
requesterEmail: userDetails.email,
|
||||
projectName: project?.name,
|
||||
orgName: org?.name,
|
||||
note: comment,
|
||||
callback_url: `${appCfg.SITE_URL}/${project.type}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createProject,
|
||||
deleteProject,
|
||||
@@ -1364,6 +1452,8 @@ export const projectServiceFactory = ({
|
||||
loadProjectKmsBackup,
|
||||
getProjectKmsKeys,
|
||||
getProjectSlackConfig,
|
||||
updateProjectSlackConfig
|
||||
updateProjectSlackConfig,
|
||||
requestProjectAccess,
|
||||
searchProjects
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { ProjectType, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { ProjectType, SortDirection, TProjectKeys } from "@app/db/schemas";
|
||||
import { OrgServiceActor, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
@@ -158,3 +158,23 @@ export type TUpdateProjectSlackConfig = {
|
||||
isSecretRequestNotificationEnabled: boolean;
|
||||
secretRequestChannels: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export enum SearchProjectSortBy {
|
||||
NAME = "name"
|
||||
}
|
||||
|
||||
export type TSearchProjectsDTO = {
|
||||
permission: OrgServiceActor;
|
||||
name?: string;
|
||||
type?: ProjectType;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: SearchProjectSortBy;
|
||||
orderDirection?: SortDirection;
|
||||
};
|
||||
|
||||
export type TProjectAccessRequestDTO = {
|
||||
permission: OrgServiceActor;
|
||||
projectId: string;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,11 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
|
||||
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
|
||||
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
|
||||
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt")
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
|
||||
db
|
||||
.ref("isPlatformManagedCredentials")
|
||||
.withSchema(TableName.AppConnection)
|
||||
.as("connectionIsPlatformManagedCredentials")
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
@@ -60,6 +64,7 @@ const expandSecretSync = (
|
||||
connectionCreatedAt,
|
||||
connectionUpdatedAt,
|
||||
connectionVersion,
|
||||
connectionIsPlatformManagedCredentials,
|
||||
...el
|
||||
} = secretSync;
|
||||
|
||||
@@ -77,7 +82,8 @@ const expandSecretSync = (
|
||||
description: connectionDescription,
|
||||
createdAt: connectionCreatedAt,
|
||||
updatedAt: connectionUpdatedAt,
|
||||
version: connectionVersion
|
||||
version: connectionVersion,
|
||||
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials
|
||||
},
|
||||
folder: folder
|
||||
? {
|
||||
|
||||
@@ -119,14 +119,10 @@ export const secretSyncServiceFactory = ({
|
||||
{ destination, syncName, projectId }: TFindSecretSyncByNameDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const folders = await folderDAL.findByProjectId(projectId);
|
||||
|
||||
// we prevent conflicting names within a project so this will only return one at most
|
||||
const [secretSync] = await secretSyncDAL.find({
|
||||
// we prevent conflicting names within a project
|
||||
const secretSync = await secretSyncDAL.findOne({
|
||||
name: syncName,
|
||||
$in: {
|
||||
folderId: folders.map((folder) => folder.id)
|
||||
}
|
||||
projectId
|
||||
});
|
||||
|
||||
if (!secretSync)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Job } from "bullmq";
|
||||
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { AuditLogInfo } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { QueueJobs } from "@app/queue";
|
||||
import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema";
|
||||
import {
|
||||
@@ -134,8 +134,6 @@ export type TDeleteSecretSyncDTO = {
|
||||
removeSecrets: boolean;
|
||||
};
|
||||
|
||||
type AuditLogInfo = Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
Pending = "pending",
|
||||
Running = "running",
|
||||
|
||||
@@ -35,15 +35,25 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
const data = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...SecretsV2Schema.parse(el),
|
||||
isRotatedSecret: Boolean(el.rotationId),
|
||||
rotationId: el.rotationId
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
@@ -79,6 +89,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
@@ -87,7 +102,8 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"));
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
@@ -98,7 +114,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
const data = sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...SecretsV2Schema.parse(el),
|
||||
rotationId: el.rotationId,
|
||||
isRotatedSecret: Boolean(el.rotationId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
@@ -332,6 +353,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
|
||||
const query = (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.whereIn("folderId", folderIds)
|
||||
.where((bd) => {
|
||||
if (filters?.search) {
|
||||
@@ -414,6 +440,11 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.where((qb) => {
|
||||
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
||||
filters.metadataFilter.forEach((meta) => {
|
||||
@@ -444,6 +475,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping))
|
||||
.where((bd) => {
|
||||
const slugs = filters?.tagSlugs?.filter(Boolean);
|
||||
if (slugs && slugs.length > 0) {
|
||||
@@ -472,7 +504,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
const data = sqlNestRelationships({
|
||||
data: secs,
|
||||
key: "id",
|
||||
parentMapper: (el) => ({ _id: el.id, ...SecretsV2Schema.parse(el) }),
|
||||
parentMapper: (el) => ({
|
||||
_id: el.id,
|
||||
...SecretsV2Schema.parse(el),
|
||||
rotationId: el.rotationId,
|
||||
isRotatedSecret: Boolean(el.rotationId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
@@ -511,6 +548,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
try {
|
||||
const secrets = await (tx || db.replicaNode())(TableName.SecretV2)
|
||||
.where({ folderId })
|
||||
|
||||
.where((bd) => {
|
||||
query.forEach((el) => {
|
||||
if (el.type === SecretType.Personal && !el.userId) {
|
||||
@@ -522,10 +560,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
userId: el.type === SecretType.Personal ? el.userId : null
|
||||
});
|
||||
});
|
||||
});
|
||||
return secrets;
|
||||
})
|
||||
.leftJoin(
|
||||
TableName.SecretRotationV2SecretMapping,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
return secrets.map((secret) => ({
|
||||
...secret,
|
||||
isRotatedSecret: Boolean(secret.rotationId)
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "find by blind indexes" });
|
||||
throw new DatabaseError({ error, name: "find by secret keys" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -666,6 +666,8 @@ export const reshapeBridgeSecret = (
|
||||
name: string;
|
||||
}[];
|
||||
secretMetadata?: ResourceMetadataDTO;
|
||||
isRotatedSecret?: boolean;
|
||||
rotationId?: string;
|
||||
},
|
||||
secretValueHidden: boolean
|
||||
) => ({
|
||||
@@ -695,7 +697,8 @@ export const reshapeBridgeSecret = (
|
||||
secretMetadata: secret.secretMetadata,
|
||||
createdAt: secret.createdAt,
|
||||
updatedAt: secret.updatedAt,
|
||||
|
||||
isRotatedSecret: secret.isRotatedSecret,
|
||||
rotationId: secret.rotationId,
|
||||
...(secretValueHidden
|
||||
? {
|
||||
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
|
||||
|
||||
@@ -25,6 +25,7 @@ import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-app
|
||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { diff, groupBy } from "@app/lib/fn";
|
||||
import { setKnexStringValue } from "@app/lib/knex";
|
||||
@@ -416,6 +417,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
if (!sharedSecretToModify)
|
||||
throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` });
|
||||
if (sharedSecretToModify.isRotatedSecret && (inputSecret.newSecretName || inputSecret.secretValue))
|
||||
throw new BadRequestError({ message: "Cannot update rotated secret name or value" });
|
||||
secretId = sharedSecretToModify.id;
|
||||
secret = sharedSecretToModify;
|
||||
}
|
||||
@@ -626,66 +629,79 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
);
|
||||
|
||||
const deletedSecret = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkDelete({
|
||||
projectId,
|
||||
folderId,
|
||||
actorId,
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: [
|
||||
{
|
||||
type: inputSecret.type as SecretType,
|
||||
secretKey: inputSecret.secretName
|
||||
}
|
||||
],
|
||||
tx
|
||||
})
|
||||
);
|
||||
try {
|
||||
const deletedSecret = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkDelete({
|
||||
projectId,
|
||||
folderId,
|
||||
actorId,
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: [
|
||||
{
|
||||
type: inputSecret.type as SecretType,
|
||||
secretKey: inputSecret.secretName
|
||||
}
|
||||
],
|
||||
tx
|
||||
})
|
||||
);
|
||||
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
if (inputSecret.type === SecretType.Shared) {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
secretPath,
|
||||
actorId,
|
||||
actor,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretToDelete.key,
|
||||
secretTags: secretToDelete.tags?.map((el) => el.slug)
|
||||
}
|
||||
);
|
||||
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretToDelete.key,
|
||||
secretTags: secretToDelete.tags?.map((el) => el.slug)
|
||||
{
|
||||
...deletedSecret[0],
|
||||
value: deletedSecret[0].encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
|
||||
: "",
|
||||
comment: deletedSecret[0].encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
|
||||
: ""
|
||||
},
|
||||
secretValueHidden
|
||||
);
|
||||
} catch (err) {
|
||||
// deferred errors aren't return as DatabaseError
|
||||
const error = err as { code: string; table: string };
|
||||
if (
|
||||
error?.code === DatabaseErrorCode.ForeignKeyViolation &&
|
||||
error?.table === TableName.SecretRotationV2SecretMapping
|
||||
) {
|
||||
throw new BadRequestError({ message: "Cannot delete rotated secrets" });
|
||||
}
|
||||
);
|
||||
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
{
|
||||
...deletedSecret[0],
|
||||
value: deletedSecret[0].encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedValue }).toString()
|
||||
: "",
|
||||
comment: deletedSecret[0].encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: deletedSecret[0].encryptedComment }).toString()
|
||||
: ""
|
||||
},
|
||||
secretValueHidden
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// get unique secrets count for multiple envs
|
||||
@@ -948,6 +964,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
projectId
|
||||
});
|
||||
|
||||
// scott: if any of this changes it also needs to be mirrored in secret rotation for getting dashboard secrets
|
||||
const decryptedSecrets = secrets
|
||||
.filter((el) => {
|
||||
const canDescribeSecret = hasSecretReadValueOrDescribePermission(
|
||||
@@ -1235,21 +1252,24 @@ export const secretV2BridgeServiceFactory = ({
|
||||
: secretVersionDAL
|
||||
.findOne({
|
||||
folderId,
|
||||
version,
|
||||
type: secretType,
|
||||
userId: secretType === SecretType.Personal ? actorId : null,
|
||||
key: secretName
|
||||
})
|
||||
.then((el) =>
|
||||
SecretsV2Schema.extend({
|
||||
tags: z
|
||||
.object({ slug: z.string(), name: z.string(), id: z.string(), color: z.string() })
|
||||
.array()
|
||||
.default([])
|
||||
.optional()
|
||||
}).parse({
|
||||
...el,
|
||||
id: el.secretId
|
||||
})
|
||||
el
|
||||
? SecretsV2Schema.extend({
|
||||
tags: z
|
||||
.object({ slug: z.string(), name: z.string(), id: z.string(), color: z.string() })
|
||||
.array()
|
||||
.default([])
|
||||
.optional()
|
||||
}).parse({
|
||||
...el,
|
||||
id: el.secretId
|
||||
})
|
||||
: undefined
|
||||
));
|
||||
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
@@ -1669,6 +1689,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretTags: el.tags.map((i) => i.slug)
|
||||
})
|
||||
);
|
||||
|
||||
if (el.isRotatedSecret) {
|
||||
const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key);
|
||||
|
||||
if (input && (input.newSecretName || input.secretValue))
|
||||
throw new BadRequestError({ message: `Cannot update rotated secret name or value: ${el.key}` });
|
||||
}
|
||||
});
|
||||
|
||||
// get all tags
|
||||
@@ -1971,61 +1998,76 @@ export const secretV2BridgeServiceFactory = ({
|
||||
);
|
||||
});
|
||||
|
||||
const secretsDeleted = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkDelete({
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
|
||||
secretKey,
|
||||
type: type || SecretType.Shared
|
||||
})),
|
||||
projectId,
|
||||
folderId,
|
||||
actorId,
|
||||
tx
|
||||
})
|
||||
);
|
||||
|
||||
// await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
actor,
|
||||
actorId,
|
||||
secretPath,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
return secretsDeleted.map((el) => {
|
||||
const secretToDeleteMatch = secretsToDelete.find(
|
||||
(i) => i.key === el.key && (i.type || SecretType.Shared) === el.type
|
||||
try {
|
||||
const secretsDeleted = await secretDAL.transaction(async (tx) =>
|
||||
fnSecretBulkDelete({
|
||||
secretDAL,
|
||||
secretQueueService,
|
||||
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
|
||||
secretKey,
|
||||
type: type || SecretType.Shared
|
||||
})),
|
||||
projectId,
|
||||
folderId,
|
||||
actorId,
|
||||
tx
|
||||
})
|
||||
);
|
||||
|
||||
const secretValueHidden =
|
||||
!secretToDeleteMatch ||
|
||||
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
await snapshotService.performSnapshot(folderId);
|
||||
await secretQueueService.syncSecrets({
|
||||
actor,
|
||||
actorId,
|
||||
secretPath,
|
||||
projectId,
|
||||
orgId: actorOrgId,
|
||||
environmentSlug: folder.environment.slug
|
||||
});
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
return secretsDeleted.map((el) => {
|
||||
const secretToDeleteMatch = secretsToDelete.find(
|
||||
(i) => i.key === el.key && (i.type || SecretType.Shared) === el.type
|
||||
);
|
||||
|
||||
const secretValueHidden =
|
||||
!secretToDeleteMatch ||
|
||||
!hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: el.key,
|
||||
secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
|
||||
});
|
||||
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: el.key,
|
||||
secretTags: secretToDeleteMatch.tags?.map((i) => i.slug)
|
||||
});
|
||||
{
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
|
||||
: ""
|
||||
},
|
||||
secretValueHidden
|
||||
);
|
||||
});
|
||||
} catch (err) {
|
||||
// deferred errors aren't return as DatabaseError
|
||||
const error = err as { code: string; table: string };
|
||||
if (
|
||||
error?.code === DatabaseErrorCode.ForeignKeyViolation &&
|
||||
error?.table === TableName.SecretRotationV2SecretMapping
|
||||
) {
|
||||
throw new BadRequestError({ message: "Cannot delete rotated secrets" });
|
||||
}
|
||||
|
||||
return reshapeBridgeSecret(
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
{
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
},
|
||||
secretValueHidden
|
||||
);
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const getSecretVersions = async ({
|
||||
@@ -2197,6 +2239,10 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
|
||||
|
||||
sourceSecrets.forEach((secret) => {
|
||||
if (secret.isRotatedSecret) {
|
||||
throw new BadRequestError({ message: `Cannot move rotated secret: ${secret.key}` });
|
||||
}
|
||||
|
||||
for (const sourceAction of sourceActions) {
|
||||
if (
|
||||
sourceAction === ProjectPermissionSecretActions.DescribeSecret ||
|
||||
|
||||
@@ -132,7 +132,7 @@ export type TUpdateManySecretDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
secrets: {
|
||||
secretKey: string;
|
||||
newSecretName?: string;
|
||||
secretValue: string;
|
||||
secretValue?: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tagIds?: string[];
|
||||
|
||||
@@ -1558,6 +1558,7 @@ export const secretServiceFactory = ({
|
||||
actorOrgId,
|
||||
actor,
|
||||
actorId,
|
||||
version,
|
||||
expandSecretReferences,
|
||||
type,
|
||||
secretName
|
||||
|
||||
@@ -298,7 +298,7 @@ export type TUpdateManySecretRawDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
secrets: {
|
||||
secretKey: string;
|
||||
newSecretName?: string;
|
||||
secretValue: string;
|
||||
secretValue?: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tagIds?: string[];
|
||||
|
||||
@@ -40,7 +40,10 @@ export enum SmtpTemplates {
|
||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
||||
ExternalImportStarted = "externalImportStarted.handlebars",
|
||||
SecretRequestCompleted = "secretRequestCompleted.handlebars"
|
||||
SecretRequestCompleted = "secretRequestCompleted.handlebars",
|
||||
SecretRotationFailed = "secretRotationFailed.handlebars",
|
||||
ProjectAccessRequest = "projectAccess.handlebars",
|
||||
OrgAdminProjectDirectAccess = "orgAdminProjectGrantAccess.handlebars"
|
||||
}
|
||||
|
||||
export enum SmtpHost {
|
||||
|
||||
@@ -49,4 +49,4 @@
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Organization admin issued direct access to project</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<p>The organization admin {{email}} has granted direct access to the project "{{projectName}}".</p>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Project Access Request</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
<h2>You have a new project access request!</h2>
|
||||
<ul>
|
||||
<li>Requester Name: "{{requesterName}}"</li>
|
||||
<li>Requester Email: "{{requesterEmail}}"</li>
|
||||
<li>Project Name: "{{projectName}}"</li>
|
||||
<li>Organization Name: "{{orgName}}"</li>
|
||||
<li>User Note: "{{note}}"</li>
|
||||
</ul>
|
||||
<p>
|
||||
Please click on the link below to grant access
|
||||
</p>
|
||||
<a href="{{callback_url}}">Grant Access</a>
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Your {{rotationType}} Rotation "{{rotationName}}" Failed to Rotate</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>Infisical</h2>
|
||||
|
||||
<div>
|
||||
<p>{{content}}</p>
|
||||
<a href="{{rotationUrl}}?search={{rotationName}}&secretPath={{secretPath}}">
|
||||
View in Infisical to see the cause of failure.
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div>
|
||||
<p><strong>Name</strong>: {{rotationName}}</p>
|
||||
<p><strong>Type</strong>: {{rotationType}}</p>
|
||||
<p><strong>Project</strong>: {{projectName}}</p>
|
||||
<p><strong>Environment</strong>: {{environment}}</p>
|
||||
<p><strong>Secret Path</strong>: {{secretPath}}</p>
|
||||
</div>
|
||||
|
||||
{{emailFooter}}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user