mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 02:51:51 +00:00
Merge remote-tracking branch 'origin/main' into feat/kmip-client-management
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
export enum AppConnection {
|
||||
GitHub = "github",
|
||||
AWS = "aws",
|
||||
GCP = "gcp"
|
||||
Databricks = "databricks",
|
||||
GCP = "gcp",
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration"
|
||||
}
|
||||
|
||||
export enum AWSRegion {
|
||||
|
||||
@@ -5,12 +5,17 @@ import { TAppConnectionServiceFactoryDep } from "@app/services/app-connection/ap
|
||||
import { TAppConnection, TAppConnectionConfig } from "@app/services/app-connection/app-connection-types";
|
||||
import {
|
||||
AwsConnectionMethod,
|
||||
getAwsAppConnectionListItem,
|
||||
getAwsConnectionListItem,
|
||||
validateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
DatabricksConnectionMethod,
|
||||
getDatabricksConnectionListItem,
|
||||
validateDatabricksConnectionCredentials
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import {
|
||||
GcpConnectionMethod,
|
||||
getGcpAppConnectionListItem,
|
||||
getGcpConnectionListItem,
|
||||
validateGcpConnectionCredentials
|
||||
} from "@app/services/app-connection/gcp";
|
||||
import {
|
||||
@@ -20,10 +25,26 @@ import {
|
||||
} from "@app/services/app-connection/github";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
getAzureAppConfigurationConnectionListItem,
|
||||
validateAzureAppConfigurationConnectionCredentials
|
||||
} from "./azure-app-configuration";
|
||||
import {
|
||||
AzureKeyVaultConnectionMethod,
|
||||
getAzureKeyVaultConnectionListItem,
|
||||
validateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
|
||||
export const listAppConnectionOptions = () => {
|
||||
return [getAwsAppConnectionListItem(), getGitHubConnectionListItem(), getGcpAppConnectionListItem()].sort((a, b) =>
|
||||
a.name.localeCompare(b.name)
|
||||
);
|
||||
return [
|
||||
getAwsConnectionListItem(),
|
||||
getGitHubConnectionListItem(),
|
||||
getGcpConnectionListItem(),
|
||||
getAzureKeyVaultConnectionListItem(),
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getDatabricksConnectionListItem()
|
||||
].sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
export const encryptAppConnectionCredentials = async ({
|
||||
@@ -75,10 +96,16 @@ export const validateAppConnectionCredentials = async (
|
||||
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);
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection ${app}`);
|
||||
@@ -89,6 +116,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
switch (method) {
|
||||
case GitHubConnectionMethod.App:
|
||||
return "GitHub App";
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
case GitHubConnectionMethod.OAuth:
|
||||
return "OAuth";
|
||||
case AwsConnectionMethod.AccessKey:
|
||||
@@ -97,6 +126,8 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
return "Assume Role";
|
||||
case GcpConnectionMethod.ServiceAccountImpersonation:
|
||||
return "Service Account Impersonation";
|
||||
case DatabricksConnectionMethod.ServicePrincipal:
|
||||
return "Service Principal";
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled App Connection Method: ${method}`);
|
||||
|
||||
@@ -3,5 +3,8 @@ import { AppConnection } from "./app-connection-enums";
|
||||
export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AWS]: "AWS",
|
||||
[AppConnection.GitHub]: "GitHub",
|
||||
[AppConnection.GCP]: "GCP"
|
||||
[AppConnection.GCP]: "GCP",
|
||||
[AppConnection.AzureKeyVault]: "Azure Key Vault",
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.Databricks]: "Databricks"
|
||||
};
|
||||
|
||||
@@ -23,11 +23,15 @@ import {
|
||||
TValidateAppConnectionCredentials
|
||||
} from "@app/services/app-connection/app-connection-types";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "@app/services/app-connection/aws";
|
||||
import { ValidateDatabricksConnectionCredentialsSchema } from "@app/services/app-connection/databricks";
|
||||
import { databricksConnectionService } from "@app/services/app-connection/databricks/databricks-connection-service";
|
||||
import { ValidateGitHubConnectionCredentialsSchema } from "@app/services/app-connection/github";
|
||||
import { githubConnectionService } from "@app/services/app-connection/github/github-connection-service";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "./app-connection-dal";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureKeyVaultConnectionCredentialsSchema } from "./azure-key-vault";
|
||||
import { ValidateGcpConnectionCredentialsSchema } from "./gcp";
|
||||
import { gcpConnectionService } from "./gcp/gcp-connection-service";
|
||||
|
||||
@@ -42,7 +46,10 @@ export type TAppConnectionServiceFactory = ReturnType<typeof appConnectionServic
|
||||
const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAppConnectionCredentials> = {
|
||||
[AppConnection.AWS]: ValidateAwsConnectionCredentialsSchema,
|
||||
[AppConnection.GitHub]: ValidateGitHubConnectionCredentialsSchema,
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema
|
||||
[AppConnection.GCP]: ValidateGcpConnectionCredentialsSchema,
|
||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema
|
||||
};
|
||||
|
||||
export const appConnectionServiceFactory = ({
|
||||
@@ -361,6 +368,7 @@ export const appConnectionServiceFactory = ({
|
||||
connectAppConnectionById,
|
||||
listAvailableAppConnectionsForUser,
|
||||
github: githubConnectionService(connectAppConnectionById),
|
||||
gcp: gcpConnectionService(connectAppConnectionById)
|
||||
gcp: gcpConnectionService(connectAppConnectionById),
|
||||
databricks: databricksConnectionService(connectAppConnectionById, appConnectionDAL, kmsService)
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,6 +4,12 @@ import {
|
||||
TAwsConnectionInput,
|
||||
TValidateAwsConnectionCredentials
|
||||
} from "@app/services/app-connection/aws";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig,
|
||||
TDatabricksConnectionInput,
|
||||
TValidateDatabricksConnectionCredentials
|
||||
} from "@app/services/app-connection/databricks";
|
||||
import {
|
||||
TGitHubConnection,
|
||||
TGitHubConnectionConfig,
|
||||
@@ -11,11 +17,37 @@ import {
|
||||
TValidateGitHubConnectionCredentials
|
||||
} from "@app/services/app-connection/github";
|
||||
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureAppConfigurationConnectionConfig,
|
||||
TAzureAppConfigurationConnectionInput,
|
||||
TValidateAzureAppConfigurationConnectionCredentials
|
||||
} from "./azure-app-configuration";
|
||||
import {
|
||||
TAzureKeyVaultConnection,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
TAzureKeyVaultConnectionInput,
|
||||
TValidateAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault";
|
||||
import { TGcpConnection, TGcpConnectionConfig, TGcpConnectionInput, TValidateGcpConnectionCredentials } from "./gcp";
|
||||
|
||||
export type TAppConnection = { id: string } & (TAwsConnection | TGitHubConnection | TGcpConnection);
|
||||
export type TAppConnection = { id: string } & (
|
||||
| TAwsConnection
|
||||
| TGitHubConnection
|
||||
| TGcpConnection
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TDatabricksConnection
|
||||
);
|
||||
|
||||
export type TAppConnectionInput = { id: string } & (TAwsConnectionInput | TGitHubConnectionInput | TGcpConnectionInput);
|
||||
export type TAppConnectionInput = { id: string } & (
|
||||
| TAwsConnectionInput
|
||||
| TGitHubConnectionInput
|
||||
| TGcpConnectionInput
|
||||
| TAzureKeyVaultConnectionInput
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
);
|
||||
|
||||
export type TCreateAppConnectionDTO = Pick<
|
||||
TAppConnectionInput,
|
||||
@@ -26,9 +58,18 @@ export type TUpdateAppConnectionDTO = Partial<Omit<TCreateAppConnectionDTO, "met
|
||||
connectionId: string;
|
||||
};
|
||||
|
||||
export type TAppConnectionConfig = TAwsConnectionConfig | TGitHubConnectionConfig | TGcpConnectionConfig;
|
||||
export type TAppConnectionConfig =
|
||||
| TAwsConnectionConfig
|
||||
| TGitHubConnectionConfig
|
||||
| TGcpConnectionConfig
|
||||
| TAzureKeyVaultConnectionConfig
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TDatabricksConnectionConfig;
|
||||
|
||||
export type TValidateAppConnectionCredentials =
|
||||
| TValidateAwsConnectionCredentials
|
||||
| TValidateGitHubConnectionCredentials
|
||||
| TValidateGcpConnectionCredentials;
|
||||
| TValidateGcpConnectionCredentials
|
||||
| TValidateAzureKeyVaultConnectionCredentials
|
||||
| TValidateAzureAppConfigurationConnectionCredentials
|
||||
| TValidateDatabricksConnectionCredentials;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { AppConnection, AWSRegion } from "@app/services/app-connection/app-conne
|
||||
import { AwsConnectionMethod } from "./aws-connection-enums";
|
||||
import { TAwsConnectionConfig } from "./aws-connection-types";
|
||||
|
||||
export const getAwsAppConnectionListItem = () => {
|
||||
export const getAwsConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_AWS_ACCESS_KEY_ID } = getConfig();
|
||||
|
||||
return {
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export enum AzureAppConfigurationConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
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 { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureAppConfigurationConnectionConfig
|
||||
} from "./azure-app-configuration-connection-types";
|
||||
|
||||
export const getAzureAppConfigurationConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
|
||||
|
||||
return {
|
||||
name: "Azure App Configuration" as const,
|
||||
app: AppConnection.AzureAppConfiguration as const,
|
||||
methods: Object.values(AzureAppConfigurationConnectionMethod) as [AzureAppConfigurationConnectionMethod.OAuth],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||
};
|
||||
};
|
||||
|
||||
export const validateAzureAppConfigurationConnectionCredentials = async (
|
||||
config: TAzureAppConfigurationConnectionConfig
|
||||
) => {
|
||||
const { credentials: inputCredentials, method } = config;
|
||||
|
||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||
|
||||
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://azconfig.io/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case AzureAppConfigurationConnectionMethod.OAuth:
|
||||
return {
|
||||
tenantId: inputCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureAppConfigurationConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AzureAppConfigurationConnectionMethod } from "./azure-app-configuration-connection-enums";
|
||||
|
||||
export const AzureAppConfigurationConnectionOAuthInputCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required"),
|
||||
tenantId: z.string().trim().optional()
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
tenantId: z.string().optional(),
|
||||
accessToken: z.string(),
|
||||
refreshToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureAppConfigurationConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureAppConfigurationConnectionMethod.OAuth)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureAppConfiguration).method),
|
||||
credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAzureAppConfigurationConnectionSchema = ValidateAzureAppConfigurationConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration)
|
||||
);
|
||||
|
||||
export const UpdateAzureAppConfigurationConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureAppConfigurationConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureAppConfiguration).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureAppConfiguration));
|
||||
|
||||
const BaseAzureAppConfigurationConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.AzureAppConfiguration)
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationConnectionSchema = z.intersection(
|
||||
BaseAzureAppConfigurationConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.OAuth),
|
||||
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedAzureAppConfigurationConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAzureAppConfigurationConnectionSchema.extend({
|
||||
method: z.literal(AzureAppConfigurationConnectionMethod.OAuth),
|
||||
credentials: AzureAppConfigurationConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const AzureAppConfigurationConnectionListItemSchema = z.object({
|
||||
name: z.literal("Azure App Configuration"),
|
||||
app: z.literal(AppConnection.AzureAppConfiguration),
|
||||
methods: z.nativeEnum(AzureAppConfigurationConnectionMethod).array(),
|
||||
oauthClientId: z.string().optional()
|
||||
});
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureAppConfigurationConnectionOAuthOutputCredentialsSchema,
|
||||
AzureAppConfigurationConnectionSchema,
|
||||
CreateAzureAppConfigurationConnectionSchema,
|
||||
ValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
} from "./azure-app-configuration-connection-schemas";
|
||||
|
||||
export type TAzureAppConfigurationConnection = z.infer<typeof AzureAppConfigurationConnectionSchema>;
|
||||
|
||||
export type TAzureAppConfigurationConnectionInput = z.infer<typeof CreateAzureAppConfigurationConnectionSchema> & {
|
||||
app: AppConnection.AzureAppConfiguration;
|
||||
};
|
||||
|
||||
export type TValidateAzureAppConfigurationConnectionCredentials =
|
||||
typeof ValidateAzureAppConfigurationConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureAppConfigurationConnectionConfig = DiscriminativePick<
|
||||
TAzureAppConfigurationConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type ExchangeCodeAzureResponse = {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
id_token: string;
|
||||
};
|
||||
|
||||
export type TAzureAppConfigurationConnectionCredentials = z.infer<
|
||||
typeof AzureAppConfigurationConnectionOAuthOutputCredentialsSchema
|
||||
>;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./azure-app-configuration-connection-enums";
|
||||
export * from "./azure-app-configuration-connection-fns";
|
||||
export * from "./azure-app-configuration-connection-schemas";
|
||||
export * from "./azure-app-configuration-connection-types";
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export enum AzureKeyVaultConnectionMethod {
|
||||
OAuth = "oauth"
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { AxiosError, AxiosResponse } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import {
|
||||
decryptAppConnectionCredentials,
|
||||
encryptAppConnectionCredentials,
|
||||
getAppConnectionMethodName
|
||||
} from "@app/services/app-connection/app-connection-fns";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums";
|
||||
import {
|
||||
ExchangeCodeAzureResponse,
|
||||
TAzureKeyVaultConnectionConfig,
|
||||
TAzureKeyVaultConnectionCredentials
|
||||
} from "./azure-key-vault-connection-types";
|
||||
|
||||
export const getAzureConnectionAccessToken = async (
|
||||
connectionId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
if (!appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID || !appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new BadRequestError({
|
||||
message: `Azure environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||
}
|
||||
|
||||
if (appConnection.app !== AppConnection.AzureKeyVault && appConnection.app !== AppConnection.AzureAppConfiguration) {
|
||||
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure Key Vault connection` });
|
||||
}
|
||||
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as TAzureKeyVaultConnectionCredentials;
|
||||
|
||||
const { data } = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", credentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "refresh_token",
|
||||
scope: `openid offline_access`,
|
||||
client_id: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: appCfg.INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
refresh_token: credentials.refreshToken
|
||||
})
|
||||
);
|
||||
|
||||
const accessExpiresAt = new Date();
|
||||
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
|
||||
|
||||
const updatedCredentials = {
|
||||
...credentials,
|
||||
accessToken: data.access_token,
|
||||
expiresAt: accessExpiresAt.getTime(),
|
||||
refreshToken: data.refresh_token
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId: appConnection.orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.update(
|
||||
{ id: connectionId },
|
||||
{
|
||||
encryptedCredentials
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken: data.access_token
|
||||
};
|
||||
};
|
||||
|
||||
export const getAzureKeyVaultConnectionListItem = () => {
|
||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID } = getConfig();
|
||||
|
||||
return {
|
||||
name: "Azure Key Vault" as const,
|
||||
app: AppConnection.AzureKeyVault as const,
|
||||
methods: Object.values(AzureKeyVaultConnectionMethod) as [AzureKeyVaultConnectionMethod.OAuth],
|
||||
oauthClientId: INF_APP_CONNECTION_AZURE_CLIENT_ID
|
||||
};
|
||||
};
|
||||
|
||||
export const validateAzureKeyVaultConnectionCredentials = async (config: TAzureKeyVaultConnectionConfig) => {
|
||||
const { credentials: inputCredentials, method } = config;
|
||||
|
||||
const { INF_APP_CONNECTION_AZURE_CLIENT_ID, INF_APP_CONNECTION_AZURE_CLIENT_SECRET, SITE_URL } = getConfig();
|
||||
|
||||
if (!INF_APP_CONNECTION_AZURE_CLIENT_ID || !INF_APP_CONNECTION_AZURE_CLIENT_SECRET) {
|
||||
throw new InternalServerError({
|
||||
message: `Azure ${getAppConnectionMethodName(method)} environment variables have not been configured`
|
||||
});
|
||||
}
|
||||
|
||||
let tokenResp: AxiosResponse<ExchangeCodeAzureResponse> | null = null;
|
||||
let tokenError: AxiosError | null = null;
|
||||
|
||||
try {
|
||||
tokenResp = await request.post<ExchangeCodeAzureResponse>(
|
||||
IntegrationUrls.AZURE_TOKEN_URL.replace("common", inputCredentials.tenantId || "common"),
|
||||
new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code: inputCredentials.code,
|
||||
scope: `openid offline_access https://vault.azure.net/.default`,
|
||||
client_id: INF_APP_CONNECTION_AZURE_CLIENT_ID,
|
||||
client_secret: INF_APP_CONNECTION_AZURE_CLIENT_SECRET,
|
||||
redirect_uri: `${SITE_URL}/organization/app-connections/azure/oauth/callback`
|
||||
})
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
tokenError = e;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenError) {
|
||||
if (tokenError instanceof AxiosError) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to get access token: ${
|
||||
(tokenError?.response?.data as { error_description?: string })?.error_description || "Unknown error"
|
||||
}`
|
||||
});
|
||||
} else {
|
||||
throw new InternalServerError({
|
||||
message: "Failed to get access token"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!tokenResp) {
|
||||
throw new InternalServerError({
|
||||
message: `Failed to get access token: Token was empty with no error`
|
||||
});
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case AzureKeyVaultConnectionMethod.OAuth:
|
||||
return {
|
||||
tenantId: inputCredentials.tenantId,
|
||||
accessToken: tokenResp.data.access_token,
|
||||
refreshToken: tokenResp.data.refresh_token,
|
||||
expiresAt: Date.now() + tokenResp.data.expires_in * 1000
|
||||
};
|
||||
default:
|
||||
throw new InternalServerError({
|
||||
message: `Unhandled Azure connection method: ${method as AzureKeyVaultConnectionMethod}`
|
||||
});
|
||||
}
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AzureKeyVaultConnectionMethod } from "./azure-key-vault-connection-enums";
|
||||
|
||||
export const AzureKeyVaultConnectionOAuthInputCredentialsSchema = z.object({
|
||||
code: z.string().trim().min(1, "OAuth code required"),
|
||||
tenantId: z.string().trim().optional()
|
||||
});
|
||||
|
||||
export const AzureKeyVaultConnectionOAuthOutputCredentialsSchema = z.object({
|
||||
tenantId: z.string().optional(),
|
||||
accessToken: z.string(),
|
||||
refreshToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
});
|
||||
|
||||
export const ValidateAzureKeyVaultConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureKeyVaultConnectionMethod.OAuth)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureKeyVault).method),
|
||||
credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAzureKeyVaultConnectionSchema = ValidateAzureKeyVaultConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureKeyVault)
|
||||
);
|
||||
|
||||
export const UpdateAzureKeyVaultConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureKeyVaultConnectionOAuthInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureKeyVault).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureKeyVault));
|
||||
|
||||
const BaseAzureKeyVaultConnectionSchema = BaseAppConnectionSchema.extend({
|
||||
app: z.literal(AppConnection.AzureKeyVault)
|
||||
});
|
||||
|
||||
export const AzureKeyVaultConnectionSchema = z.intersection(
|
||||
BaseAzureKeyVaultConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.OAuth),
|
||||
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedAzureKeyVaultConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAzureKeyVaultConnectionSchema.extend({
|
||||
method: z.literal(AzureKeyVaultConnectionMethod.OAuth),
|
||||
credentials: AzureKeyVaultConnectionOAuthOutputCredentialsSchema.pick({
|
||||
tenantId: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const AzureKeyVaultConnectionListItemSchema = z.object({
|
||||
name: z.literal("Azure Key Vault"),
|
||||
app: z.literal(AppConnection.AzureKeyVault),
|
||||
methods: z.nativeEnum(AzureKeyVaultConnectionMethod).array(),
|
||||
oauthClientId: z.string().optional()
|
||||
});
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureKeyVaultConnectionOAuthOutputCredentialsSchema,
|
||||
AzureKeyVaultConnectionSchema,
|
||||
CreateAzureKeyVaultConnectionSchema,
|
||||
ValidateAzureKeyVaultConnectionCredentialsSchema
|
||||
} from "./azure-key-vault-connection-schemas";
|
||||
|
||||
export type TAzureKeyVaultConnection = z.infer<typeof AzureKeyVaultConnectionSchema>;
|
||||
|
||||
export type TAzureKeyVaultConnectionInput = z.infer<typeof CreateAzureKeyVaultConnectionSchema> & {
|
||||
app: AppConnection.AzureKeyVault;
|
||||
};
|
||||
|
||||
export type TValidateAzureKeyVaultConnectionCredentials = typeof ValidateAzureKeyVaultConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureKeyVaultConnectionConfig = DiscriminativePick<
|
||||
TAzureKeyVaultConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type ExchangeCodeAzureResponse = {
|
||||
token_type: string;
|
||||
scope: string;
|
||||
expires_in: number;
|
||||
ext_expires_in: number;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
id_token: string;
|
||||
};
|
||||
|
||||
export type TAzureKeyVaultConnectionCredentials = z.infer<typeof AzureKeyVaultConnectionOAuthOutputCredentialsSchema>;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./azure-key-vault-connection-enums";
|
||||
export * from "./azure-key-vault-connection-fns";
|
||||
export * from "./azure-key-vault-connection-schemas";
|
||||
export * from "./azure-key-vault-connection-types";
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum DatabricksConnectionMethod {
|
||||
ServicePrincipal = "service-principal"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { encryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { DatabricksConnectionMethod } from "./databricks-connection-enums";
|
||||
import {
|
||||
TAuthorizeDatabricksConnection,
|
||||
TDatabricksConnection,
|
||||
TDatabricksConnectionConfig
|
||||
} from "./databricks-connection-types";
|
||||
|
||||
export const getDatabricksConnectionListItem = () => {
|
||||
return {
|
||||
name: "Databricks" as const,
|
||||
app: AppConnection.Databricks as const,
|
||||
methods: Object.values(DatabricksConnectionMethod) as [DatabricksConnectionMethod.ServicePrincipal]
|
||||
};
|
||||
};
|
||||
|
||||
const authorizeDatabricksConnection = async ({
|
||||
clientId,
|
||||
clientSecret,
|
||||
workspaceUrl
|
||||
}: Pick<TDatabricksConnection["credentials"], "workspaceUrl" | "clientId" | "clientSecret">) => {
|
||||
const { data } = await request.post<TAuthorizeDatabricksConnection>(
|
||||
`${removeTrailingSlash(workspaceUrl)}/oidc/v1/token`,
|
||||
"grant_type=client_credentials&scope=all-apis",
|
||||
{
|
||||
auth: {
|
||||
username: clientId,
|
||||
password: clientSecret
|
||||
},
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { accessToken: data.access_token, expiresAt: data.expires_in * 1000 + Date.now() };
|
||||
};
|
||||
|
||||
export const getDatabricksConnectionAccessToken = async (
|
||||
{ id, orgId, credentials }: TDatabricksConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const { clientSecret, clientId, workspaceUrl, accessToken, expiresAt } = credentials;
|
||||
|
||||
// get new token if less than 10 minutes from expiry
|
||||
if (Date.now() < expiresAt - 10_000) {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
const authData = await authorizeDatabricksConnection({ clientId, clientSecret, workspaceUrl });
|
||||
|
||||
const updatedCredentials: TDatabricksConnection["credentials"] = {
|
||||
...credentials,
|
||||
...authData
|
||||
};
|
||||
|
||||
const encryptedCredentials = await encryptAppConnectionCredentials({
|
||||
credentials: updatedCredentials,
|
||||
orgId,
|
||||
kmsService
|
||||
});
|
||||
|
||||
await appConnectionDAL.updateById(id, { encryptedCredentials });
|
||||
|
||||
return authData.accessToken;
|
||||
};
|
||||
|
||||
export const validateDatabricksConnectionCredentials = async (appConnection: TDatabricksConnectionConfig) => {
|
||||
const { credentials } = appConnection;
|
||||
|
||||
try {
|
||||
const { accessToken, expiresAt } = await authorizeDatabricksConnection(appConnection.credentials);
|
||||
|
||||
return {
|
||||
...credentials,
|
||||
accessToken,
|
||||
expiresAt
|
||||
};
|
||||
} catch (e: unknown) {
|
||||
throw new BadRequestError({
|
||||
message: `Unable to validate connection - verify credentials`
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { DatabricksConnectionMethod } from "./databricks-connection-enums";
|
||||
|
||||
export const DatabricksConnectionServicePrincipalInputCredentialsSchema = z.object({
|
||||
clientId: z.string().trim().min(1, "Client ID required"),
|
||||
clientSecret: z.string().trim().min(1, "Client Secret required"),
|
||||
workspaceUrl: z.string().trim().url().min(1, "Workspace URL required")
|
||||
});
|
||||
|
||||
export const DatabricksConnectionServicePrincipalOutputCredentialsSchema = z
|
||||
.object({
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number()
|
||||
})
|
||||
.merge(DatabricksConnectionServicePrincipalInputCredentialsSchema);
|
||||
|
||||
const BaseDatabricksConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.Databricks) });
|
||||
|
||||
export const DatabricksConnectionSchema = z.intersection(
|
||||
BaseDatabricksConnectionSchema,
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(DatabricksConnectionMethod.ServicePrincipal),
|
||||
credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema
|
||||
})
|
||||
])
|
||||
);
|
||||
|
||||
export const SanitizedDatabricksConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseDatabricksConnectionSchema.extend({
|
||||
method: z.literal(DatabricksConnectionMethod.ServicePrincipal),
|
||||
credentials: DatabricksConnectionServicePrincipalOutputCredentialsSchema.pick({
|
||||
clientId: true,
|
||||
workspaceUrl: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateDatabricksConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(DatabricksConnectionMethod.ServicePrincipal)
|
||||
.describe(AppConnections?.CREATE(AppConnection.Databricks).method),
|
||||
credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.Databricks).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateDatabricksConnectionSchema = ValidateDatabricksConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.Databricks)
|
||||
);
|
||||
|
||||
export const UpdateDatabricksConnectionSchema = z
|
||||
.object({
|
||||
credentials: DatabricksConnectionServicePrincipalInputCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.Databricks).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.Databricks));
|
||||
|
||||
export const DatabricksConnectionListItemSchema = z.object({
|
||||
name: z.literal("Databricks"),
|
||||
app: z.literal(AppConnection.Databricks),
|
||||
// the below is preferable but currently breaks with our zod to json schema parser
|
||||
// methods: z.tuple([z.literal(AwsConnectionMethod.ServicePrincipal), z.literal(AwsConnectionMethod.AccessKey)]),
|
||||
methods: z.nativeEnum(DatabricksConnectionMethod).array()
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks/databricks-connection-fns";
|
||||
import {
|
||||
TDatabricksConnection,
|
||||
TDatabricksListSecretScopesResponse
|
||||
} from "@app/services/app-connection/databricks/databricks-connection-types";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
app: AppConnection,
|
||||
connectionId: string,
|
||||
actor: OrgServiceActor
|
||||
) => Promise<TDatabricksConnection>;
|
||||
|
||||
const listDatabricksSecretScopes = async (
|
||||
appConnection: TDatabricksConnection,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const {
|
||||
credentials: { workspaceUrl }
|
||||
} = appConnection;
|
||||
|
||||
const accessToken = await getDatabricksConnectionAccessToken(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
const { data } = await request.get<TDatabricksListSecretScopesResponse>(
|
||||
`${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/scopes/list`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// not present in response if no scopes exists
|
||||
return data.scopes ?? [];
|
||||
};
|
||||
|
||||
export const databricksConnectionService = (
|
||||
getAppConnection: TGetAppConnectionFunc,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const listSecretScopes = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Databricks, connectionId, actor);
|
||||
|
||||
const secretScopes = await listDatabricksSecretScopes(appConnection, appConnectionDAL, kmsService);
|
||||
|
||||
return secretScopes;
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretScopes
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
|
||||
import {
|
||||
CreateDatabricksConnectionSchema,
|
||||
DatabricksConnectionSchema,
|
||||
ValidateDatabricksConnectionCredentialsSchema
|
||||
} from "./databricks-connection-schemas";
|
||||
|
||||
export type TDatabricksConnection = z.infer<typeof DatabricksConnectionSchema>;
|
||||
|
||||
export type TDatabricksConnectionInput = z.infer<typeof CreateDatabricksConnectionSchema> & {
|
||||
app: AppConnection.Databricks;
|
||||
};
|
||||
|
||||
export type TValidateDatabricksConnectionCredentials = typeof ValidateDatabricksConnectionCredentialsSchema;
|
||||
|
||||
export type TDatabricksConnectionConfig = DiscriminativePick<
|
||||
TDatabricksConnection,
|
||||
"method" | "app" | "credentials"
|
||||
> & {
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type TAuthorizeDatabricksConnection = {
|
||||
access_token: string;
|
||||
scope: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
};
|
||||
|
||||
export type TDatabricksListSecretScopesResponse = {
|
||||
scopes?: { name: string; backend_type: string; keyvault_metadata: { resource_id: string; dns_name: string } }[];
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./databricks-connection-enums";
|
||||
export * from "./databricks-connection-fns";
|
||||
export * from "./databricks-connection-schemas";
|
||||
export * from "./databricks-connection-types";
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
TGcpConnectionConfig
|
||||
} from "./gcp-connection-types";
|
||||
|
||||
export const getGcpAppConnectionListItem = () => {
|
||||
export const getGcpConnectionListItem = () => {
|
||||
return {
|
||||
name: "GCP" as const,
|
||||
app: AppConnection.GCP as const,
|
||||
|
||||
@@ -3,7 +3,8 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { ActionProjectType, ProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCmekActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import {
|
||||
TCmekDecryptDTO,
|
||||
@@ -44,17 +45,31 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Create, ProjectPermissionSub.Cmek);
|
||||
|
||||
const cmek = await kmsService.generateKmsKey({
|
||||
...dto,
|
||||
projectId,
|
||||
isReserved: false
|
||||
});
|
||||
try {
|
||||
const cmek = await kmsService.generateKmsKey({
|
||||
...dto,
|
||||
projectId,
|
||||
isReserved: false
|
||||
});
|
||||
|
||||
return cmek;
|
||||
return {
|
||||
...cmek,
|
||||
version: 1,
|
||||
encryptionAlgorithm: dto.encryptionAlgorithm
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `A KMS key with the name "${dto.name}" already exists for the project with ID "${projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const updateCmekById = async ({ keyId, ...data }: TUpdabteCmekByIdDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findById(keyId);
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
||||
|
||||
@@ -71,13 +86,27 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Edit, ProjectPermissionSub.Cmek);
|
||||
|
||||
const cmek = await kmsDAL.updateById(keyId, data);
|
||||
try {
|
||||
const cmek = await kmsDAL.updateById(keyId, data);
|
||||
|
||||
return cmek;
|
||||
return {
|
||||
...cmek,
|
||||
version: key.version,
|
||||
encryptionAlgorithm: key.encryptionAlgorithm
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `A KMS key with the name "${data.name!}" already exists for the project with ID "${key.projectId}"`
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCmekById = async (keyId: string, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findById(keyId);
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
||||
|
||||
@@ -94,9 +123,9 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Delete, ProjectPermissionSub.Cmek);
|
||||
|
||||
const cmek = kmsDAL.deleteById(keyId);
|
||||
await kmsDAL.deleteById(keyId);
|
||||
|
||||
return cmek;
|
||||
return key;
|
||||
};
|
||||
|
||||
const listCmeksByProjectId = async (
|
||||
@@ -120,15 +149,58 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
const { keys: cmeks, totalCount } = await kmsDAL.findKmsKeysByProjectId({ projectId, ...filters });
|
||||
const { keys: cmeks, totalCount } = await kmsDAL.listCmeksByProjectId({ projectId, ...filters });
|
||||
|
||||
return { cmeks, totalCount };
|
||||
};
|
||||
|
||||
const findCmekById = async (keyId: string, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
const findCmekByName = async (keyName: string, projectId: string, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findCmekByName(keyName, projectId);
|
||||
|
||||
if (!key)
|
||||
throw new NotFoundError({ message: `Key with name "${keyName}" not found for project with ID "${projectId}"` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
projectId: key.projectId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.KMS
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
const cmekEncrypt = async ({ keyId, plaintext }: TCmekEncryptDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
@@ -155,7 +227,7 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
const cmekDecrypt = async ({ keyId, ciphertext }: TCmekDecryptDTO, actor: OrgServiceActor) => {
|
||||
const key = await kmsDAL.findById(keyId);
|
||||
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID ${keyId} not found` });
|
||||
if (!key) throw new NotFoundError({ message: `Key with ID "${keyId}" not found` });
|
||||
|
||||
if (!key.projectId || key.isReserved) throw new BadRequestError({ message: "Key is not customer managed" });
|
||||
|
||||
@@ -185,6 +257,8 @@ export const cmekServiceFactory = ({ kmsService, kmsDAL, permissionService, proj
|
||||
deleteCmekById,
|
||||
listCmeksByProjectId,
|
||||
cmekEncrypt,
|
||||
cmekDecrypt
|
||||
cmekDecrypt,
|
||||
findCmekById,
|
||||
findCmekByName
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const twelveDigitRegex = /^\d{12}$/;
|
||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[\w-]+|role\/[\w-]+|\*)$/;
|
||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
|
||||
|
||||
export const validateAccountIds = z
|
||||
.string()
|
||||
|
||||
+47
-176
@@ -3,28 +3,21 @@ import axios, { AxiosError } from "axios";
|
||||
import https from "https";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
|
||||
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import {
|
||||
decryptSymmetric,
|
||||
encryptSymmetric,
|
||||
generateAsymmetricKeyPair,
|
||||
generateSymmetricKey,
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TIdentityKubernetesAuthDALFactory } from "./identity-kubernetes-auth-dal";
|
||||
import { extractK8sUsername } from "./identity-kubernetes-auth-fns";
|
||||
import {
|
||||
@@ -43,9 +36,9 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
|
||||
>;
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne" | "findById">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
|
||||
@@ -54,9 +47,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
identityKubernetesAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
identityAccessTokenDAL,
|
||||
orgBotDAL,
|
||||
permissionService,
|
||||
licenseService
|
||||
licenseService,
|
||||
kmsService
|
||||
}: TIdentityKubernetesAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
|
||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||
@@ -75,42 +68,21 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`,
|
||||
name: "OrgBotNotFound"
|
||||
});
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
|
||||
identityKubernetesAuth;
|
||||
|
||||
let caCert = "";
|
||||
if (encryptedCaCert && caCertIV && caCertTag) {
|
||||
caCert = decryptSymmetric({
|
||||
ciphertext: encryptedCaCert,
|
||||
iv: caCertIV,
|
||||
tag: caCertTag,
|
||||
key
|
||||
});
|
||||
if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
|
||||
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
||||
}
|
||||
|
||||
let tokenReviewerJwt = "";
|
||||
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
|
||||
tokenReviewerJwt = decryptSymmetric({
|
||||
ciphertext: encryptedTokenReviewerJwt,
|
||||
iv: tokenReviewerJwtIV,
|
||||
tag: tokenReviewerJwtTag,
|
||||
key
|
||||
});
|
||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||
tokenReviewerJwt = decryptor({
|
||||
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
}).toString();
|
||||
}
|
||||
|
||||
const { data } = await axios
|
||||
@@ -297,79 +269,25 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const orgBot = await orgBotDAL.transaction(async (tx) => {
|
||||
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
|
||||
if (doc) return doc;
|
||||
|
||||
const { privateKey, publicKey } = generateAsymmetricKeyPair();
|
||||
const key = generateSymmetricKey();
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: privateKeyIV,
|
||||
tag: privateKeyTag,
|
||||
encoding: privateKeyKeyEncoding,
|
||||
algorithm: privateKeyAlgorithm
|
||||
} = infisicalSymmetricEncypt(privateKey);
|
||||
const {
|
||||
ciphertext: encryptedSymmetricKey,
|
||||
iv: symmetricKeyIV,
|
||||
tag: symmetricKeyTag,
|
||||
encoding: symmetricKeyKeyEncoding,
|
||||
algorithm: symmetricKeyAlgorithm
|
||||
} = infisicalSymmetricEncypt(key);
|
||||
|
||||
return orgBotDAL.create(
|
||||
{
|
||||
name: "Infisical org bot",
|
||||
publicKey,
|
||||
privateKeyIV,
|
||||
encryptedPrivateKey,
|
||||
symmetricKeyIV,
|
||||
symmetricKeyTag,
|
||||
encryptedSymmetricKey,
|
||||
symmetricKeyAlgorithm,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
privateKeyTag,
|
||||
privateKeyAlgorithm,
|
||||
privateKeyKeyEncoding,
|
||||
symmetricKeyKeyEncoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||
const {
|
||||
ciphertext: encryptedTokenReviewerJwt,
|
||||
iv: tokenReviewerJwtIV,
|
||||
tag: tokenReviewerJwtTag
|
||||
} = encryptSymmetric(tokenReviewerJwt, key);
|
||||
|
||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityKubernetesAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
kubernetesHost,
|
||||
encryptedCaCert,
|
||||
caCertIV,
|
||||
caCertTag,
|
||||
encryptedTokenReviewerJwt,
|
||||
tokenReviewerJwtIV,
|
||||
tokenReviewerJwtTag,
|
||||
allowedNamespaces,
|
||||
allowedNames,
|
||||
allowedAudience,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||
encryptedKubernetesTokenReviewerJwt: encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob,
|
||||
encryptedKubernetesCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -455,61 +373,34 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
: undefined
|
||||
};
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`,
|
||||
name: "OrgBotNotFound"
|
||||
});
|
||||
}
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
if (caCert !== undefined) {
|
||||
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||
updateQuery.encryptedCaCert = encryptedCACert;
|
||||
updateQuery.caCertIV = caCertIV;
|
||||
updateQuery.caCertTag = caCertTag;
|
||||
updateQuery.encryptedKubernetesCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob;
|
||||
}
|
||||
|
||||
if (tokenReviewerJwt !== undefined) {
|
||||
const {
|
||||
ciphertext: encryptedTokenReviewerJwt,
|
||||
iv: tokenReviewerJwtIV,
|
||||
tag: tokenReviewerJwtTag
|
||||
} = encryptSymmetric(tokenReviewerJwt, key);
|
||||
updateQuery.encryptedTokenReviewerJwt = encryptedTokenReviewerJwt;
|
||||
updateQuery.tokenReviewerJwtIV = tokenReviewerJwtIV;
|
||||
updateQuery.tokenReviewerJwtTag = tokenReviewerJwtTag;
|
||||
updateQuery.encryptedKubernetesTokenReviewerJwt = encryptor({
|
||||
plainText: Buffer.from(tokenReviewerJwt)
|
||||
}).cipherTextBlob;
|
||||
}
|
||||
|
||||
const updatedKubernetesAuth = await identityKubernetesAuthDAL.updateById(identityKubernetesAuth.id, updateQuery);
|
||||
|
||||
const updatedCACert =
|
||||
updatedKubernetesAuth.encryptedCaCert && updatedKubernetesAuth.caCertIV && updatedKubernetesAuth.caCertTag
|
||||
? decryptSymmetric({
|
||||
ciphertext: updatedKubernetesAuth.encryptedCaCert,
|
||||
iv: updatedKubernetesAuth.caCertIV,
|
||||
tag: updatedKubernetesAuth.caCertTag,
|
||||
key
|
||||
})
|
||||
: "";
|
||||
const updatedCACert = updatedKubernetesAuth.encryptedKubernetesCaCertificate
|
||||
? decryptor({
|
||||
cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesCaCertificate
|
||||
}).toString()
|
||||
: "";
|
||||
|
||||
const updatedTokenReviewerJwt =
|
||||
updatedKubernetesAuth.encryptedTokenReviewerJwt &&
|
||||
updatedKubernetesAuth.tokenReviewerJwtIV &&
|
||||
updatedKubernetesAuth.tokenReviewerJwtTag
|
||||
? decryptSymmetric({
|
||||
ciphertext: updatedKubernetesAuth.encryptedTokenReviewerJwt,
|
||||
iv: updatedKubernetesAuth.tokenReviewerJwtIV,
|
||||
tag: updatedKubernetesAuth.tokenReviewerJwtTag,
|
||||
key
|
||||
})
|
||||
: "";
|
||||
const updatedTokenReviewerJwt = updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
? decryptor({
|
||||
cipherTextBlob: updatedKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
}).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
...updatedKubernetesAuth,
|
||||
@@ -545,41 +436,21 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot)
|
||||
throw new NotFoundError({
|
||||
message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`,
|
||||
name: "OrgBotNotFound"
|
||||
});
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const { encryptedCaCert, caCertIV, caCertTag, encryptedTokenReviewerJwt, tokenReviewerJwtIV, tokenReviewerJwtTag } =
|
||||
identityKubernetesAuth;
|
||||
|
||||
let caCert = "";
|
||||
if (encryptedCaCert && caCertIV && caCertTag) {
|
||||
caCert = decryptSymmetric({
|
||||
ciphertext: encryptedCaCert,
|
||||
iv: caCertIV,
|
||||
tag: caCertTag,
|
||||
key
|
||||
});
|
||||
if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
|
||||
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
||||
}
|
||||
|
||||
let tokenReviewerJwt = "";
|
||||
if (encryptedTokenReviewerJwt && tokenReviewerJwtIV && tokenReviewerJwtTag) {
|
||||
tokenReviewerJwt = decryptSymmetric({
|
||||
ciphertext: encryptedTokenReviewerJwt,
|
||||
iv: tokenReviewerJwtIV,
|
||||
tag: tokenReviewerJwtTag,
|
||||
key
|
||||
});
|
||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||
tokenReviewerJwt = decryptor({
|
||||
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
||||
}).toString();
|
||||
}
|
||||
|
||||
return { ...identityKubernetesAuth, caCert, tokenReviewerJwt, orgId: identityMembershipOrg.orgId };
|
||||
|
||||
@@ -4,20 +4,12 @@ import https from "https";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { JwksClient } from "jwks-rsa";
|
||||
|
||||
import { IdentityAuthMethod, SecretKeyEncoding, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
|
||||
import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import {
|
||||
decryptSymmetric,
|
||||
encryptSymmetric,
|
||||
generateSymmetricKey,
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
@@ -25,7 +17,8 @@ import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TOrgBotDALFactory } from "../org/org-bot-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TIdentityOidcAuthDALFactory } from "./identity-oidc-auth-dal";
|
||||
import { doesAudValueMatchOidcPolicy, doesFieldValueMatchOidcPolicy } from "./identity-oidc-auth-fns";
|
||||
import {
|
||||
@@ -42,7 +35,7 @@ type TIdentityOidcAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "transaction" | "create">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TIdentityOidcAuthServiceFactory = ReturnType<typeof identityOidcAuthServiceFactory>;
|
||||
@@ -53,7 +46,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
permissionService,
|
||||
licenseService,
|
||||
identityAccessTokenDAL,
|
||||
orgBotDAL
|
||||
kmsService
|
||||
}: TIdentityOidcAuthServiceFactoryDep) => {
|
||||
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
|
||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||
@@ -70,31 +63,14 @@ export const identityOidcAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`,
|
||||
name: "OrgBotNotFound"
|
||||
});
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const { encryptedCaCert, caCertIV, caCertTag } = identityOidcAuth;
|
||||
|
||||
let caCert = "";
|
||||
if (encryptedCaCert && caCertIV && caCertTag) {
|
||||
caCert = decryptSymmetric({
|
||||
ciphertext: encryptedCaCert,
|
||||
iv: caCertIV,
|
||||
tag: caCertTag,
|
||||
key
|
||||
});
|
||||
if (identityOidcAuth.encryptedCaCertificate) {
|
||||
caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString();
|
||||
}
|
||||
|
||||
const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
|
||||
@@ -264,64 +240,17 @@ export const identityOidcAuthServiceFactory = ({
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const orgBot = await orgBotDAL.transaction(async (tx) => {
|
||||
const doc = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId }, tx);
|
||||
if (doc) return doc;
|
||||
|
||||
const { privateKey, publicKey } = generateAsymmetricKeyPair();
|
||||
const key = generateSymmetricKey();
|
||||
const {
|
||||
ciphertext: encryptedPrivateKey,
|
||||
iv: privateKeyIV,
|
||||
tag: privateKeyTag,
|
||||
encoding: privateKeyKeyEncoding,
|
||||
algorithm: privateKeyAlgorithm
|
||||
} = infisicalSymmetricEncypt(privateKey);
|
||||
const {
|
||||
ciphertext: encryptedSymmetricKey,
|
||||
iv: symmetricKeyIV,
|
||||
tag: symmetricKeyTag,
|
||||
encoding: symmetricKeyKeyEncoding,
|
||||
algorithm: symmetricKeyAlgorithm
|
||||
} = infisicalSymmetricEncypt(key);
|
||||
|
||||
return orgBotDAL.create(
|
||||
{
|
||||
name: "Infisical org bot",
|
||||
publicKey,
|
||||
privateKeyIV,
|
||||
encryptedPrivateKey,
|
||||
symmetricKeyIV,
|
||||
symmetricKeyTag,
|
||||
encryptedSymmetricKey,
|
||||
symmetricKeyAlgorithm,
|
||||
orgId: identityMembershipOrg.orgId,
|
||||
privateKeyTag,
|
||||
privateKeyAlgorithm,
|
||||
privateKeyKeyEncoding,
|
||||
symmetricKeyKeyEncoding
|
||||
},
|
||||
tx
|
||||
);
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
});
|
||||
|
||||
const { ciphertext: encryptedCaCert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||
|
||||
const identityOidcAuth = await identityOidcAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityOidcAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
oidcDiscoveryUrl,
|
||||
encryptedCaCert,
|
||||
caCertIV,
|
||||
caCertTag,
|
||||
encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob,
|
||||
boundIssuer,
|
||||
boundAudiences,
|
||||
boundClaims,
|
||||
@@ -415,38 +344,19 @@ export const identityOidcAuthServiceFactory = ({
|
||||
: undefined
|
||||
};
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization bot not found for organization with ID '${identityMembershipOrg.orgId}'`,
|
||||
name: "OrgBotNotFound"
|
||||
});
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
if (caCert !== undefined) {
|
||||
const { ciphertext: encryptedCACert, iv: caCertIV, tag: caCertTag } = encryptSymmetric(caCert, key);
|
||||
updateQuery.encryptedCaCert = encryptedCACert;
|
||||
updateQuery.caCertIV = caCertIV;
|
||||
updateQuery.caCertTag = caCertTag;
|
||||
updateQuery.encryptedCaCertificate = encryptor({ plainText: Buffer.from(caCert) }).cipherTextBlob;
|
||||
}
|
||||
|
||||
const updatedOidcAuth = await identityOidcAuthDAL.updateById(identityOidcAuth.id, updateQuery);
|
||||
const updatedCACert =
|
||||
updatedOidcAuth.encryptedCaCert && updatedOidcAuth.caCertIV && updatedOidcAuth.caCertTag
|
||||
? decryptSymmetric({
|
||||
ciphertext: updatedOidcAuth.encryptedCaCert,
|
||||
iv: updatedOidcAuth.caCertIV,
|
||||
tag: updatedOidcAuth.caCertTag,
|
||||
key
|
||||
})
|
||||
: "";
|
||||
const updatedCACert = updatedOidcAuth.encryptedCaCertificate
|
||||
? decryptor({ cipherTextBlob: updatedOidcAuth.encryptedCaCertificate }).toString()
|
||||
: "";
|
||||
|
||||
return {
|
||||
...updatedOidcAuth,
|
||||
@@ -476,27 +386,14 @@ export const identityOidcAuthServiceFactory = ({
|
||||
|
||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||
|
||||
const orgBot = await orgBotDAL.findOne({ orgId: identityMembershipOrg.orgId });
|
||||
if (!orgBot) {
|
||||
throw new NotFoundError({
|
||||
message: `Organization bot not found for organization with ID ${identityMembershipOrg.orgId}`,
|
||||
name: "OrgBotNotFound"
|
||||
});
|
||||
}
|
||||
|
||||
const key = infisicalSymmetricDecrypt({
|
||||
ciphertext: orgBot.encryptedSymmetricKey,
|
||||
iv: orgBot.symmetricKeyIV,
|
||||
tag: orgBot.symmetricKeyTag,
|
||||
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const caCert = decryptSymmetric({
|
||||
ciphertext: identityOidcAuth.encryptedCaCert,
|
||||
iv: identityOidcAuth.caCertIV,
|
||||
tag: identityOidcAuth.caCertTag,
|
||||
key
|
||||
});
|
||||
const caCert = identityOidcAuth.encryptedCaCertificate
|
||||
? decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString()
|
||||
: "";
|
||||
|
||||
return { ...identityOidcAuth, orgId: identityMembershipOrg.orgId, caCert };
|
||||
};
|
||||
|
||||
@@ -3,12 +3,32 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import { KmsKeysSchema, TableName, TInternalKms, TKmsKeys } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { CmekOrderBy, TListCmeksByProjectIdDTO } from "@app/services/cmek/cmek-types";
|
||||
|
||||
export type TKmsKeyDALFactory = ReturnType<typeof kmskeyDALFactory>;
|
||||
|
||||
type TCmekFindFilter = Parameters<typeof buildFindFilter<TKmsKeys>>[0];
|
||||
|
||||
const baseCmekQuery = ({ filter, db, tx }: { db: TDbClient; filter?: TCmekFindFilter; tx?: Knex }) => {
|
||||
const query = (tx || db.replicaNode())(TableName.KmsKey)
|
||||
.where(`${TableName.KmsKey}.isReserved`, false)
|
||||
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.KmsKey),
|
||||
db.ref("encryptionAlgorithm").withSchema(TableName.InternalKms),
|
||||
db.ref("version").withSchema(TableName.InternalKms)
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.KmsKey, filter)));
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsKey);
|
||||
|
||||
@@ -101,7 +121,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findKmsKeysByProjectId = async (
|
||||
const listCmeksByProjectId = async (
|
||||
{
|
||||
projectId,
|
||||
offset = 0,
|
||||
@@ -120,6 +140,7 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
void qb.whereILike("name", `%${search}%`);
|
||||
}
|
||||
})
|
||||
.where(`${TableName.KmsKey}.isReserved`, false)
|
||||
.join(TableName.InternalKms, `${TableName.InternalKms}.kmsKeyId`, `${TableName.KmsKey}.id`)
|
||||
.select<
|
||||
(TKmsKeys &
|
||||
@@ -146,5 +167,33 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...kmsOrm, findByIdWithAssociatedKms, findKmsKeysByProjectId, findProjectCmeks };
|
||||
const findCmekById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const key = await baseCmekQuery({
|
||||
filter: { id },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
|
||||
return key;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by ID - KMS Key" });
|
||||
}
|
||||
};
|
||||
|
||||
const findCmekByName = async (keyName: string, projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const key = await baseCmekQuery({
|
||||
filter: { name: keyName, projectId },
|
||||
db,
|
||||
tx
|
||||
}).first();
|
||||
|
||||
return key;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by Name - KMS Key" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...kmsOrm, findByIdWithAssociatedKms, listCmeksByProjectId, findCmekById, findCmekByName, findProjectCmeks };
|
||||
};
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
import { Knex } from "knex";
|
||||
|
||||
export type TKmsRootConfigDALFactory = ReturnType<typeof kmsRootConfigDALFactory>;
|
||||
|
||||
export const kmsRootConfigDALFactory = (db: TDbClient) => {
|
||||
const kmsOrm = ormify(db, TableName.KmsServerRootConfig);
|
||||
return kmsOrm;
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db)(TableName.KmsServerRootConfig)
|
||||
.where({ id } as never)
|
||||
.first("*");
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by id" });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...kmsOrm, findById };
|
||||
};
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
TExternalKmsProviderFns
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { THsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { TEnvConfig } from "@app/lib/config/env";
|
||||
import { randomSecureBytes } from "@app/lib/crypto";
|
||||
import { symmetricCipherService, SymmetricEncryption } from "@app/lib/crypto/cipher";
|
||||
import { generateHash } from "@app/lib/crypto/encryption";
|
||||
@@ -46,23 +46,22 @@ type TKmsServiceFactoryDep = {
|
||||
kmsDAL: TKmsKeyDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "updateById" | "transaction">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findById" | "updateById" | "transaction">;
|
||||
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create" | "updateById">;
|
||||
kmsRootConfigDAL: Pick<TKmsRootConfigDALFactory, "findById" | "create" | "updateById" | "transaction">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "waitTillReady" | "setItemWithExpiry">;
|
||||
internalKmsDAL: Pick<TInternalKmsDALFactory, "create">;
|
||||
hsmService: THsmServiceFactory;
|
||||
envConfig: Pick<TEnvConfig, "ENCRYPTION_KEY" | "ROOT_ENCRYPTION_KEY">;
|
||||
};
|
||||
|
||||
export type TKmsServiceFactory = ReturnType<typeof kmsServiceFactory>;
|
||||
|
||||
const KMS_ROOT_CREATION_WAIT_KEY = "wait_till_ready_kms_root_key";
|
||||
const KMS_ROOT_CREATION_WAIT_TIME = 10;
|
||||
|
||||
// akhilmhdh: Don't edit this value. This is measured for blob concatination in kms
|
||||
const KMS_VERSION = "v01";
|
||||
const KMS_VERSION_BLOB_LENGTH = 3;
|
||||
const KmsSanitizedSchema = KmsKeysSchema.extend({ isExternal: z.boolean() });
|
||||
|
||||
export const kmsServiceFactory = ({
|
||||
envConfig,
|
||||
kmsDAL,
|
||||
kmsRootConfigDAL,
|
||||
keyStore,
|
||||
@@ -541,7 +540,8 @@ export const kmsServiceFactory = ({
|
||||
}
|
||||
|
||||
const kmsDecryptor = await decryptWithKmsKey({
|
||||
kmsId: kmsKeyId
|
||||
kmsId: kmsKeyId,
|
||||
tx: trx
|
||||
});
|
||||
|
||||
return kmsDecryptor({
|
||||
@@ -703,10 +703,8 @@ export const kmsServiceFactory = ({
|
||||
};
|
||||
|
||||
const $getBasicEncryptionKey = () => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const encryptionKey = appCfg.ENCRYPTION_KEY || appCfg.ROOT_ENCRYPTION_KEY;
|
||||
const isBase64 = !appCfg.ENCRYPTION_KEY;
|
||||
const encryptionKey = envConfig.ENCRYPTION_KEY || envConfig.ROOT_ENCRYPTION_KEY;
|
||||
const isBase64 = !envConfig.ENCRYPTION_KEY;
|
||||
if (!encryptionKey)
|
||||
throw new Error(
|
||||
"Root encryption key not found for KMS service. Did you set the ENCRYPTION_KEY or ROOT_ENCRYPTION_KEY environment variables?"
|
||||
@@ -942,54 +940,33 @@ export const kmsServiceFactory = ({
|
||||
return { id, name, orgId, isExternal };
|
||||
};
|
||||
|
||||
// akhilmhdh: a copy of this is made in migrations/utils/kms
|
||||
const startService = async () => {
|
||||
const lock = await keyStore.acquireLock([`KMS_ROOT_CFG_LOCK`], 3000, { retryCount: 3 }).catch(() => null);
|
||||
if (!lock) {
|
||||
await keyStore.waitTillReady({
|
||||
key: KMS_ROOT_CREATION_WAIT_KEY,
|
||||
keyCheckCb: (val) => val === "true",
|
||||
waitingCb: () => logger.info("KMS. Waiting for leader to finish creation of KMS Root Key")
|
||||
const kmsRootConfig = await kmsRootConfigDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.KmsRootKeyInit]);
|
||||
// check if KMS root key was already generated and saved in DB
|
||||
const existingRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||
if (existingRootConfig) return existingRootConfig;
|
||||
|
||||
logger.info("KMS: Generating new ROOT Key");
|
||||
const newRootKey = randomSecureBytes(32);
|
||||
const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => {
|
||||
logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key");
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// check if KMS root key was already generated and saved in DB
|
||||
const kmsRootConfig = await kmsRootConfigDAL.findById(KMS_ROOT_CONFIG_UUID);
|
||||
|
||||
// case 1: a root key already exists in the DB
|
||||
if (kmsRootConfig) {
|
||||
if (lock) await lock.release();
|
||||
logger.info(`KMS: Encrypted ROOT Key found from DB. Decrypting. [strategy=${kmsRootConfig.encryptionStrategy}]`);
|
||||
|
||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||
|
||||
// set the flag so that other instance nodes can start
|
||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||
return;
|
||||
}
|
||||
|
||||
// case 2: no config is found, so we create a new root key with basic encryption
|
||||
logger.info("KMS: Generating new ROOT Key");
|
||||
const newRootKey = randomSecureBytes(32);
|
||||
const encryptedRootKey = await $encryptRootKey(newRootKey, RootKeyEncryptionStrategy.Software).catch((err) => {
|
||||
logger.error({ hsmEnabled: hsmService.isActive() }, "KMS: Failed to encrypt ROOT Key");
|
||||
throw err;
|
||||
const newRootConfig = await kmsRootConfigDAL.create({
|
||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||
id: KMS_ROOT_CONFIG_UUID,
|
||||
encryptedRootKey,
|
||||
encryptionStrategy: RootKeyEncryptionStrategy.Software
|
||||
});
|
||||
return newRootConfig;
|
||||
});
|
||||
|
||||
await kmsRootConfigDAL.create({
|
||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||
id: KMS_ROOT_CONFIG_UUID,
|
||||
encryptedRootKey,
|
||||
encryptionStrategy: RootKeyEncryptionStrategy.Software
|
||||
});
|
||||
const decryptedRootKey = await $decryptRootKey(kmsRootConfig);
|
||||
|
||||
// set the flag so that other instance nodes can start
|
||||
await keyStore.setItemWithExpiry(KMS_ROOT_CREATION_WAIT_KEY, KMS_ROOT_CREATION_WAIT_TIME, "true");
|
||||
logger.info("KMS: Saved and loaded ROOT Key into memory");
|
||||
if (lock) await lock.release();
|
||||
ROOT_ENCRYPTION_KEY = newRootKey;
|
||||
logger.info("KMS: Loading ROOT Key into Memory.");
|
||||
ROOT_ENCRYPTION_KEY = decryptedRootKey;
|
||||
};
|
||||
|
||||
const updateEncryptionStrategy = async (strategy: RootKeyEncryptionStrategy) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
|
||||
import { ActorAuthMethod } from "../auth/auth-type";
|
||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||
|
||||
@@ -4,8 +4,17 @@ import { ProjectType, TProjectKeys } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { CaStatus } from "../certificate-authority/certificate-authority-types";
|
||||
import { KmsType } from "../kms/kms-types";
|
||||
|
||||
enum KmsType {
|
||||
External = "external",
|
||||
Internal = "internal"
|
||||
}
|
||||
|
||||
enum CaStatus {
|
||||
ACTIVE = "active",
|
||||
DISABLED = "disabled",
|
||||
PENDING_CERTIFICATE = "pending-certificate"
|
||||
}
|
||||
|
||||
export enum ProjectFilterType {
|
||||
ID = "id",
|
||||
|
||||
@@ -69,6 +69,8 @@ const getParametersByPath = async (ssm: AWS.SSM, path: string): Promise<TAWSPara
|
||||
attempt += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleep();
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
||||
+2
-2
@@ -10,14 +10,14 @@ import {
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
|
||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.REGION),
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||
path: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Parameter Store Path required")
|
||||
.max(2048, "Cannot exceed 2048 characters")
|
||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format')
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.PATH)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||
});
|
||||
|
||||
export const AwsParameterStoreSyncSchema = BaseSecretSyncSchema(SecretSync.AWSParameterStore).extend({
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const AWS_SECRETS_MANAGER_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "AWS Secrets Manager",
|
||||
destination: SecretSync.AWSSecretsManager,
|
||||
connection: AppConnection.AWS,
|
||||
canImportSecrets: true
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum AwsSecretsManagerSyncMappingBehavior {
|
||||
OneToOne = "one-to-one",
|
||||
ManyToOne = "many-to-one"
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import {
|
||||
BatchGetSecretValueCommand,
|
||||
CreateSecretCommand,
|
||||
CreateSecretCommandInput,
|
||||
DeleteSecretCommand,
|
||||
DeleteSecretResponse,
|
||||
ListSecretsCommand,
|
||||
SecretsManagerClient,
|
||||
UpdateSecretCommand,
|
||||
UpdateSecretCommandInput
|
||||
} from "@aws-sdk/client-secrets-manager";
|
||||
import { AWSError } from "aws-sdk";
|
||||
import { CreateSecretResponse, SecretListEntry, SecretValueEntry } from "aws-sdk/clients/secretsmanager";
|
||||
|
||||
import { getAwsConnectionConfig } from "@app/services/app-connection/aws/aws-connection-fns";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { TAwsSecretsManagerSyncWithCredentials } from "./aws-secrets-manager-sync-types";
|
||||
|
||||
type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const BATCH_SIZE = 20;
|
||||
|
||||
const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => {
|
||||
const { destinationConfig, connection } = secretSync;
|
||||
|
||||
const config = await getAwsConnectionConfig(connection, destinationConfig.region);
|
||||
|
||||
const secretsManagerClient = new SecretsManagerClient({
|
||||
region: config.region,
|
||||
credentials: config.credentials!
|
||||
});
|
||||
|
||||
return secretsManagerClient;
|
||||
};
|
||||
|
||||
const sleep = async () =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 1000);
|
||||
});
|
||||
|
||||
const getSecretsRecord = async (client: SecretsManagerClient): Promise<TAwsSecretsRecord> => {
|
||||
const awsSecretsRecord: TAwsSecretsRecord = {};
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
let attempt = 0;
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const output = await client.send(new ListSecretsCommand({ NextToken: nextToken }));
|
||||
|
||||
attempt = 0;
|
||||
|
||||
if (output.SecretList) {
|
||||
output.SecretList.forEach((secretEntry) => {
|
||||
if (secretEntry.Name) {
|
||||
awsSecretsRecord[secretEntry.Name] = secretEntry;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasNext = Boolean(output.NextToken);
|
||||
nextToken = output.NextToken;
|
||||
} catch (e) {
|
||||
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
attempt += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleep();
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return awsSecretsRecord;
|
||||
};
|
||||
|
||||
const getSecretValuesRecord = async (
|
||||
client: SecretsManagerClient,
|
||||
awsSecretsRecord: TAwsSecretsRecord
|
||||
): Promise<TAwsSecretValuesRecord> => {
|
||||
const awsSecretValuesRecord: TAwsSecretValuesRecord = {};
|
||||
let attempt = 0;
|
||||
|
||||
const secretIdList = Object.keys(awsSecretsRecord);
|
||||
|
||||
for (let i = 0; i < secretIdList.length; i += BATCH_SIZE) {
|
||||
const batchSecretIds = secretIdList.slice(i, i + BATCH_SIZE);
|
||||
let hasNext = true;
|
||||
let nextToken: string | undefined;
|
||||
|
||||
while (hasNext) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const output = await client.send(
|
||||
new BatchGetSecretValueCommand({
|
||||
SecretIdList: batchSecretIds,
|
||||
NextToken: nextToken
|
||||
})
|
||||
);
|
||||
|
||||
attempt = 0;
|
||||
|
||||
if (output.SecretValues) {
|
||||
output.SecretValues.forEach((secretValueEntry) => {
|
||||
if (secretValueEntry.Name) {
|
||||
awsSecretValuesRecord[secretValueEntry.Name] = secretValueEntry;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasNext = Boolean(output.NextToken);
|
||||
nextToken = output.NextToken;
|
||||
} catch (e) {
|
||||
if ((e as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
attempt += 1;
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await sleep();
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return awsSecretValuesRecord;
|
||||
};
|
||||
|
||||
const createSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: CreateSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<CreateSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new CreateSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return createSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const updateSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
input: UpdateSecretCommandInput,
|
||||
attempt = 0
|
||||
): Promise<CreateSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new UpdateSecretCommand(input));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return updateSecret(client, input, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSecret = async (
|
||||
client: SecretsManagerClient,
|
||||
secretKey: string,
|
||||
attempt = 0
|
||||
): Promise<DeleteSecretResponse> => {
|
||||
try {
|
||||
return await client.send(new DeleteSecretCommand({ SecretId: secretKey, ForceDeleteWithoutRecovery: true }));
|
||||
} catch (error) {
|
||||
if ((error as AWSError).code === "ThrottlingException" && attempt < MAX_RETRIES) {
|
||||
await sleep();
|
||||
|
||||
// retry
|
||||
return deleteSecret(client, secretKey, attempt + 1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const AwsSecretsManagerSyncFns = {
|
||||
syncSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client);
|
||||
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
|
||||
// skip secrets that don't have a value set
|
||||
if (!value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (awsSecretsRecord[key]) {
|
||||
// skip secrets that haven't changed
|
||||
if (awsValuesRecord[key]?.SecretString === value) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateSecret(client, {
|
||||
SecretId: key,
|
||||
SecretString: value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await createSecret(client, {
|
||||
Name: key,
|
||||
SecretString: value
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (!(secretKey in secretMap) || !secretMap[secretKey].value) {
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Many-To-One Mapping
|
||||
|
||||
const secretValue = JSON.stringify(
|
||||
Object.fromEntries(Object.entries(secretMap).map(([key, secretData]) => [key, secretData.value]))
|
||||
);
|
||||
|
||||
if (awsValuesRecord[destinationConfig.secretName]) {
|
||||
await updateSecret(client, {
|
||||
SecretId: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
});
|
||||
} else {
|
||||
await createSecret(client, {
|
||||
Name: destinationConfig.secretName,
|
||||
SecretString: secretValue
|
||||
});
|
||||
}
|
||||
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (secretKey === destinationConfig.secretName) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials): Promise<TSecretMap> => {
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client);
|
||||
const awsValuesRecord = await getSecretValuesRecord(client, awsSecretsRecord);
|
||||
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
return Object.fromEntries(
|
||||
Object.keys(awsSecretsRecord).map((key) => [key, { value: awsValuesRecord[key].SecretString ?? "" }])
|
||||
);
|
||||
}
|
||||
|
||||
// Many-To-One Mapping
|
||||
|
||||
const secretValueEntry = awsValuesRecord[destinationConfig.secretName];
|
||||
|
||||
if (!secretValueEntry) return {};
|
||||
|
||||
try {
|
||||
const parsedValue = (secretValueEntry.SecretString ? JSON.parse(secretValueEntry.SecretString) : {}) as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
|
||||
return Object.fromEntries(Object.entries(parsedValue).map(([key, value]) => [key, { value }]));
|
||||
} catch {
|
||||
throw new SecretSyncError({
|
||||
message:
|
||||
"Failed to import secrets. Invalid format for Many-To-One mapping behavior: requires key/value configuration.",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
},
|
||||
removeSecrets: async (secretSync: TAwsSecretsManagerSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { destinationConfig } = secretSync;
|
||||
|
||||
const client = await getSecretsManagerClient(secretSync);
|
||||
|
||||
const awsSecretsRecord = await getSecretsRecord(client);
|
||||
|
||||
if (destinationConfig.mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne) {
|
||||
for await (const secretKey of Object.keys(awsSecretsRecord)) {
|
||||
if (secretKey in secretMap) {
|
||||
try {
|
||||
await deleteSecret(client, secretKey);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await deleteSecret(client, destinationConfig.secretName);
|
||||
}
|
||||
}
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
|
||||
const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.discriminatedUnion("mappingBehavior", [
|
||||
z.object({
|
||||
mappingBehavior: z
|
||||
.literal(AwsSecretsManagerSyncMappingBehavior.OneToOne)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior)
|
||||
}),
|
||||
z.object({
|
||||
mappingBehavior: z
|
||||
.literal(AwsSecretsManagerSyncMappingBehavior.ManyToOne)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName)
|
||||
})
|
||||
])
|
||||
.and(
|
||||
z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.region)
|
||||
})
|
||||
);
|
||||
|
||||
export const AwsSecretsManagerSyncSchema = BaseSecretSyncSchema(SecretSync.AWSSecretsManager).extend({
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAwsSecretsManagerSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAwsSecretsManagerSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AWSSecretsManager
|
||||
).extend({
|
||||
destinationConfig: AwsSecretsManagerSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const AwsSecretsManagerSyncListItemSchema = z.object({
|
||||
name: z.literal("AWS Secrets Manager"),
|
||||
connection: z.literal(AppConnection.AWS),
|
||||
destination: z.literal(SecretSync.AWSSecretsManager),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAwsConnection } from "@app/services/app-connection/aws";
|
||||
|
||||
import {
|
||||
AwsSecretsManagerSyncListItemSchema,
|
||||
AwsSecretsManagerSyncSchema,
|
||||
CreateAwsSecretsManagerSyncSchema
|
||||
} from "./aws-secrets-manager-sync-schemas";
|
||||
|
||||
export type TAwsSecretsManagerSync = z.infer<typeof AwsSecretsManagerSyncSchema>;
|
||||
|
||||
export type TAwsSecretsManagerSyncInput = z.infer<typeof CreateAwsSecretsManagerSyncSchema>;
|
||||
|
||||
export type TAwsSecretsManagerSyncListItem = z.infer<typeof AwsSecretsManagerSyncListItemSchema>;
|
||||
|
||||
export type TAwsSecretsManagerSyncWithCredentials = TAwsSecretsManagerSync & {
|
||||
connection: TAwsConnection;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./aws-secrets-manager-sync-constants";
|
||||
export * from "./aws-secrets-manager-sync-fns";
|
||||
export * from "./aws-secrets-manager-sync-schemas";
|
||||
export * from "./aws-secrets-manager-sync-types";
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Azure App Configuration",
|
||||
destination: SecretSync.AzureAppConfiguration,
|
||||
connection: AppConnection.AzureAppConfiguration,
|
||||
canImportSecrets: true
|
||||
};
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import https from "https";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
||||
import { isAzureKeyVaultReference } from "@app/services/integration-auth/integration-sync-secret-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { TAzureAppConfigurationSyncWithCredentials } from "./azure-app-configuration-sync-types";
|
||||
|
||||
type TAzureAppConfigurationSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
interface AzureAppConfigKeyValue {
|
||||
key: string;
|
||||
value: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const azureAppConfigurationSyncFactory = ({
|
||||
kmsService,
|
||||
appConnectionDAL
|
||||
}: TAzureAppConfigurationSyncFactoryDeps) => {
|
||||
const $getCompleteAzureAppConfigValues = async (accessToken: string, baseURL: string, url: string) => {
|
||||
let result: AzureAppConfigKeyValue[] = [];
|
||||
let currentUrl = url;
|
||||
|
||||
while (currentUrl) {
|
||||
const res = await request.get<{ items: AzureAppConfigKeyValue[]; ["@nextLink"]: string }>(currentUrl, {
|
||||
baseURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
// we force IPV4 because docker setup fails with ipv6
|
||||
httpsAgent: new https.Agent({
|
||||
family: 4
|
||||
})
|
||||
});
|
||||
|
||||
result = result.concat(res.data.items);
|
||||
currentUrl = res.data?.["@nextLink"];
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const $deleteAzureSecret = async (accessToken: string, configurationUrl: string, key: string, label?: string) => {
|
||||
await request.delete(`${configurationUrl}/kv/${key}?api-version=2023-11-01`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
...(label &&
|
||||
label.length > 0 && {
|
||||
params: {
|
||||
label
|
||||
}
|
||||
}),
|
||||
httpsAgent: new https.Agent({
|
||||
family: 4
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
const syncSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
if (!secretSync.destinationConfig.configurationUrl.endsWith(".azconfig.io")) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid Azure App Configuration URL provided."
|
||||
});
|
||||
}
|
||||
|
||||
const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService);
|
||||
|
||||
const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${
|
||||
secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00"
|
||||
}`;
|
||||
|
||||
const azureAppConfigValuesUrlAllSecrets = `/kv?api-version=2023-11-01`;
|
||||
|
||||
const azureAppConfigSecretsLabeled = Object.fromEntries(
|
||||
(
|
||||
await $getCompleteAzureAppConfigValues(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.configurationUrl,
|
||||
azureAppConfigValuesUrl
|
||||
)
|
||||
).map((entry) => [entry.key, entry.value])
|
||||
);
|
||||
|
||||
const azureAppConfigSecrets = Object.fromEntries(
|
||||
(
|
||||
await $getCompleteAzureAppConfigValues(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.configurationUrl,
|
||||
azureAppConfigValuesUrlAllSecrets
|
||||
)
|
||||
).map((entry) => [
|
||||
entry.key,
|
||||
{
|
||||
value: entry.value,
|
||||
label: entry.label
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
// add the secrets to azure app config, that are in infisical
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
if (!(key in azureAppConfigSecretsLabeled) || secretMap[key]?.value !== azureAppConfigSecretsLabeled[key]) {
|
||||
await request.put(
|
||||
`${secretSync.destinationConfig.configurationUrl}/kv/${key}?api-version=2023-11-01`,
|
||||
{
|
||||
value: secretMap[key]?.value,
|
||||
...(isAzureKeyVaultReference(secretMap[key]?.value || "") && {
|
||||
content_type: "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
|
||||
})
|
||||
},
|
||||
{
|
||||
...(secretSync.destinationConfig.label && {
|
||||
params: {
|
||||
label: secretSync.destinationConfig.label
|
||||
}
|
||||
}),
|
||||
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
family: 4
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for await (const key of Object.keys(azureAppConfigSecrets)) {
|
||||
const azureSecret = azureAppConfigSecrets[key];
|
||||
if (
|
||||
!(key in secretMap) ||
|
||||
secretMap[key] === null ||
|
||||
(azureSecret.label && azureSecret.label !== secretSync.destinationConfig.label) ||
|
||||
(!azureSecret.label && secretSync.destinationConfig.label)
|
||||
) {
|
||||
await $deleteAzureSecret(accessToken, secretSync.destinationConfig.configurationUrl, key, azureSecret.label);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService);
|
||||
|
||||
const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${
|
||||
secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00"
|
||||
}`;
|
||||
|
||||
const azureAppConfigSecrets = Object.fromEntries(
|
||||
(
|
||||
await $getCompleteAzureAppConfigValues(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.configurationUrl,
|
||||
azureAppConfigValuesUrl
|
||||
)
|
||||
).map((entry) => [entry.key, entry.value])
|
||||
);
|
||||
|
||||
for await (const infisicalKey of Object.keys(secretMap)) {
|
||||
if (infisicalKey in azureAppConfigSecrets) {
|
||||
await $deleteAzureSecret(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.configurationUrl,
|
||||
infisicalKey,
|
||||
secretSync.destinationConfig.label
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSecrets = async (secretSync: TAzureAppConfigurationSyncWithCredentials) => {
|
||||
const { accessToken } = await getAzureConnectionAccessToken(secretSync.connectionId, appConnectionDAL, kmsService);
|
||||
|
||||
const secretMap: TSecretMap = {};
|
||||
|
||||
const azureAppConfigValuesUrl = `/kv?api-version=2023-11-01${
|
||||
secretSync.destinationConfig.label ? `&label=${secretSync.destinationConfig.label}` : "&label=%00"
|
||||
}`;
|
||||
|
||||
const azureAppConfigSecrets = Object.fromEntries(
|
||||
(
|
||||
await $getCompleteAzureAppConfigValues(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.configurationUrl,
|
||||
azureAppConfigValuesUrl
|
||||
)
|
||||
).map((entry) => [entry.key, entry.value])
|
||||
);
|
||||
|
||||
Object.keys(azureAppConfigSecrets).forEach((key) => {
|
||||
secretMap[key] = {
|
||||
value: azureAppConfigSecrets[key]
|
||||
};
|
||||
});
|
||||
|
||||
return secretMap;
|
||||
};
|
||||
|
||||
return {
|
||||
syncSecrets,
|
||||
removeSecrets,
|
||||
getSecrets
|
||||
};
|
||||
};
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AzureAppConfigurationSyncDestinationConfigSchema = z.object({
|
||||
configurationUrl: z
|
||||
.string()
|
||||
.min(1, "App Configuration URL required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.configurationUrl),
|
||||
label: z.string().optional().describe(SecretSyncs.DESTINATION_CONFIG.AZURE_APP_CONFIGURATION.label)
|
||||
});
|
||||
|
||||
const AzureAppConfigurationSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AzureAppConfigurationSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AzureAppConfiguration,
|
||||
AzureAppConfigurationSyncOptionsConfig
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||
destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAzureAppConfigurationSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AzureAppConfiguration,
|
||||
AzureAppConfigurationSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAzureAppConfigurationSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AzureAppConfiguration,
|
||||
AzureAppConfigurationSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AzureAppConfigurationSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const AzureAppConfigurationSyncListItemSchema = z.object({
|
||||
name: z.literal("Azure App Configuration"),
|
||||
connection: z.literal(AppConnection.AzureAppConfiguration),
|
||||
destination: z.literal(SecretSync.AzureAppConfiguration),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAzureAppConfigurationConnection } from "@app/services/app-connection/azure-app-configuration";
|
||||
|
||||
import {
|
||||
AzureAppConfigurationSyncListItemSchema,
|
||||
AzureAppConfigurationSyncSchema,
|
||||
CreateAzureAppConfigurationSyncSchema
|
||||
} from "./azure-app-configuration-sync-schemas";
|
||||
|
||||
export type TAzureAppConfigurationSync = z.infer<typeof AzureAppConfigurationSyncSchema>;
|
||||
|
||||
export type TAzureAppConfigurationSyncInput = z.infer<typeof CreateAzureAppConfigurationSyncSchema>;
|
||||
|
||||
export type TAzureAppConfigurationSyncListItem = z.infer<typeof AzureAppConfigurationSyncListItemSchema>;
|
||||
|
||||
export type TAzureAppConfigurationSyncWithCredentials = TAzureAppConfigurationSync & {
|
||||
connection: TAzureAppConfigurationConnection;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./azure-app-configuration-sync-constants";
|
||||
export * from "./azure-app-configuration-sync-fns";
|
||||
export * from "./azure-app-configuration-sync-schemas";
|
||||
export * from "./azure-app-configuration-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const AZURE_KEY_VAULT_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Azure Key Vault",
|
||||
destination: SecretSync.AzureKeyVault,
|
||||
connection: AppConnection.AzureKeyVault,
|
||||
canImportSecrets: true
|
||||
};
|
||||
@@ -0,0 +1,253 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { getAzureConnectionAccessToken } from "@app/services/app-connection/azure-key-vault";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { SecretSyncError } from "../secret-sync-errors";
|
||||
import { GetAzureKeyVaultSecret, TAzureKeyVaultSyncWithCredentials } from "./azure-key-vault-sync-types";
|
||||
|
||||
type TAzureKeyVaultSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzureKeyVaultSyncFactoryDeps) => {
|
||||
const $getAzureKeyVaultSecrets = async (accessToken: string, vaultBaseUrl: string) => {
|
||||
const paginateAzureKeyVaultSecrets = async () => {
|
||||
let result: GetAzureKeyVaultSecret[] = [];
|
||||
|
||||
let currentUrl = `${vaultBaseUrl}/secrets?api-version=7.3`;
|
||||
|
||||
while (currentUrl) {
|
||||
const res = await request.get<{ value: GetAzureKeyVaultSecret; nextLink: string }>(currentUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
result = result.concat(res.data.value);
|
||||
currentUrl = res.data.nextLink;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets();
|
||||
|
||||
const enabledAzureKeyVaultSecrets = getAzureKeyVaultSecrets.filter((secret) => secret.attributes.enabled);
|
||||
|
||||
// disabled keys to skip sending updates to
|
||||
const disabledAzureKeyVaultSecretKeys = getAzureKeyVaultSecrets
|
||||
.filter(({ attributes }) => !attributes.enabled)
|
||||
.map((getAzureKeyVaultSecret) => {
|
||||
return getAzureKeyVaultSecret.id.substring(getAzureKeyVaultSecret.id.lastIndexOf("/") + 1);
|
||||
});
|
||||
|
||||
let lastSlashIndex: number;
|
||||
const res = (
|
||||
await Promise.all(
|
||||
enabledAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
|
||||
if (!lastSlashIndex) {
|
||||
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
|
||||
}
|
||||
|
||||
const azureKeyVaultSecret = await request.get<GetAzureKeyVaultSecret>(
|
||||
`${getAzureKeyVaultSecret.id}?api-version=7.3`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
...azureKeyVaultSecret.data,
|
||||
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1)
|
||||
};
|
||||
})
|
||||
)
|
||||
).reduce(
|
||||
(obj, secret) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}),
|
||||
{} as Record<string, GetAzureKeyVaultSecret>
|
||||
);
|
||||
|
||||
return {
|
||||
vaultSecrets: res,
|
||||
disabledAzureKeyVaultSecretKeys
|
||||
};
|
||||
};
|
||||
|
||||
const syncSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService);
|
||||
|
||||
const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.vaultBaseUrl
|
||||
);
|
||||
|
||||
const setSecrets: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
|
||||
const deleteSecrets: string[] = [];
|
||||
|
||||
Object.keys(secretMap).forEach((infisicalKey) => {
|
||||
const hyphenatedKey = infisicalKey.replace(/_/g, "-");
|
||||
if (!(hyphenatedKey in vaultSecrets)) {
|
||||
// case: secret has been created
|
||||
setSecrets.push({
|
||||
key: hyphenatedKey,
|
||||
value: secretMap[infisicalKey].value
|
||||
});
|
||||
} else if (secretMap[infisicalKey].value !== vaultSecrets[hyphenatedKey].value) {
|
||||
// case: secret has been updated
|
||||
setSecrets.push({
|
||||
key: hyphenatedKey,
|
||||
value: secretMap[infisicalKey].value
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(vaultSecrets).forEach((key) => {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
if (!(underscoredKey in secretMap)) {
|
||||
deleteSecrets.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
const setSecretAzureKeyVault = async ({ key, value }: { key: string; value: string }) => {
|
||||
let isSecretSet = false;
|
||||
let syncError: Error | null = null;
|
||||
let maxTries = 6;
|
||||
if (disabledAzureKeyVaultSecretKeys.includes(key)) return;
|
||||
|
||||
while (!isSecretSet && maxTries > 0) {
|
||||
// try to set secret
|
||||
try {
|
||||
await request.put(
|
||||
`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`,
|
||||
{
|
||||
value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
isSecretSet = true;
|
||||
} catch (err) {
|
||||
syncError = err as Error;
|
||||
if (err instanceof AxiosError) {
|
||||
// eslint-disable-next-line
|
||||
if (err.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") {
|
||||
await request.post(
|
||||
`${secretSync.destinationConfig.vaultBaseUrl}/deletedsecrets/${key}/recover?api-version=7.3`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10_000);
|
||||
});
|
||||
} else {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10_000);
|
||||
});
|
||||
maxTries -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSecretSet) {
|
||||
throw new SecretSyncError({
|
||||
error: syncError,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
for await (const setSecret of setSecrets) {
|
||||
const { key, value } = setSecret;
|
||||
await setSecretAzureKeyVault({
|
||||
key,
|
||||
value
|
||||
});
|
||||
}
|
||||
|
||||
for await (const deleteSecretKey of deleteSecrets.filter(
|
||||
(secret) => !setSecrets.find((setSecret) => setSecret.key === secret)
|
||||
)) {
|
||||
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${deleteSecretKey}?api-version=7.3`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService);
|
||||
|
||||
const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.vaultBaseUrl
|
||||
);
|
||||
|
||||
for await (const [key] of Object.entries(vaultSecrets)) {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
|
||||
if (underscoredKey in secretMap) {
|
||||
if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) {
|
||||
await request.delete(`${secretSync.destinationConfig.vaultBaseUrl}/secrets/${key}?api-version=7.3`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSecrets = async (secretSync: TAzureKeyVaultSyncWithCredentials) => {
|
||||
const { accessToken } = await getAzureConnectionAccessToken(secretSync.connection.id, appConnectionDAL, kmsService);
|
||||
|
||||
const { vaultSecrets, disabledAzureKeyVaultSecretKeys } = await $getAzureKeyVaultSecrets(
|
||||
accessToken,
|
||||
secretSync.destinationConfig.vaultBaseUrl
|
||||
);
|
||||
|
||||
const secretMap: TSecretMap = {};
|
||||
|
||||
Object.keys(vaultSecrets).forEach((key) => {
|
||||
if (!disabledAzureKeyVaultSecretKeys.includes(key)) {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
secretMap[underscoredKey] = {
|
||||
value: vaultSecrets[key].value
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return secretMap;
|
||||
};
|
||||
|
||||
return {
|
||||
syncSecrets,
|
||||
removeSecrets,
|
||||
getSecrets
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const AzureKeyVaultSyncDestinationConfigSchema = z.object({
|
||||
vaultBaseUrl: z
|
||||
.string()
|
||||
.url("Invalid vault base URL format")
|
||||
.min(1, "Vault base URL required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AZURE_KEY_VAULT.vaultBaseUrl)
|
||||
});
|
||||
|
||||
const AzureKeyVaultSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
export const AzureKeyVaultSyncSchema = BaseSecretSyncSchema(
|
||||
SecretSync.AzureKeyVault,
|
||||
AzureKeyVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destination: z.literal(SecretSync.AzureKeyVault),
|
||||
destinationConfig: AzureKeyVaultSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateAzureKeyVaultSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.AzureKeyVault,
|
||||
AzureKeyVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AzureKeyVaultSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateAzureKeyVaultSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.AzureKeyVault,
|
||||
AzureKeyVaultSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: AzureKeyVaultSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const AzureKeyVaultSyncListItemSchema = z.object({
|
||||
name: z.literal("Azure Key Vault"),
|
||||
connection: z.literal(AppConnection.AzureKeyVault),
|
||||
destination: z.literal(SecretSync.AzureKeyVault),
|
||||
canImportSecrets: z.literal(true)
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TAzureKeyVaultConnection } from "@app/services/app-connection/azure-key-vault";
|
||||
|
||||
import {
|
||||
AzureKeyVaultSyncListItemSchema,
|
||||
AzureKeyVaultSyncSchema,
|
||||
CreateAzureKeyVaultSyncSchema
|
||||
} from "./azure-key-vault-sync-schemas";
|
||||
|
||||
export type TAzureKeyVaultSync = z.infer<typeof AzureKeyVaultSyncSchema>;
|
||||
|
||||
export type TAzureKeyVaultSyncInput = z.infer<typeof CreateAzureKeyVaultSyncSchema>;
|
||||
|
||||
export type TAzureKeyVaultSyncListItem = z.infer<typeof AzureKeyVaultSyncListItemSchema>;
|
||||
|
||||
export type TAzureKeyVaultSyncWithCredentials = TAzureKeyVaultSync & {
|
||||
connection: TAzureKeyVaultConnection;
|
||||
};
|
||||
|
||||
export interface GetAzureKeyVaultSecret {
|
||||
id: string; // secret URI
|
||||
value: string;
|
||||
attributes: {
|
||||
enabled: boolean;
|
||||
created: number;
|
||||
updated: number;
|
||||
recoveryLevel: string;
|
||||
recoverableDays: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
|
||||
key: string;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./azure-key-vault-sync-constants";
|
||||
export * from "./azure-key-vault-sync-fns";
|
||||
export * from "./azure-key-vault-sync-schemas";
|
||||
export * from "./azure-key-vault-sync-types";
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { TSecretSyncListItem } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
export const DATABRICKS_SYNC_LIST_OPTION: TSecretSyncListItem = {
|
||||
name: "Databricks",
|
||||
destination: SecretSync.Databricks,
|
||||
connection: AppConnection.Databricks,
|
||||
canImportSecrets: false
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { getDatabricksConnectionAccessToken } from "@app/services/app-connection/databricks";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import {
|
||||
TDatabricksDeleteSecret,
|
||||
TDatabricksListSecretKeys,
|
||||
TDatabricksListSecretKeysResponse,
|
||||
TDatabricksPutSecret,
|
||||
TDatabricksSyncWithCredentials
|
||||
} from "@app/services/secret-sync/databricks/databricks-sync-types";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
import { SECRET_SYNC_NAME_MAP } from "@app/services/secret-sync/secret-sync-maps";
|
||||
|
||||
import { TSecretMap } from "../secret-sync-types";
|
||||
|
||||
type TDatabricksSecretSyncFactoryDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const DATABRICKS_SCOPE_SECRET_LIMIT = 1000;
|
||||
|
||||
const listDatabricksSecrets = async ({ workspaceUrl, scope, accessToken }: TDatabricksListSecretKeys) => {
|
||||
const { data } = await request.get<TDatabricksListSecretKeysResponse>(
|
||||
`${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/list`,
|
||||
{
|
||||
params: {
|
||||
scope
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// not present in response if no secrets exist in scope
|
||||
return data.secrets ?? [];
|
||||
};
|
||||
const putDatabricksSecret = async ({ workspaceUrl, scope, key, value, accessToken }: TDatabricksPutSecret) =>
|
||||
request.post(
|
||||
`${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/put`,
|
||||
{
|
||||
scope,
|
||||
key,
|
||||
string_value: value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const deleteDatabricksSecrets = async ({ workspaceUrl, scope, key, accessToken }: TDatabricksDeleteSecret) =>
|
||||
request.post(
|
||||
`${removeTrailingSlash(workspaceUrl)}/api/2.0/secrets/delete`,
|
||||
{
|
||||
scope,
|
||||
key
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const databricksSyncFactory = ({ kmsService, appConnectionDAL }: TDatabricksSecretSyncFactoryDeps) => {
|
||||
const syncSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
if (Object.keys(secretSync).length > DATABRICKS_SCOPE_SECRET_LIMIT) {
|
||||
throw new Error(
|
||||
`Databricks does not support storing more than ${DATABRICKS_SCOPE_SECRET_LIMIT} secrets per scope.`
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
destinationConfig: { scope },
|
||||
connection
|
||||
} = secretSync;
|
||||
|
||||
const { workspaceUrl } = connection.credentials;
|
||||
|
||||
const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService);
|
||||
|
||||
for await (const entry of Object.entries(secretMap)) {
|
||||
const [key, { value }] = entry;
|
||||
|
||||
try {
|
||||
await putDatabricksSecret({
|
||||
key,
|
||||
value,
|
||||
workspaceUrl,
|
||||
scope,
|
||||
accessToken
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const databricksSecretKeys = await listDatabricksSecrets({
|
||||
workspaceUrl,
|
||||
scope,
|
||||
accessToken
|
||||
});
|
||||
|
||||
for await (const secret of databricksSecretKeys) {
|
||||
if (!(secret.key in secretMap)) {
|
||||
await deleteDatabricksSecrets({
|
||||
key: secret.key,
|
||||
workspaceUrl,
|
||||
scope,
|
||||
accessToken
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeSecrets = async (secretSync: TDatabricksSyncWithCredentials, secretMap: TSecretMap) => {
|
||||
const {
|
||||
destinationConfig: { scope },
|
||||
connection
|
||||
} = secretSync;
|
||||
|
||||
const { workspaceUrl } = connection.credentials;
|
||||
|
||||
const accessToken = await getDatabricksConnectionAccessToken(connection, appConnectionDAL, kmsService);
|
||||
|
||||
const databricksSecretKeys = await listDatabricksSecrets({
|
||||
workspaceUrl,
|
||||
scope,
|
||||
accessToken
|
||||
});
|
||||
|
||||
for await (const secret of databricksSecretKeys) {
|
||||
if (secret.key in secretMap) {
|
||||
await deleteDatabricksSecrets({
|
||||
key: secret.key,
|
||||
workspaceUrl,
|
||||
scope,
|
||||
accessToken
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getSecrets = async (secretSync: TDatabricksSyncWithCredentials) => {
|
||||
throw new Error(`${SECRET_SYNC_NAME_MAP[secretSync.destination]} does not support importing secrets.`);
|
||||
};
|
||||
|
||||
return {
|
||||
syncSecrets,
|
||||
removeSecrets,
|
||||
getSecrets
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
GenericCreateSecretSyncFieldsSchema,
|
||||
GenericUpdateSecretSyncFieldsSchema
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const DatabricksSyncDestinationConfigSchema = z.object({
|
||||
scope: z.string().trim().min(1, "Databricks scope required").describe(SecretSyncs.DESTINATION_CONFIG.DATABRICKS.scope)
|
||||
});
|
||||
|
||||
const DatabricksSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
||||
export const DatabricksSyncSchema = BaseSecretSyncSchema(SecretSync.Databricks, DatabricksSyncOptionsConfig).extend({
|
||||
destination: z.literal(SecretSync.Databricks),
|
||||
destinationConfig: DatabricksSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const CreateDatabricksSyncSchema = GenericCreateSecretSyncFieldsSchema(
|
||||
SecretSync.Databricks,
|
||||
DatabricksSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: DatabricksSyncDestinationConfigSchema
|
||||
});
|
||||
|
||||
export const UpdateDatabricksSyncSchema = GenericUpdateSecretSyncFieldsSchema(
|
||||
SecretSync.Databricks,
|
||||
DatabricksSyncOptionsConfig
|
||||
).extend({
|
||||
destinationConfig: DatabricksSyncDestinationConfigSchema.optional()
|
||||
});
|
||||
|
||||
export const DatabricksSyncListItemSchema = z.object({
|
||||
name: z.literal("Databricks"),
|
||||
connection: z.literal(AppConnection.Databricks),
|
||||
destination: z.literal(SecretSync.Databricks),
|
||||
canImportSecrets: z.literal(false)
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { TDatabricksConnection } from "@app/services/app-connection/databricks";
|
||||
|
||||
import {
|
||||
CreateDatabricksSyncSchema,
|
||||
DatabricksSyncListItemSchema,
|
||||
DatabricksSyncSchema
|
||||
} from "./databricks-sync-schemas";
|
||||
|
||||
export type TDatabricksSync = z.infer<typeof DatabricksSyncSchema>;
|
||||
|
||||
export type TDatabricksSyncInput = z.infer<typeof CreateDatabricksSyncSchema>;
|
||||
|
||||
export type TDatabricksSyncListItem = z.infer<typeof DatabricksSyncListItemSchema>;
|
||||
|
||||
export type TDatabricksSyncWithCredentials = TDatabricksSync & {
|
||||
connection: TDatabricksConnection;
|
||||
};
|
||||
|
||||
export type TDatabricksListSecretKeysResponse = {
|
||||
secrets?: { key: string; last_updated_timestamp: number }[];
|
||||
};
|
||||
|
||||
type TBaseDatabricksSecretRequest = {
|
||||
scope: string;
|
||||
workspaceUrl: string;
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
export type TDatabricksListSecretKeys = TBaseDatabricksSecretRequest;
|
||||
|
||||
export type TDatabricksPutSecret = {
|
||||
key: string;
|
||||
value?: string;
|
||||
} & TBaseDatabricksSecretRequest;
|
||||
|
||||
export type TDatabricksDeleteSecret = {
|
||||
key: string;
|
||||
} & TBaseDatabricksSecretRequest;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./databricks-sync-constants";
|
||||
export * from "./databricks-sync-fns";
|
||||
export * from "./databricks-sync-schemas";
|
||||
export * from "./databricks-sync-types";
|
||||
@@ -1,5 +1,6 @@
|
||||
import z from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseSecretSyncSchema,
|
||||
@@ -14,8 +15,8 @@ import { GcpSyncScope } from "./gcp-sync-enums";
|
||||
const GcpSyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: true };
|
||||
|
||||
const GcpSyncDestinationConfigSchema = z.object({
|
||||
scope: z.literal(GcpSyncScope.Global),
|
||||
projectId: z.string().min(1, "Project ID is required")
|
||||
scope: z.literal(GcpSyncScope.Global).describe(SecretSyncs.DESTINATION_CONFIG.GCP.scope),
|
||||
projectId: z.string().min(1, "Project ID is required").describe(SecretSyncs.DESTINATION_CONFIG.GCP.projectId)
|
||||
});
|
||||
|
||||
export const GcpSyncSchema = BaseSecretSyncSchema(SecretSync.GCPSecretManager, GcpSyncOptionsConfig).extend({
|
||||
|
||||
@@ -14,21 +14,21 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
|
||||
const GitHubSyncDestinationConfigSchema = z
|
||||
.discriminatedUnion("scope", [
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Organization),
|
||||
org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.ORG),
|
||||
scope: z.literal(GitHubSyncScope.Organization).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope),
|
||||
org: z.string().min(1, "Organization name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.org),
|
||||
visibility: z.nativeEnum(GitHubSyncVisibility),
|
||||
selectedRepositoryIds: z.number().array().optional()
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.Repository),
|
||||
owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.OWNER),
|
||||
repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.REPO)
|
||||
scope: z.literal(GitHubSyncScope.Repository).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope),
|
||||
owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner),
|
||||
repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment),
|
||||
owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.OWNER),
|
||||
repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.REPO),
|
||||
env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.ENV)
|
||||
scope: z.literal(GitHubSyncScope.RepositoryEnvironment).describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.scope),
|
||||
owner: z.string().min(1, "Repository owner name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.owner),
|
||||
repo: z.string().min(1, "Repository name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.repo),
|
||||
env: z.string().min(1, "Environment name required").describe(SecretSyncs.DESTINATION_CONFIG.GITHUB.env)
|
||||
})
|
||||
])
|
||||
.superRefine((options, ctx) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TSecretSyncs } from "@app/db/schemas/secret-syncs";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
|
||||
export type TSecretSyncDALFactory = ReturnType<typeof secretSyncDALFactory>;
|
||||
@@ -34,17 +34,9 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
|
||||
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt")
|
||||
);
|
||||
|
||||
// prepends table name to filter keys to avoid ambiguous col references, skipping utility filters like $in, etc.
|
||||
const prependTableName = (filterObj: object): SecretSyncFindFilter =>
|
||||
Object.fromEntries(
|
||||
Object.entries(filterObj).map(([key, value]) =>
|
||||
key.startsWith("$") ? [key, prependTableName(value as object)] : [`${TableName.SecretSync}.${key}`, value]
|
||||
)
|
||||
);
|
||||
|
||||
if (filter) {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
void query.where(buildFindFilter(prependTableName(filter)));
|
||||
void query.where(buildFindFilter(prependTableNameToFindFilter(TableName.SecretSync, filter)));
|
||||
}
|
||||
|
||||
return query;
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export enum SecretSync {
|
||||
AWSParameterStore = "aws-parameter-store",
|
||||
AWSSecretsManager = "aws-secrets-manager",
|
||||
GitHub = "github",
|
||||
GCPSecretManager = "gcp-secret-manager"
|
||||
GCPSecretManager = "gcp-secret-manager",
|
||||
AzureKeyVault = "azure-key-vault",
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
Databricks = "databricks"
|
||||
}
|
||||
|
||||
export enum SecretSyncInitialSyncBehavior {
|
||||
|
||||
@@ -4,6 +4,11 @@ import {
|
||||
AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
AwsParameterStoreSyncFns
|
||||
} from "@app/services/secret-sync/aws-parameter-store";
|
||||
import {
|
||||
AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||
AwsSecretsManagerSyncFns
|
||||
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||
import { DATABRICKS_SYNC_LIST_OPTION, databricksSyncFactory } from "@app/services/secret-sync/databricks";
|
||||
import { GITHUB_SYNC_LIST_OPTION, GithubSyncFns } from "@app/services/secret-sync/github";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
@@ -13,19 +18,32 @@ import {
|
||||
TSecretSyncWithCredentials
|
||||
} from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION, azureAppConfigurationSyncFactory } from "./azure-app-configuration";
|
||||
import { AZURE_KEY_VAULT_SYNC_LIST_OPTION, azureKeyVaultSyncFactory } from "./azure-key-vault";
|
||||
import { GCP_SYNC_LIST_OPTION } from "./gcp";
|
||||
import { GcpSyncFns } from "./gcp/gcp-sync-fns";
|
||||
|
||||
const SECRET_SYNC_LIST_OPTIONS: Record<SecretSync, TSecretSyncListItem> = {
|
||||
[SecretSync.AWSParameterStore]: AWS_PARAMETER_STORE_SYNC_LIST_OPTION,
|
||||
[SecretSync.AWSSecretsManager]: AWS_SECRETS_MANAGER_SYNC_LIST_OPTION,
|
||||
[SecretSync.GitHub]: GITHUB_SYNC_LIST_OPTION,
|
||||
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION
|
||||
[SecretSync.GCPSecretManager]: GCP_SYNC_LIST_OPTION,
|
||||
[SecretSync.AzureKeyVault]: AZURE_KEY_VAULT_SYNC_LIST_OPTION,
|
||||
[SecretSync.AzureAppConfiguration]: AZURE_APP_CONFIGURATION_SYNC_LIST_OPTION,
|
||||
[SecretSync.Databricks]: DATABRICKS_SYNC_LIST_OPTION
|
||||
};
|
||||
|
||||
export const listSecretSyncOptions = () => {
|
||||
return Object.values(SECRET_SYNC_LIST_OPTIONS).sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
type TSyncSecretDeps = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
// const addAffixes = (secretSync: TSecretSyncWithCredentials, unprocessedSecretMap: TSecretMap) => {
|
||||
// let secretMap = { ...unprocessedSecretMap };
|
||||
//
|
||||
@@ -67,34 +85,78 @@ export const listSecretSyncOptions = () => {
|
||||
// };
|
||||
|
||||
export const SecretSyncFns = {
|
||||
syncSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
|
||||
syncSecrets: (
|
||||
secretSync: TSecretSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
): Promise<void> => {
|
||||
// const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return AwsParameterStoreSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.AWSSecretsManager:
|
||||
return AwsSecretsManagerSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.GCPSecretManager:
|
||||
return GcpSyncFns.syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.AzureKeyVault:
|
||||
return azureKeyVaultSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.AzureAppConfiguration:
|
||||
return azureAppConfigurationSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).syncSecrets(secretSync, secretMap);
|
||||
case SecretSync.Databricks:
|
||||
return databricksSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).syncSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for sync secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
);
|
||||
}
|
||||
},
|
||||
getSecrets: async (secretSync: TSecretSyncWithCredentials): Promise<TSecretMap> => {
|
||||
getSecrets: async (
|
||||
secretSync: TSecretSyncWithCredentials,
|
||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
): Promise<TSecretMap> => {
|
||||
let secretMap: TSecretMap;
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
secretMap = await AwsParameterStoreSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.AWSSecretsManager:
|
||||
secretMap = await AwsSecretsManagerSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.GitHub:
|
||||
secretMap = await GithubSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.GCPSecretManager:
|
||||
secretMap = await GcpSyncFns.getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.AzureKeyVault:
|
||||
secretMap = await azureKeyVaultSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.AzureAppConfiguration:
|
||||
secretMap = await azureAppConfigurationSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).getSecrets(secretSync);
|
||||
break;
|
||||
case SecretSync.Databricks:
|
||||
return databricksSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).getSecrets(secretSync);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for get secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -104,16 +166,37 @@ export const SecretSyncFns = {
|
||||
return secretMap;
|
||||
// return stripAffixes(secretSync, secretMap);
|
||||
},
|
||||
removeSecrets: (secretSync: TSecretSyncWithCredentials, secretMap: TSecretMap): Promise<void> => {
|
||||
removeSecrets: (
|
||||
secretSync: TSecretSyncWithCredentials,
|
||||
secretMap: TSecretMap,
|
||||
{ kmsService, appConnectionDAL }: TSyncSecretDeps
|
||||
): Promise<void> => {
|
||||
// const affixedSecretMap = addAffixes(secretSync, secretMap);
|
||||
|
||||
switch (secretSync.destination) {
|
||||
case SecretSync.AWSParameterStore:
|
||||
return AwsParameterStoreSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.AWSSecretsManager:
|
||||
return AwsSecretsManagerSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GitHub:
|
||||
return GithubSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.GCPSecretManager:
|
||||
return GcpSyncFns.removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.AzureKeyVault:
|
||||
return azureKeyVaultSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.AzureAppConfiguration:
|
||||
return azureAppConfigurationSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).removeSecrets(secretSync, secretMap);
|
||||
case SecretSync.Databricks:
|
||||
return databricksSyncFactory({
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}).removeSecrets(secretSync, secretMap);
|
||||
default:
|
||||
throw new Error(
|
||||
`Unhandled sync destination for remove secrets fns: ${(secretSync as TSecretSyncWithCredentials).destination}`
|
||||
@@ -122,17 +205,25 @@ export const SecretSyncFns = {
|
||||
}
|
||||
};
|
||||
|
||||
const MAX_MESSAGE_LENGTH = 1024;
|
||||
|
||||
export const parseSyncErrorMessage = (err: unknown): string => {
|
||||
let errorMessage: string;
|
||||
|
||||
if (err instanceof SecretSyncError) {
|
||||
return JSON.stringify({
|
||||
errorMessage = JSON.stringify({
|
||||
secretKey: err.secretKey,
|
||||
error: err.message || parseSyncErrorMessage(err.error)
|
||||
});
|
||||
} else if (err instanceof AxiosError) {
|
||||
errorMessage = err?.response?.data
|
||||
? JSON.stringify(err?.response?.data)
|
||||
: err?.message ?? "An unknown error occurred.";
|
||||
} else {
|
||||
errorMessage = (err as Error)?.message || "An unknown error occurred.";
|
||||
}
|
||||
|
||||
if (err instanceof AxiosError) {
|
||||
return err?.response?.data ? JSON.stringify(err?.response?.data) : err?.message ?? "An unknown error occurred.";
|
||||
}
|
||||
|
||||
return (err as Error)?.message || "An unknown error occurred.";
|
||||
return errorMessage.length <= MAX_MESSAGE_LENGTH
|
||||
? errorMessage
|
||||
: `${errorMessage.substring(0, MAX_MESSAGE_LENGTH - 3)}...`;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,20 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
export const SECRET_SYNC_NAME_MAP: Record<SecretSync, string> = {
|
||||
[SecretSync.AWSParameterStore]: "AWS Parameter Store",
|
||||
[SecretSync.AWSSecretsManager]: "AWS Secrets Manager",
|
||||
[SecretSync.GitHub]: "GitHub",
|
||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager"
|
||||
[SecretSync.GCPSecretManager]: "GCP Secret Manager",
|
||||
[SecretSync.AzureKeyVault]: "Azure Key Vault",
|
||||
[SecretSync.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[SecretSync.Databricks]: "Databricks"
|
||||
};
|
||||
|
||||
export const SECRET_SYNC_CONNECTION_MAP: Record<SecretSync, AppConnection> = {
|
||||
[SecretSync.AWSParameterStore]: AppConnection.AWS,
|
||||
[SecretSync.AWSSecretsManager]: AppConnection.AWS,
|
||||
[SecretSync.GitHub]: AppConnection.GitHub,
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP
|
||||
[SecretSync.GCPSecretManager]: AppConnection.GCP,
|
||||
[SecretSync.AzureKeyVault]: AppConnection.AzureKeyVault,
|
||||
[SecretSync.AzureAppConfiguration]: AppConnection.AzureAppConfiguration,
|
||||
[SecretSync.Databricks]: AppConnection.Databricks
|
||||
};
|
||||
|
||||
@@ -57,11 +57,14 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
|
||||
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
|
||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
|
||||
type TSecretSyncQueueFactoryDep = {
|
||||
queueService: Pick<TQueueServiceFactory, "queue" | "start">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
secretV2BridgeDAL: Pick<
|
||||
@@ -111,6 +114,7 @@ const getRequeueDelay = (failureCount?: number) => {
|
||||
export const secretSyncQueueFactory = ({
|
||||
queueService,
|
||||
kmsService,
|
||||
appConnectionDAL,
|
||||
keyStore,
|
||||
folderDAL,
|
||||
secretV2BridgeDAL,
|
||||
@@ -322,7 +326,10 @@ export const secretSyncQueueFactory = ({
|
||||
"Invalid Secret Sync source configuration: folder no longer exists. Please update source environment and secret path."
|
||||
);
|
||||
|
||||
const importedSecrets = await SecretSyncFns.getSecrets(secretSync);
|
||||
const importedSecrets = await SecretSyncFns.getSecrets(secretSync, {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
if (!Object.keys(importedSecrets).length) return {};
|
||||
|
||||
@@ -434,7 +441,10 @@ export const secretSyncQueueFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap);
|
||||
await SecretSyncFns.syncSecrets(secretSyncWithCredentials, secretMap, {
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
});
|
||||
|
||||
isSynced = true;
|
||||
} catch (err) {
|
||||
@@ -672,7 +682,11 @@ export const secretSyncQueueFactory = ({
|
||||
credentials
|
||||
}
|
||||
} as TSecretSyncWithCredentials,
|
||||
secretMap
|
||||
secretMap,
|
||||
{
|
||||
appConnectionDAL,
|
||||
kmsService
|
||||
}
|
||||
);
|
||||
|
||||
isSuccess = true;
|
||||
|
||||
@@ -13,7 +13,7 @@ const SyncOptionsSchema = (secretSync: SecretSync, options: TSyncOptionsConfig =
|
||||
initialSyncBehavior: (options.canImportSecrets
|
||||
? z.nativeEnum(SecretSyncInitialSyncBehavior)
|
||||
: z.literal(SecretSyncInitialSyncBehavior.OverwriteDestination)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).INITIAL_SYNC_BEHAVIOR)
|
||||
).describe(SecretSyncs.SYNC_OPTIONS(secretSync).initialSyncBehavior)
|
||||
// prependPrefix: z
|
||||
// .string()
|
||||
// .trim()
|
||||
|
||||
@@ -2,6 +2,18 @@ import { Job } from "bullmq";
|
||||
|
||||
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { QueueJobs } from "@app/queue";
|
||||
import {
|
||||
TAwsSecretsManagerSync,
|
||||
TAwsSecretsManagerSyncInput,
|
||||
TAwsSecretsManagerSyncListItem,
|
||||
TAwsSecretsManagerSyncWithCredentials
|
||||
} from "@app/services/secret-sync/aws-secrets-manager";
|
||||
import {
|
||||
TDatabricksSync,
|
||||
TDatabricksSyncInput,
|
||||
TDatabricksSyncListItem,
|
||||
TDatabricksSyncWithCredentials
|
||||
} from "@app/services/secret-sync/databricks";
|
||||
import {
|
||||
TGitHubSync,
|
||||
TGitHubSyncInput,
|
||||
@@ -17,18 +29,55 @@ import {
|
||||
TAwsParameterStoreSyncListItem,
|
||||
TAwsParameterStoreSyncWithCredentials
|
||||
} from "./aws-parameter-store";
|
||||
import {
|
||||
TAzureAppConfigurationSync,
|
||||
TAzureAppConfigurationSyncInput,
|
||||
TAzureAppConfigurationSyncListItem,
|
||||
TAzureAppConfigurationSyncWithCredentials
|
||||
} from "./azure-app-configuration";
|
||||
import {
|
||||
TAzureKeyVaultSync,
|
||||
TAzureKeyVaultSyncInput,
|
||||
TAzureKeyVaultSyncListItem,
|
||||
TAzureKeyVaultSyncWithCredentials
|
||||
} from "./azure-key-vault";
|
||||
import { TGcpSync, TGcpSyncInput, TGcpSyncListItem, TGcpSyncWithCredentials } from "./gcp";
|
||||
|
||||
export type TSecretSync = TAwsParameterStoreSync | TGitHubSync | TGcpSync;
|
||||
export type TSecretSync =
|
||||
| TAwsParameterStoreSync
|
||||
| TAwsSecretsManagerSync
|
||||
| TGitHubSync
|
||||
| TGcpSync
|
||||
| TAzureKeyVaultSync
|
||||
| TAzureAppConfigurationSync
|
||||
| TDatabricksSync;
|
||||
|
||||
export type TSecretSyncWithCredentials =
|
||||
| TAwsParameterStoreSyncWithCredentials
|
||||
| TAwsSecretsManagerSyncWithCredentials
|
||||
| TGitHubSyncWithCredentials
|
||||
| TGcpSyncWithCredentials;
|
||||
| TGcpSyncWithCredentials
|
||||
| TAzureKeyVaultSyncWithCredentials
|
||||
| TAzureAppConfigurationSyncWithCredentials
|
||||
| TDatabricksSyncWithCredentials;
|
||||
|
||||
export type TSecretSyncInput = TAwsParameterStoreSyncInput | TGitHubSyncInput | TGcpSyncInput;
|
||||
export type TSecretSyncInput =
|
||||
| TAwsParameterStoreSyncInput
|
||||
| TAwsSecretsManagerSyncInput
|
||||
| TGitHubSyncInput
|
||||
| TGcpSyncInput
|
||||
| TAzureKeyVaultSyncInput
|
||||
| TAzureAppConfigurationSyncInput
|
||||
| TDatabricksSyncInput;
|
||||
|
||||
export type TSecretSyncListItem = TAwsParameterStoreSyncListItem | TGitHubSyncListItem | TGcpSyncListItem;
|
||||
export type TSecretSyncListItem =
|
||||
| TAwsParameterStoreSyncListItem
|
||||
| TAwsSecretsManagerSyncListItem
|
||||
| TGitHubSyncListItem
|
||||
| TGcpSyncListItem
|
||||
| TAzureKeyVaultSyncListItem
|
||||
| TAzureAppConfigurationSyncListItem
|
||||
| TDatabricksSyncListItem;
|
||||
|
||||
export type TSyncOptionsConfig = {
|
||||
canImportSecrets: boolean;
|
||||
|
||||
@@ -414,6 +414,20 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.where((qb) => {
|
||||
if (filters?.metadataFilter && filters.metadataFilter.length > 0) {
|
||||
filters.metadataFilter.forEach((meta) => {
|
||||
void qb.whereExists((subQuery) => {
|
||||
void subQuery
|
||||
.select("secretId")
|
||||
.from(TableName.ResourceMetadata)
|
||||
.whereRaw(`"${TableName.ResourceMetadata}"."secretId" = "${TableName.SecretV2}"."id"`)
|
||||
.where(`${TableName.ResourceMetadata}.key`, meta.key)
|
||||
.where(`${TableName.ResourceMetadata}.value`, meta.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretV2),
|
||||
db.raw(
|
||||
@@ -481,6 +495,7 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "get all secret" });
|
||||
|
||||
@@ -1291,8 +1291,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
]
|
||||
}
|
||||
});
|
||||
if (secretsToUpdate.length !== inputSecrets.length)
|
||||
throw new NotFoundError({ message: `Secret does not exist: ${secretsToUpdate.map((el) => el.key).join(",")}` });
|
||||
if (secretsToUpdate.length !== inputSecrets.length) {
|
||||
const secretsToUpdateNames = secretsToUpdate.map((secret) => secret.key);
|
||||
const invalidSecrets = inputSecrets.filter((secret) => !secretsToUpdateNames.includes(secret.secretKey));
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${invalidSecrets.map((el) => el.secretKey).join(",")}`
|
||||
});
|
||||
}
|
||||
const secretsToUpdateInDBGroupedByKey = groupBy(secretsToUpdate, (i) => i.key);
|
||||
|
||||
secretsToUpdate.forEach((el) => {
|
||||
|
||||
@@ -30,6 +30,10 @@ export type TGetSecretsDTO = {
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
metadataFilter?: {
|
||||
key?: string;
|
||||
value?: string;
|
||||
}[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
offset?: number;
|
||||
@@ -310,6 +314,7 @@ export type TFindSecretsByFolderIdsFilter = {
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
tagSlugs?: string[];
|
||||
metadataFilter?: { key?: string; value?: string }[];
|
||||
includeTagsInSearch?: boolean;
|
||||
keys?: string[];
|
||||
};
|
||||
|
||||
@@ -1488,7 +1488,18 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
|
||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: job.data.projectId
|
||||
});
|
||||
|
||||
await fnTriggerWebhook({
|
||||
...job.data,
|
||||
projectEnvDAL,
|
||||
webhookDAL,
|
||||
projectDAL,
|
||||
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1263,6 +1263,13 @@ export const secretServiceFactory = ({
|
||||
name: "bot_not_found_error"
|
||||
});
|
||||
|
||||
if (paramsV2.metadataFilter) {
|
||||
throw new BadRequestError({
|
||||
message: "Please upgrade your project to filter secrets by metadata",
|
||||
name: "SecretMetadataNotSupported"
|
||||
});
|
||||
}
|
||||
|
||||
const { secrets, imports } = await getSecrets({
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -1444,7 +1451,7 @@ export const secretServiceFactory = ({
|
||||
decryptedSecret.secretValue = expandedSecretValue || "";
|
||||
}
|
||||
|
||||
return decryptedSecret;
|
||||
return { secretMetadata: undefined, ...decryptedSecret };
|
||||
};
|
||||
|
||||
const createSecretRaw = async ({
|
||||
|
||||
@@ -182,6 +182,10 @@ export type TGetSecretsRawDTO = {
|
||||
includeImports?: boolean;
|
||||
recursive?: boolean;
|
||||
tagSlugs?: string[];
|
||||
metadataFilter?: {
|
||||
key?: string;
|
||||
value?: string;
|
||||
}[];
|
||||
orderBy?: SecretsOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
offset?: number;
|
||||
|
||||
@@ -2,7 +2,7 @@ import bcrypt from "bcrypt";
|
||||
|
||||
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||
@@ -87,17 +87,21 @@ export const superAdminServiceFactory = ({
|
||||
|
||||
// reset on initialized
|
||||
await keyStore.deleteItem(ADMIN_CONFIG_KEY);
|
||||
const serverCfg = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (serverCfg) return;
|
||||
const serverCfg = await serverCfgDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.SuperAdminInit]);
|
||||
const serverCfgInDB = await serverCfgDAL.findById(ADMIN_CONFIG_DB_UUID);
|
||||
if (serverCfgInDB) return serverCfgInDB;
|
||||
|
||||
const newCfg = await serverCfgDAL.create({
|
||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||
id: ADMIN_CONFIG_DB_UUID,
|
||||
initialized: false,
|
||||
allowSignUp: true,
|
||||
defaultAuthOrgId: null
|
||||
const newCfg = await serverCfgDAL.create({
|
||||
// @ts-expect-error id is kept as fixed for idempotence and to avoid race condition
|
||||
id: ADMIN_CONFIG_DB_UUID,
|
||||
initialized: false,
|
||||
allowSignUp: true,
|
||||
defaultAuthOrgId: null
|
||||
});
|
||||
return newCfg;
|
||||
});
|
||||
return newCfg;
|
||||
return serverCfg;
|
||||
};
|
||||
|
||||
const updateServerCfg = async (
|
||||
|
||||
@@ -3,9 +3,8 @@ import crypto from "node:crypto";
|
||||
import { AxiosError } from "axios";
|
||||
import picomatch from "picomatch";
|
||||
|
||||
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
|
||||
import { TWebhooks } from "@app/db/schemas";
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
@@ -16,28 +15,14 @@ import { WebhookType } from "./webhook-types";
|
||||
|
||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||
|
||||
export const decryptWebhookDetails = (webhook: TWebhooks) => {
|
||||
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
|
||||
export const decryptWebhookDetails = (webhook: TWebhooks, decryptor: (value: Buffer) => string) => {
|
||||
const { encryptedPassKey, encryptedUrl } = webhook;
|
||||
|
||||
const decryptedUrl = decryptor(encryptedUrl);
|
||||
|
||||
let decryptedSecretKey = "";
|
||||
let decryptedUrl = url;
|
||||
|
||||
if (encryptedSecretKey) {
|
||||
decryptedSecretKey = infisicalSymmetricDecrypt({
|
||||
keyEncoding: keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: encryptedSecretKey,
|
||||
iv: iv as string,
|
||||
tag: tag as string
|
||||
});
|
||||
}
|
||||
|
||||
if (urlCipherText) {
|
||||
decryptedUrl = infisicalSymmetricDecrypt({
|
||||
keyEncoding: keyEncoding as SecretKeyEncoding,
|
||||
ciphertext: urlCipherText,
|
||||
iv: urlIV as string,
|
||||
tag: urlTag as string
|
||||
});
|
||||
if (encryptedPassKey) {
|
||||
decryptedSecretKey = decryptor(encryptedPassKey);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -46,10 +31,14 @@ export const decryptWebhookDetails = (webhook: TWebhooks) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record<string, unknown>) => {
|
||||
export const triggerWebhookRequest = async (
|
||||
webhook: TWebhooks,
|
||||
decryptor: (value: Buffer) => string,
|
||||
data: Record<string, unknown>
|
||||
) => {
|
||||
const headers: Record<string, string> = {};
|
||||
const payload = { ...data, timestamp: Date.now() };
|
||||
const { secretKey, url } = decryptWebhookDetails(webhook);
|
||||
const { secretKey, url } = decryptWebhookDetails(webhook, decryptor);
|
||||
|
||||
if (secretKey) {
|
||||
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
|
||||
@@ -124,6 +113,7 @@ export type TFnTriggerWebhookDTO = {
|
||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
secretManagerDecryptor: (value: Buffer) => string;
|
||||
};
|
||||
|
||||
// this is reusable function
|
||||
@@ -134,7 +124,8 @@ export const fnTriggerWebhook = async ({
|
||||
projectId,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
secretManagerDecryptor
|
||||
}: TFnTriggerWebhookDTO) => {
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||
const toBeTriggeredHooks = webhooks.filter(
|
||||
@@ -148,6 +139,7 @@ export const fnTriggerWebhook = async ({
|
||||
toBeTriggeredHooks.map((hook) =>
|
||||
triggerWebhookRequest(
|
||||
hook,
|
||||
secretManagerDecryptor,
|
||||
getWebhookPayload("secrets.modified", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: projectId,
|
||||
|
||||
@@ -3,9 +3,10 @@ import { ForbiddenError } from "@casl/ability";
|
||||
import { ActionProjectType, TWebhooksInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TWebhookDALFactory } from "./webhook-dal";
|
||||
@@ -23,6 +24,7 @@ type TWebhookServiceFactoryDep = {
|
||||
projectEnvDAL: TProjectEnvDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
||||
@@ -31,7 +33,8 @@ export const webhookServiceFactory = ({
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
permissionService,
|
||||
projectDAL
|
||||
projectDAL,
|
||||
kmsService
|
||||
}: TWebhookServiceFactoryDep) => {
|
||||
const createWebhook = async ({
|
||||
actor,
|
||||
@@ -60,30 +63,20 @@ export const webhookServiceFactory = ({
|
||||
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`
|
||||
});
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
const insertDoc: TWebhooksInsert = {
|
||||
url: "", // deprecated - we are moving away from plaintext URLs
|
||||
envId: env.id,
|
||||
isDisabled: false,
|
||||
secretPath: secretPath || "/",
|
||||
type
|
||||
type,
|
||||
encryptedUrl: secretManagerEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob
|
||||
};
|
||||
|
||||
if (webhookSecretKey) {
|
||||
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
|
||||
insertDoc.encryptedSecretKey = ciphertext;
|
||||
insertDoc.iv = iv;
|
||||
insertDoc.tag = tag;
|
||||
insertDoc.algorithm = algorithm;
|
||||
insertDoc.keyEncoding = encoding;
|
||||
}
|
||||
|
||||
if (webhookUrl) {
|
||||
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
|
||||
insertDoc.urlCipherText = ciphertext;
|
||||
insertDoc.urlIV = iv;
|
||||
insertDoc.urlTag = tag;
|
||||
insertDoc.algorithm = algorithm;
|
||||
insertDoc.keyEncoding = encoding;
|
||||
insertDoc.encryptedPassKey = secretManagerEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob;
|
||||
}
|
||||
|
||||
const webhook = await webhookDAL.create(insertDoc);
|
||||
@@ -140,12 +133,17 @@ export const webhookServiceFactory = ({
|
||||
});
|
||||
|
||||
const project = await projectDAL.findById(webhook.projectId);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: project.id
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
let webhookError: string | undefined;
|
||||
try {
|
||||
await triggerWebhookRequest(
|
||||
webhook,
|
||||
(value) => secretManagerDecryptor({ cipherTextBlob: value }).toString(),
|
||||
getWebhookPayload("test", {
|
||||
workspaceName: project.name,
|
||||
workspaceId: webhook.projectId,
|
||||
@@ -185,8 +183,13 @@ export const webhookServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
|
||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
return webhooks.map((w) => {
|
||||
const { url } = decryptWebhookDetails(w);
|
||||
const { url } = decryptWebhookDetails(w, (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString());
|
||||
return {
|
||||
...w,
|
||||
url
|
||||
|
||||
Reference in New Issue
Block a user