mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 08:35:52 +00:00
Merge origin/main to feat/pki-sync-v2
This commit is contained in:
@@ -5,5 +5,6 @@ export enum GitLabConnectionMethod {
|
||||
|
||||
export enum GitLabAccessTokenType {
|
||||
Project = "project",
|
||||
Personal = "personal"
|
||||
Personal = "personal",
|
||||
Group = "group"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
THCVaultKubernetesAuthConfig,
|
||||
THCVaultKubernetesAuthRole,
|
||||
THCVaultKubernetesAuthRoleWithConfig,
|
||||
THCVaultKubernetesRole,
|
||||
THCVaultKubernetesSecretsConfig,
|
||||
THCVaultMount,
|
||||
THCVaultMountResponse
|
||||
} from "./hc-vault-connection-types";
|
||||
@@ -816,3 +818,122 @@ export const getHCVaultKubernetesAuthRoles = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getHCVaultKubernetesRoles = async (
|
||||
namespace: string,
|
||||
mountPath: string,
|
||||
connection: THCVaultConnection,
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||
): Promise<THCVaultKubernetesRole[]> => {
|
||||
// Remove trailing slash from mount path
|
||||
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||
|
||||
try {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||
// 1. Get the Kubernetes secrets engine configuration for this mount
|
||||
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const kubernetesConfig = configResponse.data;
|
||||
|
||||
// 2. List all roles in this mount
|
||||
let roleNames: string[] = [];
|
||||
try {
|
||||
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
roleNames = roleListResponse.data.keys || [];
|
||||
} catch (error) {
|
||||
// Vault returns 404 when no roles are configured yet
|
||||
if (error && typeof error === "object" && "response" in error) {
|
||||
const axiosError = error as { response?: { status?: number } };
|
||||
if (axiosError.response?.status === 404) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!roleNames || roleNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Fetch details for each role with concurrency control
|
||||
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
|
||||
|
||||
const roleDetailsPromises = roleNames.map((roleName) =>
|
||||
limiter(async () => {
|
||||
const { data: roleResponse } = await requestWithHCVaultGateway<{
|
||||
data: {
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
};
|
||||
}>(connection, gatewayService, {
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Merge the role with the config
|
||||
return {
|
||||
...roleResponse.data,
|
||||
name: roleName,
|
||||
config: kubernetesConfig,
|
||||
mountPath: cleanMountPath
|
||||
} as THCVaultKubernetesRole;
|
||||
})
|
||||
);
|
||||
|
||||
const roles = await Promise.all(roleDetailsPromises);
|
||||
|
||||
return roles;
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
|
||||
|
||||
if (error instanceof AxiosError) {
|
||||
const errorMessage =
|
||||
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
|
||||
config: THCVaultKubernetesAuthConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesSecretsConfig = {
|
||||
kubernetes_host: string;
|
||||
kubernetes_ca_cert?: string;
|
||||
};
|
||||
|
||||
export type THCVaultKubernetesRole = {
|
||||
name: string;
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
config: THCVaultKubernetesSecretsConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
@@ -82,7 +82,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
|
||||
if (existingEnv) {
|
||||
throw new BadRequestError({
|
||||
message: `Environment with slug '${slug}' already exist`,
|
||||
message: `Environment with slug '${slug}' already exists`,
|
||||
name: "CreateEnvironment"
|
||||
});
|
||||
}
|
||||
@@ -312,7 +312,7 @@ export const importDataIntoInfisicalFn = async ({
|
||||
);
|
||||
if (secretsByKeys.length) {
|
||||
throw new BadRequestError({
|
||||
message: `Secret already exist: ${secretsByKeys.map((el) => el.key).join(",")}`
|
||||
message: `Secret already exists: ${secretsByKeys.map((el) => el.key).join(",")}`
|
||||
});
|
||||
}
|
||||
await fnSecretBulkInsert({
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
convertVaultValueToString,
|
||||
getHCVaultAuthMounts,
|
||||
getHCVaultKubernetesAuthRoles,
|
||||
getHCVaultKubernetesRoles,
|
||||
getHCVaultSecretsForPath,
|
||||
HCVaultAuthType,
|
||||
listHCVaultMounts,
|
||||
@@ -762,6 +763,56 @@ export const externalMigrationServiceFactory = ({
|
||||
return roles;
|
||||
};
|
||||
|
||||
const getVaultKubernetesRoles = async ({
|
||||
actor,
|
||||
namespace,
|
||||
mountPath
|
||||
}: {
|
||||
actor: OrgServiceActor;
|
||||
namespace: string;
|
||||
mountPath: string;
|
||||
}) => {
|
||||
const { hasRole } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
orgId: actor.orgId,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId
|
||||
});
|
||||
|
||||
if (!hasRole(OrgMembershipRole.Admin)) {
|
||||
throw new ForbiddenRequestError({ message: "Only admins can get Kubernetes roles" });
|
||||
}
|
||||
|
||||
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
|
||||
orgId: actor.orgId,
|
||||
namespace
|
||||
});
|
||||
|
||||
if (!vaultConfig) {
|
||||
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
|
||||
}
|
||||
|
||||
if (!vaultConfig.connection) {
|
||||
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
|
||||
}
|
||||
|
||||
const credentials = await decryptAppConnectionCredentials({
|
||||
orgId: vaultConfig.orgId,
|
||||
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
|
||||
kmsService,
|
||||
projectId: null
|
||||
});
|
||||
|
||||
const connection = {
|
||||
...vaultConfig.connection,
|
||||
credentials
|
||||
} as THCVaultConnection;
|
||||
|
||||
return getHCVaultKubernetesRoles(namespace, mountPath, connection, gatewayService);
|
||||
};
|
||||
|
||||
return {
|
||||
importEnvKeyData,
|
||||
importVaultData,
|
||||
@@ -776,6 +827,7 @@ export const externalMigrationServiceFactory = ({
|
||||
getVaultAuthMounts,
|
||||
getVaultSecretPaths,
|
||||
importVaultSecrets,
|
||||
getVaultKubernetesAuthRoles
|
||||
getVaultKubernetesAuthRoles,
|
||||
getVaultKubernetesRoles
|
||||
};
|
||||
};
|
||||
|
||||
@@ -112,7 +112,8 @@ export const kmskeyDALFactory = (db: TDbClient) => {
|
||||
...KmsKeysSchema.parse(entry),
|
||||
isActive: !entry.isDisabled,
|
||||
algorithm: entry.internalKmsEncryptionAlgorithm,
|
||||
version: entry.internalKmsVersion
|
||||
version: entry.internalKmsVersion,
|
||||
kmipMetadata: entry.kmipMetadata as Record<string, unknown>
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find project cmeks" });
|
||||
|
||||
@@ -27,5 +27,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
scannerProductEnabled: true,
|
||||
shareSecretsProductEnabled: true,
|
||||
maxSharedSecretLifetime: true,
|
||||
maxSharedSecretViewLimit: true
|
||||
maxSharedSecretViewLimit: true,
|
||||
blockDuplicateSecretSyncDestinations: true
|
||||
});
|
||||
|
||||
@@ -405,7 +405,8 @@ export const orgServiceFactory = ({
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
}
|
||||
}: TUpdateOrgDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -516,7 +517,7 @@ export const orgServiceFactory = ({
|
||||
if (slug) {
|
||||
const existingOrg = await orgDAL.findOne({ slug, rootOrgId: null });
|
||||
if (existingOrg && existingOrg?.id !== orgId)
|
||||
throw new BadRequestError({ message: `Organization with slug ${slug} already exist` });
|
||||
throw new BadRequestError({ message: `Organization with slug ${slug} already exists` });
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced) {
|
||||
@@ -589,7 +590,8 @@ export const orgServiceFactory = ({
|
||||
scannerProductEnabled,
|
||||
shareSecretsProductEnabled,
|
||||
maxSharedSecretLifetime,
|
||||
maxSharedSecretViewLimit
|
||||
maxSharedSecretViewLimit,
|
||||
blockDuplicateSecretSyncDestinations
|
||||
});
|
||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||
return org;
|
||||
@@ -1147,7 +1149,7 @@ export const orgServiceFactory = ({
|
||||
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
|
||||
if (doesIncidentContactExist) {
|
||||
throw new BadRequestError({
|
||||
message: "Incident contact already exist",
|
||||
message: "Incident contact already exists",
|
||||
name: "Incident contact exist"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ export type TUpdateOrgDTO = {
|
||||
shareSecretsProductEnabled: boolean;
|
||||
maxSharedSecretLifetime: number;
|
||||
maxSharedSecretViewLimit: number | null;
|
||||
blockDuplicateSecretSyncDestinations: boolean;
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const projectEnvServiceFactory = ({
|
||||
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
||||
if (existingEnv)
|
||||
throw new BadRequestError({
|
||||
message: "Environment with slug already exist",
|
||||
message: "Environment with slug already exists",
|
||||
name: "CreateEnvironment"
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ export const projectEnvServiceFactory = ({
|
||||
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
||||
if (existingEnv && existingEnv.id !== id) {
|
||||
throw new BadRequestError({
|
||||
message: "Environment with slug already exist",
|
||||
message: "Environment with slug already exists",
|
||||
name: "UpdateEnvironment"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
ProjectType,
|
||||
ProjectVersion,
|
||||
TableName,
|
||||
TProjectEnvironments
|
||||
TProjectEnvironments,
|
||||
TProjects
|
||||
} from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
@@ -40,7 +41,8 @@ import { TSshHostGroupDALFactory } from "@app/ee/services/ssh-host-group/ssh-hos
|
||||
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
@@ -310,19 +312,34 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
type,
|
||||
description: workspaceDescription,
|
||||
orgId: organization.id,
|
||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
kmsSecretManagerKeyId: kmsKeyId,
|
||||
version: ProjectVersion.V3,
|
||||
pitVersionLimit: 10
|
||||
},
|
||||
tx
|
||||
);
|
||||
const slug = projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`);
|
||||
|
||||
let project: TProjects;
|
||||
try {
|
||||
project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
type,
|
||||
description: workspaceDescription,
|
||||
orgId: organization.id,
|
||||
slug,
|
||||
kmsSecretManagerKeyId: kmsKeyId,
|
||||
version: ProjectVersion.V3,
|
||||
pitVersionLimit: 10
|
||||
},
|
||||
tx
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof DatabaseError &&
|
||||
(err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `A project with the slug "${slug}" already exists in your organization. Please choose a different name or slug.`
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (type === ProjectType.SSH) {
|
||||
await bootstrapSshProject({
|
||||
@@ -586,39 +603,36 @@ export const projectServiceFactory = ({
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||
|
||||
if (update.slug) {
|
||||
const existingProject = await projectDAL.findOne({
|
||||
slug: update.slug,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
if (existingProject && existingProject.id !== project.id) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (update.secretDetectionIgnoreValues && !hasRole(ProjectMembershipRole.Admin)) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Only admins can update secret detection ignore values"
|
||||
});
|
||||
}
|
||||
|
||||
const updatedProject = await projectDAL.updateById(project.id, {
|
||||
name: update.name,
|
||||
description: update.description,
|
||||
autoCapitalization: update.autoCapitalization,
|
||||
enforceCapitalization: update.autoCapitalization,
|
||||
hasDeleteProtection: update.hasDeleteProtection,
|
||||
slug: update.slug,
|
||||
secretSharing: update.secretSharing,
|
||||
defaultProduct: update.defaultProduct,
|
||||
showSnapshotsLegacy: update.showSnapshotsLegacy,
|
||||
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues,
|
||||
pitVersionLimit: update.pitVersionLimit
|
||||
});
|
||||
try {
|
||||
const updatedProject = await projectDAL.updateById(project.id, {
|
||||
name: update.name,
|
||||
description: update.description,
|
||||
autoCapitalization: update.autoCapitalization,
|
||||
enforceCapitalization: update.autoCapitalization,
|
||||
hasDeleteProtection: update.hasDeleteProtection,
|
||||
slug: update.slug,
|
||||
secretSharing: update.secretSharing,
|
||||
defaultProduct: update.defaultProduct,
|
||||
showSnapshotsLegacy: update.showSnapshotsLegacy,
|
||||
secretDetectionIgnoreValues: update.secretDetectionIgnoreValues,
|
||||
pitVersionLimit: update.pitVersionLimit
|
||||
});
|
||||
|
||||
return updatedProject;
|
||||
return updatedProject;
|
||||
} catch (err) {
|
||||
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
|
||||
throw new BadRequestError({
|
||||
message: `Failed to update project. A project with the slug "${update.slug}" already exists in your organization. Please choose a different slug.`
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutoCapitalization = async ({
|
||||
|
||||
@@ -15,6 +15,8 @@ import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { deepEqualSkipFields } from "@app/lib/fn/object";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@@ -50,6 +52,8 @@ type TSecretSyncServiceFactoryDep = {
|
||||
secretImportDAL: TSecretImportDALFactory;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "validateAppConnectionUsageById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByProjectId" | "findById" | "findBySecretPath">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem">;
|
||||
@@ -68,6 +72,8 @@ export const secretSyncServiceFactory = ({
|
||||
secretImportDAL,
|
||||
permissionService,
|
||||
appConnectionService,
|
||||
projectDAL,
|
||||
orgDAL,
|
||||
projectBotService,
|
||||
secretSyncQueue,
|
||||
keyStore,
|
||||
@@ -225,6 +231,61 @@ export const secretSyncServiceFactory = ({
|
||||
return secretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||
if (baseFieldsMatch) {
|
||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||
sync.destinationConfig as Record<string, unknown>,
|
||||
destinationConfig
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const hasDuplicate = duplicates.length > 0;
|
||||
return {
|
||||
hasDuplicate,
|
||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const createSecretSync = async (
|
||||
{ projectId, secretPath, environment, ...params }: TCreateSecretSyncDTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -271,6 +332,30 @@ export const secretSyncServiceFactory = ({
|
||||
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
|
||||
const organization = await orgDAL.findById(project.orgId);
|
||||
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||
const duplicateCheck = await checkDuplicateDestination(
|
||||
{
|
||||
destination: params.destination,
|
||||
destinationConfig: params.destinationConfig,
|
||||
projectId
|
||||
},
|
||||
actor
|
||||
);
|
||||
if (duplicateCheck.hasDuplicate) {
|
||||
throw new BadRequestError({
|
||||
message: `A secret sync with this destination already exists${
|
||||
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||
}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[params.destination];
|
||||
|
||||
// validates permission to connect and app is valid for sync destination
|
||||
@@ -369,6 +454,33 @@ export const secretSyncServiceFactory = ({
|
||||
|
||||
let { folderId } = secretSync;
|
||||
|
||||
if (params.destinationConfig) {
|
||||
const project = await projectDAL.findById(secretSync.projectId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({ message: "Project not found" });
|
||||
}
|
||||
const organization = await orgDAL.findById(project.orgId);
|
||||
|
||||
if (organization?.blockDuplicateSecretSyncDestinations) {
|
||||
const duplicateCheck = await checkDuplicateDestination(
|
||||
{
|
||||
destination,
|
||||
destinationConfig: params.destinationConfig,
|
||||
projectId: secretSync.projectId,
|
||||
excludeSyncId: secretSync.id
|
||||
},
|
||||
actor
|
||||
);
|
||||
if (duplicateCheck.hasDuplicate) {
|
||||
throw new BadRequestError({
|
||||
message: `A secret sync with this destination already exists${
|
||||
duplicateCheck.duplicateProjectId ? ` in project ${duplicateCheck.duplicateProjectId}` : ""
|
||||
}.`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (params.connectionId) {
|
||||
const destinationApp = SECRET_SYNC_CONNECTION_MAP[secretSync.destination as SecretSync];
|
||||
|
||||
@@ -703,61 +815,6 @@ export const secretSyncServiceFactory = ({
|
||||
return updatedSecretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const skipFields = SECRET_SYNC_SKIP_FIELDS_MAP[destination];
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.findByDestinationAndOrgId(destination, actor.orgId);
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
|
||||
if (baseFieldsMatch) {
|
||||
return DESTINATION_DUPLICATE_CHECK_MAP[destination](
|
||||
sync.destinationConfig as Record<string, unknown>,
|
||||
destinationConfig
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const hasDuplicate = duplicates.length > 0;
|
||||
return {
|
||||
hasDuplicate,
|
||||
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
|
||||
};
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false, duplicateProjectId: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretSyncOptions,
|
||||
listSecretSyncsByProjectId,
|
||||
|
||||
@@ -35,7 +35,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
|
||||
|
||||
const existingTag = await secretTagDAL.findOne({ slug, projectId });
|
||||
if (existingTag) throw new BadRequestError({ message: "Tag already exist" });
|
||||
if (existingTag) throw new BadRequestError({ message: "Tag already exists" });
|
||||
|
||||
const newTag = await secretTagDAL.create({
|
||||
projectId,
|
||||
@@ -53,7 +53,7 @@ export const secretTagServiceFactory = ({ secretTagDAL, permissionService }: TSe
|
||||
|
||||
if (slug) {
|
||||
const existingTag = await secretTagDAL.findOne({ slug, projectId: tag.projectId });
|
||||
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exist" });
|
||||
if (existingTag && existingTag.id !== tag.id) throw new BadRequestError({ message: "Tag already exists" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
|
||||
@@ -282,7 +282,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
folderId
|
||||
});
|
||||
if (inputSecret.type === SecretType.Shared && doesSecretExist)
|
||||
throw new BadRequestError({ message: "Secret already exist" });
|
||||
throw new BadRequestError({ message: "Secret already exists" });
|
||||
|
||||
// if user creating personal check its shared also exist
|
||||
if (inputSecret.type === SecretType.Personal && !doesSecretExist) {
|
||||
@@ -527,7 +527,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: SecretType.Shared,
|
||||
folderId
|
||||
});
|
||||
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exist" });
|
||||
if (doesNewNameSecretExist) throw new BadRequestError({ message: "Secret with the new name already exists" });
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
@@ -1674,7 +1674,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
}
|
||||
});
|
||||
if (secrets.length)
|
||||
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
|
||||
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
|
||||
|
||||
@@ -525,7 +525,7 @@ export const fnSecretBlindIndexCheck = async ({
|
||||
);
|
||||
|
||||
if (isNew) {
|
||||
if (secrets.length) throw new BadRequestError({ message: "Secret already exist" });
|
||||
if (secrets.length) throw new BadRequestError({ message: "Secret already exists" });
|
||||
} else {
|
||||
const secretKeysInDB = unique(secrets, (el) => el.secretBlindIndex as string).map(
|
||||
(el) => blindIndex2KeyName[el.secretBlindIndex as string]
|
||||
@@ -819,7 +819,7 @@ export const createManySecretsRawFnFactory = ({
|
||||
);
|
||||
if (secretsStoredInDB.length)
|
||||
throw new BadRequestError({
|
||||
message: `Secret already exist: ${secretsStoredInDB.map((el) => el.key).join(",")}`
|
||||
message: `Secret already exists: ${secretsStoredInDB.map((el) => el.key).join(",")}`
|
||||
});
|
||||
|
||||
const inputSecrets = secrets.map((secret) => {
|
||||
|
||||
@@ -2751,7 +2751,7 @@ export const secretServiceFactory = ({
|
||||
const existingSecretTags = await secretDAL.getSecretTags(secret.id);
|
||||
|
||||
if (existingSecretTags.some((tag) => tagSlugs.includes(tag.slug))) {
|
||||
throw new BadRequestError({ message: "One or more tags already exist on the secret" });
|
||||
throw new BadRequestError({ message: "One or more tags already exists on the secret" });
|
||||
}
|
||||
|
||||
const combinedTags = new Set([...existingSecretTags.map((tag) => tag.id), ...tags.map((el) => el.id)]);
|
||||
|
||||
Reference in New Issue
Block a user