mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 05:19:49 +00:00
Merge remote-tracking branch 'origin/main' into feat/gateway-v2
This commit is contained in:
@@ -58,7 +58,7 @@ const getConnectionConfig = ({
|
||||
? {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
ca: sslCertificate,
|
||||
servername: host
|
||||
serverName: host
|
||||
}
|
||||
: false
|
||||
};
|
||||
@@ -92,7 +92,7 @@ export const getSqlConnectionClient = async (appConnection: Pick<TSqlConnection,
|
||||
connection: {
|
||||
database,
|
||||
port,
|
||||
host,
|
||||
host: app === AppConnection.Postgres ? host : baseHost,
|
||||
user: username,
|
||||
password,
|
||||
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
|
||||
@@ -174,7 +174,7 @@ export const executeWithPotentialGateway = async <T>(
|
||||
},
|
||||
{
|
||||
protocol: GatewayProxyProtocol.Tcp,
|
||||
targetHost,
|
||||
targetHost: app === AppConnection.Postgres ? targetHost : credentials.host,
|
||||
targetPort: credentials.port,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
|
||||
+3
-1
@@ -1190,7 +1190,9 @@ export const internalCertificateAuthorityServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
collectionId = certificateTemplate.pkiCollectionId as string;
|
||||
if (!collectionId) {
|
||||
collectionId = certificateTemplate.pkiCollectionId as string;
|
||||
}
|
||||
ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(certificateTemplate.caId);
|
||||
}
|
||||
|
||||
|
||||
@@ -408,19 +408,123 @@ export const transformToInfisicalFormatNamespaceToProjects = (
|
||||
};
|
||||
};
|
||||
|
||||
export const transformToInfisicalFormatKeyVaultToProjectsCustomC1 = (vaultData: VaultData[]): InfisicalImportData => {
|
||||
const projects: Array<{ name: string; id: string }> = [];
|
||||
const environments: Array<{ name: string; id: string; projectId: string; envParentId?: string }> = [];
|
||||
const folders: Array<{ id: string; name: string; environmentId: string; parentFolderId?: string }> = [];
|
||||
const secrets: Array<{ id: string; name: string; environmentId: string; value: string; folderId?: string }> = [];
|
||||
|
||||
// track created entities to avoid duplicates
|
||||
const projectMap = new Map<string, string>(); // team name -> projectId
|
||||
const environmentMap = new Map<string, string>(); // team-name:envName -> environmentId
|
||||
const folderMap = new Map<string, string>(); // team-name:envName:folderPath -> folderId
|
||||
|
||||
for (const data of vaultData) {
|
||||
const { path, secretData } = data;
|
||||
|
||||
const pathParts = path.split("/").filter(Boolean);
|
||||
if (pathParts.length < 2) {
|
||||
// eslint-disable-next-line no-continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// first level: environment (dev, prod, staging, etc.)
|
||||
const environmentName = pathParts[0];
|
||||
// second level: team name (team1, team2, etc.)
|
||||
const teamName = pathParts[1];
|
||||
// remaining parts: folder structure
|
||||
const folderParts = pathParts.slice(2);
|
||||
|
||||
// create project (team) if if doesn't exist
|
||||
if (!projectMap.has(teamName)) {
|
||||
const projectId = uuidv4();
|
||||
projectMap.set(teamName, projectId);
|
||||
projects.push({
|
||||
name: teamName,
|
||||
id: projectId
|
||||
});
|
||||
}
|
||||
const projectId = projectMap.get(teamName)!;
|
||||
|
||||
// create environment (dev, prod, etc.) for team
|
||||
const envKey = `${teamName}:${environmentName}`;
|
||||
if (!environmentMap.has(envKey)) {
|
||||
const environmentId = uuidv4();
|
||||
environmentMap.set(envKey, environmentId);
|
||||
environments.push({
|
||||
name: environmentName,
|
||||
id: environmentId,
|
||||
projectId
|
||||
});
|
||||
}
|
||||
const environmentId = environmentMap.get(envKey)!;
|
||||
|
||||
// create folder structure for path segments
|
||||
let currentFolderId: string | undefined;
|
||||
let currentPath = "";
|
||||
|
||||
for (const folderName of folderParts) {
|
||||
currentPath = currentPath ? `${currentPath}/${folderName}` : folderName;
|
||||
const folderKey = `${teamName}:${environmentName}:${currentPath}`;
|
||||
|
||||
if (!folderMap.has(folderKey)) {
|
||||
const folderId = uuidv4();
|
||||
folderMap.set(folderKey, folderId);
|
||||
folders.push({
|
||||
id: folderId,
|
||||
name: folderName,
|
||||
environmentId,
|
||||
parentFolderId: currentFolderId || environmentId
|
||||
});
|
||||
currentFolderId = folderId;
|
||||
} else {
|
||||
currentFolderId = folderMap.get(folderKey)!;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(secretData)) {
|
||||
secrets.push({
|
||||
id: uuidv4(),
|
||||
name: key,
|
||||
environmentId,
|
||||
value: String(value),
|
||||
folderId: currentFolderId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projects,
|
||||
environments,
|
||||
folders,
|
||||
secrets
|
||||
};
|
||||
};
|
||||
|
||||
// refer to internal doc for more details on which ID's belong to which orgs.
|
||||
// when its a custom migration, then it doesn't matter which mapping type is used (as of now).
|
||||
export const vaultMigrationTransformMappings: Record<
|
||||
string,
|
||||
(vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData
|
||||
> = {
|
||||
"68c57ab3-cea5-41fc-ae38-e156b10c14d2": transformToInfisicalFormatKeyVaultToProjectsCustomC1
|
||||
} as const;
|
||||
|
||||
export const importVaultDataFn = async (
|
||||
{
|
||||
vaultAccessToken,
|
||||
vaultNamespace,
|
||||
vaultUrl,
|
||||
mappingType,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
orgId
|
||||
}: {
|
||||
vaultAccessToken: string;
|
||||
vaultNamespace?: string;
|
||||
vaultUrl: string;
|
||||
mappingType: VaultMappingType;
|
||||
gatewayId?: string;
|
||||
orgId: string;
|
||||
},
|
||||
{ gatewayService }: { gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> }
|
||||
) => {
|
||||
@@ -432,6 +536,25 @@ export const importVaultDataFn = async (
|
||||
});
|
||||
}
|
||||
|
||||
let transformFn: (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData;
|
||||
|
||||
if (mappingType === VaultMappingType.Custom) {
|
||||
transformFn = vaultMigrationTransformMappings[orgId];
|
||||
|
||||
if (!transformFn) {
|
||||
throw new BadRequestError({
|
||||
message: "Please contact our sales team to enable custom vault migrations."
|
||||
});
|
||||
}
|
||||
} else {
|
||||
transformFn = transformToInfisicalFormatNamespaceToProjects;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
{ orgId, mappingType },
|
||||
`[importVaultDataFn]: Running ${orgId in vaultMigrationTransformMappings ? "custom" : "default"} transform`
|
||||
);
|
||||
|
||||
const vaultApi = vaultFactory(gatewayService);
|
||||
|
||||
const vaultData = await vaultApi.collectVaultData({
|
||||
@@ -441,7 +564,5 @@ export const importVaultDataFn = async (
|
||||
gatewayId
|
||||
});
|
||||
|
||||
const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
|
||||
|
||||
return infisicalData;
|
||||
return transformFn(vaultData, mappingType);
|
||||
};
|
||||
|
||||
@@ -5,9 +5,20 @@ import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns";
|
||||
import {
|
||||
decryptEnvKeyDataFn,
|
||||
importVaultDataFn,
|
||||
parseEnvKeyDataFn,
|
||||
vaultMigrationTransformMappings
|
||||
} from "./external-migration-fns";
|
||||
import { TExternalMigrationQueueFactory } from "./external-migration-queue";
|
||||
import { ExternalPlatforms, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types";
|
||||
import {
|
||||
ExternalMigrationProviders,
|
||||
ExternalPlatforms,
|
||||
THasCustomVaultMigrationDTO,
|
||||
TImportEnvKeyDataDTO,
|
||||
TImportVaultDataDTO
|
||||
} from "./external-migration-types";
|
||||
|
||||
type TExternalMigrationServiceFactoryDep = {
|
||||
permissionService: TPermissionServiceFactory;
|
||||
@@ -101,7 +112,8 @@ export const externalMigrationServiceFactory = ({
|
||||
vaultNamespace,
|
||||
vaultUrl,
|
||||
mappingType,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
orgId: actorOrgId
|
||||
},
|
||||
{
|
||||
gatewayService
|
||||
@@ -127,8 +139,37 @@ export const externalMigrationServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const hasCustomVaultMigration = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
provider
|
||||
}: THasCustomVaultMigrationDTO) => {
|
||||
const { membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (membership.role !== OrgMembershipRole.Admin) {
|
||||
throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" });
|
||||
}
|
||||
|
||||
if (provider !== ExternalMigrationProviders.Vault) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid provider. Vault is the only supported provider for custom migrations."
|
||||
});
|
||||
}
|
||||
|
||||
return actorOrgId in vaultMigrationTransformMappings;
|
||||
};
|
||||
|
||||
return {
|
||||
importEnvKeyData,
|
||||
importVaultData
|
||||
importVaultData,
|
||||
hasCustomVaultMigration
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,8 @@ import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
|
||||
export enum VaultMappingType {
|
||||
Namespace = "namespace",
|
||||
KeyVault = "key-vault"
|
||||
KeyVault = "key-vault",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
export type InfisicalImportData = {
|
||||
@@ -26,6 +27,10 @@ export type TImportEnvKeyDataDTO = {
|
||||
encryptedJson: { nonce: string; data: string };
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type THasCustomVaultMigrationDTO = {
|
||||
provider: ExternalMigrationProviders;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TImportVaultDataDTO = {
|
||||
vaultAccessToken: string;
|
||||
vaultNamespace?: string;
|
||||
@@ -111,3 +116,8 @@ export enum ExternalPlatforms {
|
||||
EnvKey = "EnvKey",
|
||||
Vault = "Vault"
|
||||
}
|
||||
|
||||
export enum ExternalMigrationProviders {
|
||||
Vault = "vault",
|
||||
EnvKey = "env-key"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ const getIntegrationSecretsV2 = async (
|
||||
},
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">,
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">,
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds" | "findByIds">
|
||||
) => {
|
||||
const content: Record<string, boolean> = {};
|
||||
if (dto.depth > MAX_SYNC_SECRET_DEPTH) {
|
||||
@@ -300,7 +300,7 @@ export const deleteIntegrationSecrets = async ({
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath" | "findBySecretPath">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds" | "findByIds">;
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
}) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ type TIntegrationServiceFactoryDep = {
|
||||
projectBotService: TProjectBotServiceFactory;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncIntegrations">;
|
||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find" | "findByFolderId">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds" | "findByIds">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { ProjectType, TableName } from "@app/db/schemas";
|
||||
|
||||
export type TOfflineUsageReportDALFactory = ReturnType<typeof offlineUsageReportDALFactory>;
|
||||
|
||||
export const offlineUsageReportDALFactory = (db: TDbClient) => {
|
||||
const getUserMetrics = async () => {
|
||||
// Get total users and admin users
|
||||
const userMetrics = (await db
|
||||
.from(TableName.Users)
|
||||
.select(
|
||||
db.raw(
|
||||
`
|
||||
COUNT(*) as total_users,
|
||||
COUNT(CASE WHEN "superAdmin" = true THEN 1 END) as admin_users
|
||||
`
|
||||
)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.first()) as { total_users: string; admin_users: string } | undefined;
|
||||
|
||||
// Get users by auth method
|
||||
const authMethodStats = (await db
|
||||
.from(TableName.Users)
|
||||
.select(
|
||||
db.raw(`
|
||||
unnest("authMethods") as auth_method,
|
||||
COUNT(*) as count
|
||||
`)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.whereNotNull("authMethods")
|
||||
.groupBy(db.raw('unnest("authMethods")'))) as Array<{ auth_method: string; count: string }>;
|
||||
|
||||
const usersByAuthMethod = authMethodStats.reduce(
|
||||
(acc: Record<string, number>, row: { auth_method: string; count: string }) => {
|
||||
acc[row.auth_method] = parseInt(row.count, 10);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
return {
|
||||
totalUsers: parseInt(userMetrics?.total_users || "0", 10),
|
||||
adminUsers: parseInt(userMetrics?.admin_users || "0", 10),
|
||||
usersByAuthMethod
|
||||
};
|
||||
};
|
||||
|
||||
const getMachineIdentityMetrics = async () => {
|
||||
// Get total machine identities
|
||||
const identityMetrics = (await db
|
||||
.from(TableName.Identity)
|
||||
.select(
|
||||
db.raw(
|
||||
`
|
||||
COUNT(*) as total_identities
|
||||
`
|
||||
)
|
||||
)
|
||||
.first()) as { total_identities: string } | undefined;
|
||||
|
||||
// Get identities by auth method
|
||||
const authMethodStats = (await db
|
||||
.from(TableName.Identity)
|
||||
.select("authMethod")
|
||||
.count("* as count")
|
||||
.whereNotNull("authMethod")
|
||||
.groupBy("authMethod")) as Array<{ authMethod: string; count: string }>;
|
||||
|
||||
const machineIdentitiesByAuthMethod = authMethodStats.reduce(
|
||||
(acc: Record<string, number>, row: { authMethod: string; count: string }) => {
|
||||
acc[row.authMethod] = parseInt(row.count, 10);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
return {
|
||||
totalMachineIdentities: parseInt(identityMetrics?.total_identities || "0", 10),
|
||||
machineIdentitiesByAuthMethod
|
||||
};
|
||||
};
|
||||
|
||||
const getProjectMetrics = async () => {
|
||||
// Get total projects and projects by type
|
||||
const projectMetrics = (await db
|
||||
.from(TableName.Project)
|
||||
.select("type")
|
||||
.count("* as count")
|
||||
.groupBy("type")) as Array<{ type: string; count: string }>;
|
||||
|
||||
const totalProjects = projectMetrics.reduce(
|
||||
(sum, row: { type: string; count: string }) => sum + parseInt(row.count, 10),
|
||||
0
|
||||
);
|
||||
const projectsByType = projectMetrics.reduce(
|
||||
(acc: Record<string, number>, row: { type: string; count: string }) => {
|
||||
acc[row.type] = parseInt(row.count, 10);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
// Calculate average secrets per project
|
||||
const secretsPerProject = (await db
|
||||
.from(`${TableName.SecretV2} as s`)
|
||||
.select("p.id as projectId")
|
||||
.count("s.id as count")
|
||||
.leftJoin(`${TableName.SecretFolder} as sf`, "s.folderId", "sf.id")
|
||||
.leftJoin(`${TableName.Environment} as e`, "sf.envId", "e.id")
|
||||
.leftJoin(`${TableName.Project} as p`, "e.projectId", "p.id")
|
||||
.where("p.type", ProjectType.SecretManager)
|
||||
.groupBy("p.id")
|
||||
.whereNotNull("p.id")) as Array<{ projectId: string; count: string }>;
|
||||
|
||||
const averageSecretsPerProject =
|
||||
secretsPerProject.length > 0
|
||||
? secretsPerProject.reduce(
|
||||
(sum, row: { projectId: string; count: string }) => sum + parseInt(row.count, 10),
|
||||
0
|
||||
) / secretsPerProject.length
|
||||
: 0;
|
||||
|
||||
return {
|
||||
totalProjects,
|
||||
projectsByType,
|
||||
averageSecretsPerProject: Math.round(averageSecretsPerProject * 100) / 100
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretMetrics = async () => {
|
||||
// Get total secrets
|
||||
const totalSecretsResult = (await db.from(TableName.SecretV2).count("* as count").first()) as
|
||||
| { count: string }
|
||||
| undefined;
|
||||
|
||||
const totalSecrets = parseInt(totalSecretsResult?.count || "0", 10);
|
||||
|
||||
// Get secrets by project
|
||||
const secretsByProject = (await db
|
||||
.from(`${TableName.SecretV2} as s`)
|
||||
.select("p.id as projectId", "p.name as projectName")
|
||||
.count("s.id as secretCount")
|
||||
.leftJoin(`${TableName.SecretFolder} as sf`, "s.folderId", "sf.id")
|
||||
.leftJoin(`${TableName.Environment} as e`, "sf.envId", "e.id")
|
||||
.leftJoin(`${TableName.Project} as p`, "e.projectId", "p.id")
|
||||
.where("p.type", ProjectType.SecretManager)
|
||||
.groupBy("p.id", "p.name")
|
||||
.whereNotNull("p.id")) as Array<{ projectId: string; projectName: string; secretCount: string }>;
|
||||
|
||||
return {
|
||||
totalSecrets,
|
||||
secretsByProject: secretsByProject.map(
|
||||
(row: { projectId: string; projectName: string; secretCount: string }) => ({
|
||||
projectId: row.projectId,
|
||||
projectName: row.projectName,
|
||||
secretCount: parseInt(row.secretCount, 10)
|
||||
})
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretSyncMetrics = async () => {
|
||||
const totalSecretSyncsResult = (await db.from(TableName.SecretSync).count("* as count").first()) as
|
||||
| { count: string }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
totalSecretSyncs: parseInt(totalSecretSyncsResult?.count || "0", 10)
|
||||
};
|
||||
};
|
||||
|
||||
const getDynamicSecretMetrics = async () => {
|
||||
const totalDynamicSecretsResult = (await db.from(TableName.DynamicSecret).count("* as count").first()) as
|
||||
| { count: string }
|
||||
| undefined;
|
||||
|
||||
return {
|
||||
totalDynamicSecrets: parseInt(totalDynamicSecretsResult?.count || "0", 10)
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretRotationMetrics = async () => {
|
||||
// Check both v1 and v2 secret rotation tables
|
||||
const [v1RotationsResult, v2RotationsResult] = await Promise.all([
|
||||
db.from(TableName.SecretRotation).count("* as count").first() as Promise<{ count: string } | undefined>,
|
||||
db.from(TableName.SecretRotationV2).count("* as count").first() as Promise<{ count: string } | undefined>
|
||||
]);
|
||||
|
||||
const totalV1Rotations = parseInt(v1RotationsResult?.count || "0", 10);
|
||||
const totalV2Rotations = parseInt(v2RotationsResult?.count || "0", 10);
|
||||
|
||||
return {
|
||||
totalSecretRotations: totalV1Rotations + totalV2Rotations
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getUserMetrics,
|
||||
getMachineIdentityMetrics,
|
||||
getProjectMetrics,
|
||||
getSecretMetrics,
|
||||
getSecretSyncMetrics,
|
||||
getDynamicSecretMetrics,
|
||||
getSecretRotationMetrics
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TOfflineUsageReportDALFactory } from "./offline-usage-report-dal";
|
||||
|
||||
type TOfflineUsageReportServiceFactoryDep = {
|
||||
offlineUsageReportDAL: TOfflineUsageReportDALFactory;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getCustomerId" | "getLicenseId">;
|
||||
};
|
||||
|
||||
export type TOfflineUsageReportServiceFactory = ReturnType<typeof offlineUsageReportServiceFactory>;
|
||||
|
||||
export const offlineUsageReportServiceFactory = ({
|
||||
offlineUsageReportDAL,
|
||||
licenseService
|
||||
}: TOfflineUsageReportServiceFactoryDep) => {
|
||||
const signReportContent = (content: string, licenseId: string): string => {
|
||||
const contentHash = crypto.createHash("sha256").update(content).digest("hex");
|
||||
const hmac = crypto.createHmac("sha256", licenseId);
|
||||
hmac.update(contentHash);
|
||||
return hmac.digest("hex");
|
||||
};
|
||||
|
||||
const verifyReportContent = (content: string, signature: string, licenseId: string): boolean => {
|
||||
const expectedSignature = signReportContent(content, licenseId);
|
||||
return signature === expectedSignature;
|
||||
};
|
||||
|
||||
const generateUsageReportCSV = async () => {
|
||||
const cfg = getConfig();
|
||||
if (!cfg.LICENSE_KEY_OFFLINE) {
|
||||
throw new BadRequestError({
|
||||
message: "Offline usage reports are not enabled. LICENSE_KEY_OFFLINE must be configured."
|
||||
});
|
||||
}
|
||||
|
||||
const customerId = licenseService.getCustomerId() as string;
|
||||
const licenseId = licenseService.getLicenseId();
|
||||
|
||||
const [
|
||||
userMetrics,
|
||||
machineIdentityMetrics,
|
||||
projectMetrics,
|
||||
secretMetrics,
|
||||
secretSyncMetrics,
|
||||
dynamicSecretMetrics,
|
||||
secretRotationMetrics
|
||||
] = await Promise.all([
|
||||
offlineUsageReportDAL.getUserMetrics(),
|
||||
offlineUsageReportDAL.getMachineIdentityMetrics(),
|
||||
offlineUsageReportDAL.getProjectMetrics(),
|
||||
offlineUsageReportDAL.getSecretMetrics(),
|
||||
offlineUsageReportDAL.getSecretSyncMetrics(),
|
||||
offlineUsageReportDAL.getDynamicSecretMetrics(),
|
||||
offlineUsageReportDAL.getSecretRotationMetrics()
|
||||
]);
|
||||
|
||||
const headers = [
|
||||
"Total Users",
|
||||
"Admin Users",
|
||||
"Total Identities",
|
||||
"Total Projects",
|
||||
"Total Secrets",
|
||||
"Total Secret Syncs",
|
||||
"Total Dynamic Secrets",
|
||||
"Total Secret Rotations",
|
||||
"Avg Secrets Per Project"
|
||||
];
|
||||
|
||||
const allUserAuthMethods = Object.keys(userMetrics.usersByAuthMethod);
|
||||
allUserAuthMethods.forEach((method) => {
|
||||
headers.push(`Users Auth ${method}`);
|
||||
});
|
||||
|
||||
const allIdentityAuthMethods = Object.keys(machineIdentityMetrics.machineIdentitiesByAuthMethod);
|
||||
allIdentityAuthMethods.forEach((method) => {
|
||||
headers.push(`Identities Auth ${method}`);
|
||||
});
|
||||
|
||||
const allProjectTypes = Object.keys(projectMetrics.projectsByType);
|
||||
allProjectTypes.forEach((type) => {
|
||||
headers.push(`Projects ${type}`);
|
||||
});
|
||||
|
||||
headers.push("Signature");
|
||||
|
||||
const dataRow: (string | number)[] = [
|
||||
userMetrics.totalUsers,
|
||||
userMetrics.adminUsers,
|
||||
machineIdentityMetrics.totalMachineIdentities,
|
||||
projectMetrics.totalProjects,
|
||||
secretMetrics.totalSecrets,
|
||||
secretSyncMetrics.totalSecretSyncs,
|
||||
dynamicSecretMetrics.totalDynamicSecrets,
|
||||
secretRotationMetrics.totalSecretRotations,
|
||||
projectMetrics.averageSecretsPerProject
|
||||
];
|
||||
|
||||
allUserAuthMethods.forEach((method) => {
|
||||
dataRow.push(userMetrics.usersByAuthMethod[method] || 0);
|
||||
});
|
||||
allIdentityAuthMethods.forEach((method) => {
|
||||
dataRow.push(machineIdentityMetrics.machineIdentitiesByAuthMethod[method] || 0);
|
||||
});
|
||||
|
||||
allProjectTypes.forEach((type) => {
|
||||
dataRow.push(projectMetrics.projectsByType[type] || 0);
|
||||
});
|
||||
|
||||
const headersWithoutSignature = headers.slice(0, -1);
|
||||
const contentWithoutSignature = [headersWithoutSignature.join(","), dataRow.join(",")].join("\n");
|
||||
|
||||
const signature = signReportContent(contentWithoutSignature, licenseId);
|
||||
dataRow.push(signature);
|
||||
|
||||
const csvContent = [headers.join(","), dataRow.join(",")].join("\n");
|
||||
|
||||
return {
|
||||
csvContent,
|
||||
signature,
|
||||
filename: `infisical-usage-report-${customerId}-${new Date().toISOString().split("T")[0]}.csv`
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
generateUsageReportCSV,
|
||||
verifyReportSignature: (csvContent: string, signature: string, licenseId: string) =>
|
||||
verifyReportContent(csvContent, signature, licenseId)
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
export interface TUsageMetrics {
|
||||
// User metrics
|
||||
totalUsers: number;
|
||||
usersByAuthMethod: Record<string, number>;
|
||||
adminUsers: number;
|
||||
|
||||
// Machine identity metrics
|
||||
totalMachineIdentities: number;
|
||||
machineIdentitiesByAuthMethod: Record<string, number>;
|
||||
|
||||
// Project metrics
|
||||
totalProjects: number;
|
||||
projectsByType: Record<string, number>;
|
||||
averageSecretsPerProject: number;
|
||||
|
||||
// Secret metrics
|
||||
totalSecrets: number;
|
||||
totalSecretSyncs: number;
|
||||
totalDynamicSecrets: number;
|
||||
totalSecretRotations: number;
|
||||
}
|
||||
|
||||
export interface TUsageReportMetadata {
|
||||
generatedAt: string;
|
||||
instanceId: string;
|
||||
reportVersion: string;
|
||||
}
|
||||
|
||||
export interface TUsageReport {
|
||||
metadata: TUsageReportMetadata;
|
||||
metrics: TUsageMetrics;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface TGenerateUsageReportDTO {
|
||||
includeSignature?: boolean;
|
||||
}
|
||||
|
||||
export interface TVerifyUsageReportDTO {
|
||||
reportData: string;
|
||||
signature: string;
|
||||
}
|
||||
@@ -83,6 +83,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").withSchema(TableName.OrgMembership).as("orgMembershipId"))
|
||||
.select(db.ref("role").withSchema(TableName.OrgMembership).as("orgMembershipRole"))
|
||||
.select(db.ref("roleId").withSchema(TableName.OrgMembership).as("orgMembershipRoleId"))
|
||||
.select(db.ref("status").withSchema(TableName.OrgMembership).as("orgMembershipStatus"))
|
||||
.select(db.ref("name").withSchema(TableName.OrgRoles).as("orgMembershipRoleName"));
|
||||
|
||||
const formattedDocs = sqlNestRelationships({
|
||||
@@ -112,7 +113,8 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
orgMembershipId,
|
||||
orgMembershipRole,
|
||||
orgMembershipRoleName,
|
||||
orgMembershipRoleId
|
||||
orgMembershipRoleId,
|
||||
orgMembershipStatus
|
||||
}) => ({
|
||||
user: {
|
||||
id: userId,
|
||||
@@ -121,6 +123,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
firstName,
|
||||
lastName
|
||||
},
|
||||
status: orgMembershipStatus,
|
||||
membershipId: orgMembershipId,
|
||||
role: orgMembershipRoleName || orgMembershipRole, // custom role name or pre-defined role name
|
||||
roleId: orgMembershipRoleId
|
||||
@@ -488,6 +491,15 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const bulkCreateMemberships = async (data: TOrgMembershipsInsert[], tx?: Knex) => {
|
||||
try {
|
||||
const memberships = await (tx || db)(TableName.OrgMembership).insert(data).returning("*");
|
||||
return memberships;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create org memberships" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const [membership] = await (tx || db)(TableName.OrgMembership).where({ id }).update(data).returning("*");
|
||||
@@ -668,6 +680,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
findMembership,
|
||||
findMembershipWithScimFilter,
|
||||
createMembership,
|
||||
bulkCreateMemberships,
|
||||
updateMembershipById,
|
||||
deleteMembershipById,
|
||||
deleteMembershipsById,
|
||||
|
||||
@@ -528,15 +528,18 @@ export const orgServiceFactory = ({
|
||||
/*
|
||||
* Create organization
|
||||
* */
|
||||
const createOrganization = async ({
|
||||
userId,
|
||||
userEmail,
|
||||
orgName
|
||||
}: {
|
||||
userId: string;
|
||||
orgName: string;
|
||||
userEmail?: string | null;
|
||||
}) => {
|
||||
const createOrganization = async (
|
||||
{
|
||||
userId,
|
||||
userEmail,
|
||||
orgName
|
||||
}: {
|
||||
userId?: string;
|
||||
orgName: string;
|
||||
userEmail?: string | null;
|
||||
},
|
||||
trx?: Knex
|
||||
) => {
|
||||
const { privateKey, publicKey } = await crypto.encryption().asymmetric().generateKeyPair();
|
||||
const key = crypto.randomBytes(32).toString("base64");
|
||||
const {
|
||||
@@ -555,22 +558,25 @@ export const orgServiceFactory = ({
|
||||
} = crypto.encryption().symmetric().encryptWithRootEncryptionKey(key);
|
||||
|
||||
const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail);
|
||||
const organization = await orgDAL.transaction(async (tx) => {
|
||||
|
||||
const createOrg = async (tx: Knex) => {
|
||||
// akhilmhdh: for now this is auto created. in future we can input from user and for previous users just modifiy
|
||||
const org = await orgDAL.create(
|
||||
{ name: orgName, customerId, slug: slugify(`${orgName}-${alphaNumericNanoId(4)}`) },
|
||||
tx
|
||||
);
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId,
|
||||
orgId: org.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (userId) {
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId,
|
||||
orgId: org.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
await orgBotDAL.create(
|
||||
{
|
||||
name: org.name,
|
||||
@@ -590,7 +596,9 @@ export const orgServiceFactory = ({
|
||||
tx
|
||||
);
|
||||
return org;
|
||||
});
|
||||
};
|
||||
|
||||
const organization = await (trx ? createOrg(trx) : orgDAL.transaction(createOrg));
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
return organization;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { createMongoAbility, ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
|
||||
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import {
|
||||
@@ -16,9 +17,11 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCertificateActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSshHostActions,
|
||||
ProjectPermissionSub
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
@@ -1852,9 +1855,52 @@ export const projectServiceFactory = ({
|
||||
if (projectMember) throw new BadRequestError({ message: "User already has access to the project" });
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const filteredProjectMembers = projectMembers
|
||||
|
||||
let filteredProjectMembers = projectMembers
|
||||
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||
.map((el) => el.user.email!);
|
||||
if (filteredProjectMembers.length === 0) {
|
||||
const customRolesWithMemberCreate = await projectRoleDAL.find({ projectId });
|
||||
const customRoleSlugsCanCreate = customRolesWithMemberCreate
|
||||
.filter((role) => {
|
||||
try {
|
||||
const permissions = (
|
||||
typeof role.permissions === "string"
|
||||
? (JSON.parse(role.permissions) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||
: role.permissions
|
||||
) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[];
|
||||
|
||||
const ability = createMongoAbility<MongoAbility<ProjectPermissionSet>>(
|
||||
unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(permissions)
|
||||
);
|
||||
return ability.can(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map((role) => role.slug);
|
||||
|
||||
if (customRoleSlugsCanCreate.length > 0) {
|
||||
const usersWithCustomCreateMemberRole = projectMembers
|
||||
.filter((member) =>
|
||||
member.roles.some((role) => role.customRoleSlug && customRoleSlugsCanCreate.includes(role.customRoleSlug))
|
||||
)
|
||||
.map((el) => el.user.email!)
|
||||
.filter(Boolean);
|
||||
|
||||
if (usersWithCustomCreateMemberRole.length > 0) {
|
||||
filteredProjectMembers = usersWithCustomCreateMemberRole;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredProjectMembers.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"No users in this project have permission to grant you access. Please contact an organization administrator to assign the necessary permissions."
|
||||
});
|
||||
}
|
||||
|
||||
const org = await orgDAL.findOne({ id: permission.orgId });
|
||||
const project = await projectDAL.findById(projectId);
|
||||
const userDetails = await userDAL.findById(permission.id);
|
||||
|
||||
@@ -127,6 +127,27 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByIds = async (ids: string[], tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.SecretImport)
|
||||
.whereIn(`${TableName.SecretImport}.id`, ids)
|
||||
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
|
||||
.select(
|
||||
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
|
||||
db.ref("slug").withSchema(TableName.Environment),
|
||||
db.ref("name").withSchema(TableName.Environment),
|
||||
db.ref("id").withSchema(TableName.Environment).as("envId")
|
||||
);
|
||||
|
||||
return docs.map(({ envId, slug, name, ...el }) => ({
|
||||
...el,
|
||||
importEnv: { id: envId, slug, name }
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find secret imports by ids" });
|
||||
}
|
||||
};
|
||||
|
||||
const getProjectImportCount = async (
|
||||
{ search, ...filter }: Partial<TSecretImports & { projectId: string; search?: string }>,
|
||||
tx?: Knex
|
||||
@@ -325,6 +346,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
|
||||
...secretImportOrm,
|
||||
find,
|
||||
findById,
|
||||
findByIds,
|
||||
findByFolderIds,
|
||||
findLastImportPosition,
|
||||
updateAllPosition,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import RE2 from "re2";
|
||||
|
||||
import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas";
|
||||
import { groupBy, unique } from "@app/lib/fn";
|
||||
|
||||
@@ -54,6 +56,74 @@ type TSecretImportSecretsV2 = {
|
||||
|
||||
const LEVEL_BREAK = 10;
|
||||
const getImportUniqKey = (envSlug: string, path: string) => `${envSlug}=${path}`;
|
||||
const RESERVED_IMPORT_REGEX = new RE2("/__reserve_replication_([a-f0-9-]{36})");
|
||||
|
||||
/**
|
||||
* Processes reserved imports by resolving them to their replication source.
|
||||
*/
|
||||
const processReservedImports = async <
|
||||
T extends {
|
||||
isReserved?: boolean | null;
|
||||
importPath: string;
|
||||
importEnv: { id: string; slug: string; name: string };
|
||||
folderId: string;
|
||||
}
|
||||
>(
|
||||
imports: T[],
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByIds">
|
||||
): Promise<T[]> => {
|
||||
const reservedImportIds: string[] = [];
|
||||
|
||||
imports.forEach((secretImport) => {
|
||||
if (secretImport.isReserved) {
|
||||
const reservedMatch = RESERVED_IMPORT_REGEX.exec(secretImport.importPath);
|
||||
if (reservedMatch) {
|
||||
const referencedImportId = reservedMatch[1];
|
||||
reservedImportIds.push(referencedImportId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (reservedImportIds.length === 0) {
|
||||
return imports;
|
||||
}
|
||||
|
||||
try {
|
||||
const importDetailsMap = new Map<
|
||||
string,
|
||||
{ importPath: string; importEnv: { id: string; slug: string; name: string } }
|
||||
>();
|
||||
|
||||
const referencedImports = await secretImportDAL.findByIds(reservedImportIds);
|
||||
referencedImports.forEach((referencedImport) => {
|
||||
importDetailsMap.set(referencedImport.id, {
|
||||
importPath: referencedImport.importPath,
|
||||
importEnv: referencedImport.importEnv
|
||||
});
|
||||
});
|
||||
|
||||
return imports.map((secretImport) => {
|
||||
if (secretImport.isReserved) {
|
||||
const reservedMatch = RESERVED_IMPORT_REGEX.exec(secretImport.importPath);
|
||||
if (reservedMatch) {
|
||||
const referencedImportId = reservedMatch[1];
|
||||
const referencedDetails = importDetailsMap.get(referencedImportId);
|
||||
|
||||
if (referencedDetails) {
|
||||
return {
|
||||
...secretImport,
|
||||
importPath: referencedDetails.importPath,
|
||||
importEnv: referencedDetails.importEnv
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return secretImport;
|
||||
});
|
||||
} catch (error) {
|
||||
return imports;
|
||||
}
|
||||
};
|
||||
export const fnSecretsFromImports = async ({
|
||||
allowedImports: possibleCyclicImports,
|
||||
folderDAL,
|
||||
@@ -167,7 +237,7 @@ export const fnSecretsV2FromImports = async ({
|
||||
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
|
||||
viewSecretValue: boolean;
|
||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds" | "findByIds">;
|
||||
decryptor: (value?: Buffer | null) => string;
|
||||
expandSecretReferences?: (inputSecret: {
|
||||
value?: string;
|
||||
@@ -188,6 +258,10 @@ export const fnSecretsV2FromImports = async ({
|
||||
})[];
|
||||
}[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }];
|
||||
|
||||
const processedSecretImports = await processReservedImports(rootSecretImports, secretImportDAL);
|
||||
|
||||
stack[0] = { secretImports: processedSecretImports, depth: 0, parentImportedSecrets: [] };
|
||||
|
||||
const processedImports: TSecretImportSecretsV2[] = [];
|
||||
|
||||
while (stack.length) {
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ type TAWSParameterStoreRecord = Record<string, AWS.SSM.Parameter>;
|
||||
type TAWSParameterStoreMetadataRecord = Record<string, AWS.SSM.ParameterMetadata>;
|
||||
type TAWSParameterStoreTagsRecord = Record<string, Record<string, string>>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const MAX_RETRIES = 10;
|
||||
const BATCH_SIZE = 10;
|
||||
|
||||
const getSSM = async (secretSync: TAwsParameterStoreSyncWithCredentials) => {
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ type TAwsSecretsRecord = Record<string, SecretListEntry>;
|
||||
type TAwsSecretValuesRecord = Record<string, SecretValueEntry>;
|
||||
type TAwsSecretDescriptionsRecord = Record<string, DescribeSecretResponse>;
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
const MAX_RETRIES = 10;
|
||||
const BATCH_SIZE = 20;
|
||||
|
||||
const getSecretsManagerClient = async (secretSync: TAwsSecretsManagerSyncWithCredentials) => {
|
||||
|
||||
@@ -81,8 +81,8 @@ type TSecretSyncQueueFactoryDep = {
|
||||
| "deleteMany"
|
||||
| "invalidateSecretCacheByProjectId"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds" | "findByIds">;
|
||||
secretSyncDAL: Pick<TSecretSyncDALFactory, "findById" | "find" | "updateById" | "deleteById" | "update">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: TProjectDALFactory;
|
||||
@@ -106,17 +106,15 @@ type SecretSyncActionJob = Job<
|
||||
TQueueSecretSyncSyncSecretsByIdDTO | TQueueSecretSyncImportSecretsByIdDTO | TQueueSecretSyncRemoveSecretsByIdDTO
|
||||
>;
|
||||
|
||||
const JITTER_MS = 10 * 1000;
|
||||
const REQUEUE_MS = 30 * 1000;
|
||||
const REQUEUE_LIMIT = 30;
|
||||
const CONNECTION_CONCURRENCY_LIMIT = 3;
|
||||
|
||||
const getRequeueDelay = (failureCount?: number) => {
|
||||
if (!failureCount) return 0;
|
||||
|
||||
const baseDelay = 1000;
|
||||
const maxDelay = 30000;
|
||||
|
||||
const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
|
||||
|
||||
const jitter = delay * (0.5 + Math.random() * 0.5);
|
||||
|
||||
return jitter;
|
||||
const jitter = Math.random() * JITTER_MS;
|
||||
if (!failureCount) return jitter;
|
||||
return REQUEUE_MS + jitter;
|
||||
};
|
||||
|
||||
export const secretSyncQueueFactory = ({
|
||||
@@ -196,6 +194,46 @@ export const secretSyncQueueFactory = ({
|
||||
folderCommitService
|
||||
});
|
||||
|
||||
const $isConnectionConcurrencyLimitReached = async (connectionId: string) => {
|
||||
const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId));
|
||||
|
||||
if (!concurrencyCount) return false;
|
||||
|
||||
const count = Number.parseInt(concurrencyCount, 10);
|
||||
|
||||
if (Number.isNaN(count)) return false;
|
||||
|
||||
return count >= CONNECTION_CONCURRENCY_LIMIT;
|
||||
};
|
||||
|
||||
const $incrementConnectionConcurrencyCount = async (connectionId: string) => {
|
||||
const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId));
|
||||
|
||||
const currentCount = Number.parseInt(concurrencyCount || "0", 10);
|
||||
|
||||
const incrementedCount = Number.isNaN(currentCount) ? 1 : currentCount + 1;
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId),
|
||||
(REQUEUE_MS * REQUEUE_LIMIT) / 1000, // in seconds
|
||||
incrementedCount
|
||||
);
|
||||
};
|
||||
|
||||
const $decrementConnectionConcurrencyCount = async (connectionId: string) => {
|
||||
const concurrencyCount = await keyStore.getItem(KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId));
|
||||
|
||||
const currentCount = Number.parseInt(concurrencyCount || "0", 10);
|
||||
|
||||
const decrementedCount = Math.max(0, Number.isNaN(currentCount) ? 0 : currentCount - 1);
|
||||
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.AppConnectionConcurrentJobs(connectionId),
|
||||
(REQUEUE_MS * REQUEUE_LIMIT) / 1000, // in seconds
|
||||
decrementedCount
|
||||
);
|
||||
};
|
||||
|
||||
const $getInfisicalSecrets = async (
|
||||
secretSync: TSecretSyncRaw | TSecretSyncWithCredentials,
|
||||
includeImports = true
|
||||
@@ -420,15 +458,11 @@ export const secretSyncQueueFactory = ({
|
||||
return importedSecretMap;
|
||||
};
|
||||
|
||||
const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO) => {
|
||||
const $handleSyncSecretsJob = async (job: TSecretSyncSyncSecretsDTO, secretSync: TSecretSyncRaw) => {
|
||||
const {
|
||||
data: { syncId, auditLogInfo }
|
||||
} = job;
|
||||
|
||||
const secretSync = await secretSyncDAL.findById(syncId);
|
||||
|
||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
|
||||
await enterpriseSyncCheck(
|
||||
licenseService,
|
||||
secretSync.destination as SecretSync,
|
||||
@@ -571,15 +605,11 @@ export const secretSyncQueueFactory = ({
|
||||
logger.info("SecretSync Sync Job with ID %s Completed", job.id);
|
||||
};
|
||||
|
||||
const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO) => {
|
||||
const $handleImportSecretsJob = async (job: TSecretSyncImportSecretsDTO, secretSync: TSecretSyncRaw) => {
|
||||
const {
|
||||
data: { syncId, auditLogInfo, importBehavior }
|
||||
} = job;
|
||||
|
||||
const secretSync = await secretSyncDAL.findById(syncId);
|
||||
|
||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
|
||||
await secretSyncDAL.updateById(syncId, {
|
||||
importStatus: SecretSyncStatus.Running
|
||||
});
|
||||
@@ -688,15 +718,11 @@ export const secretSyncQueueFactory = ({
|
||||
logger.info("SecretSync Import Job with ID %s Completed", job.id);
|
||||
};
|
||||
|
||||
const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO) => {
|
||||
const $handleRemoveSecretsJob = async (job: TSecretSyncRemoveSecretsDTO, secretSync: TSecretSyncRaw) => {
|
||||
const {
|
||||
data: { syncId, auditLogInfo, deleteSyncOnComplete }
|
||||
} = job;
|
||||
|
||||
const secretSync = await secretSyncDAL.findById(syncId);
|
||||
|
||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
|
||||
await enterpriseSyncCheck(
|
||||
licenseService,
|
||||
secretSync.destination as SecretSync,
|
||||
@@ -900,6 +926,17 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
const secretSyncs = await secretSyncDAL.find({ folderId: folder.id, isAutoSyncEnabled: true });
|
||||
|
||||
await secretSyncDAL.update(
|
||||
{
|
||||
$in: {
|
||||
id: secretSyncs.map((sync) => sync.id)
|
||||
}
|
||||
},
|
||||
{
|
||||
syncStatus: SecretSyncStatus.Pending
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all(secretSyncs.map((secretSync) => queueSecretSyncSyncSecretsById({ syncId: secretSync.id })));
|
||||
};
|
||||
|
||||
@@ -910,7 +947,7 @@ export const secretSyncQueueFactory = ({
|
||||
case QueueJobs.SecretSyncSyncSecrets: {
|
||||
const { failedToAcquireLockCount = 0, ...rest } = job.data as TQueueSecretSyncSyncSecretsByIdDTO;
|
||||
|
||||
if (failedToAcquireLockCount < 10) {
|
||||
if (failedToAcquireLockCount < REQUEUE_LIMIT) {
|
||||
await queueSecretSyncSyncSecretsById({ ...rest, failedToAcquireLockCount: failedToAcquireLockCount + 1 });
|
||||
return;
|
||||
}
|
||||
@@ -980,6 +1017,26 @@ export const secretSyncQueueFactory = ({
|
||||
| TQueueSecretSyncImportSecretsByIdDTO
|
||||
| TQueueSecretSyncRemoveSecretsByIdDTO;
|
||||
|
||||
const secretSync = await secretSyncDAL.findById(syncId);
|
||||
|
||||
if (!secretSync) throw new Error(`Cannot find secret sync with ID ${syncId}`);
|
||||
|
||||
const { connectionId } = secretSync;
|
||||
|
||||
if (job.name === QueueJobs.SecretSyncSyncSecrets) {
|
||||
const isConcurrentLimitReached = await $isConnectionConcurrencyLimitReached(connectionId);
|
||||
|
||||
if (isConcurrentLimitReached) {
|
||||
logger.info(
|
||||
`SecretSync Concurrency limit reached [syncId=${syncId}] [job=${job.name}] [connectionId=${connectionId}]`
|
||||
);
|
||||
|
||||
await $handleAcquireLockFailure(job as SecretSyncActionJob);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>>;
|
||||
|
||||
try {
|
||||
@@ -999,20 +1056,26 @@ export const secretSyncQueueFactory = ({
|
||||
|
||||
try {
|
||||
switch (job.name) {
|
||||
case QueueJobs.SecretSyncSyncSecrets:
|
||||
await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO);
|
||||
case QueueJobs.SecretSyncSyncSecrets: {
|
||||
await $incrementConnectionConcurrencyCount(connectionId);
|
||||
await $handleSyncSecretsJob(job as TSecretSyncSyncSecretsDTO, secretSync);
|
||||
break;
|
||||
}
|
||||
case QueueJobs.SecretSyncImportSecrets:
|
||||
await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO);
|
||||
await $handleImportSecretsJob(job as TSecretSyncImportSecretsDTO, secretSync);
|
||||
break;
|
||||
case QueueJobs.SecretSyncRemoveSecrets:
|
||||
await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO);
|
||||
await $handleRemoveSecretsJob(job as TSecretSyncRemoveSecretsDTO, secretSync);
|
||||
break;
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new Error(`Unhandled Secret Sync Job ${job.name}`);
|
||||
}
|
||||
} finally {
|
||||
if (job.name === QueueJobs.SecretSyncSyncSecrets) {
|
||||
await $decrementConnectionConcurrencyCount(connectionId);
|
||||
}
|
||||
|
||||
await lock.release();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
||||
| "findBySecretPathMultiEnv"
|
||||
| "findSecretPathByFolderIds"
|
||||
>;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds" | "findByIds">;
|
||||
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
|
||||
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
|
||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "create" | "transaction">;
|
||||
|
||||
@@ -86,7 +86,7 @@ type TSecretQueueFactoryDep = {
|
||||
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken">;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
secretDAL: TSecretDALFactory;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds" | "findByIds">;
|
||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||
projectDAL: TProjectDALFactory;
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Heading, Section, Text } from "@react-email/components";
|
||||
import React from "react";
|
||||
|
||||
import { BaseButton } from "./BaseButton";
|
||||
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
||||
import { BaseLink } from "./BaseLink";
|
||||
|
||||
interface OrganizationAssignmentTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||
inviterFirstName?: string;
|
||||
inviterUsername?: string;
|
||||
organizationName: string;
|
||||
callback_url: string;
|
||||
}
|
||||
|
||||
export const OrganizationAssignmentTemplate = ({
|
||||
organizationName,
|
||||
inviterFirstName,
|
||||
inviterUsername,
|
||||
callback_url,
|
||||
siteUrl
|
||||
}: OrganizationAssignmentTemplateProps) => {
|
||||
return (
|
||||
<BaseEmailWrapper
|
||||
title="New Organization"
|
||||
preview="You've been added to a new organization on Infisical."
|
||||
siteUrl={siteUrl}
|
||||
>
|
||||
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
|
||||
You've been added to the organization
|
||||
<br />
|
||||
<strong>{organizationName}</strong> on <strong>Infisical</strong>
|
||||
</Heading>
|
||||
<Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||
<Text className="text-black text-[14px] leading-[24px]">
|
||||
{inviterFirstName && inviterUsername ? (
|
||||
<>
|
||||
<strong>{inviterFirstName}</strong> (
|
||||
<BaseLink href={`mailto:${inviterUsername}`}>{inviterUsername}</BaseLink>) has added you as an
|
||||
organization admin to <strong>{organizationName}</strong>.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
An instance admin has added you as an organization admin to <strong>{organizationName}</strong>.
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
<Section className="text-center">
|
||||
<BaseButton href={callback_url}>View Dashboard</BaseButton>
|
||||
</Section>
|
||||
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
|
||||
<Text className="mb-[0px]">
|
||||
<strong>About Infisical:</strong> Infisical is an all-in-one platform to securely manage application secrets,
|
||||
certificates, SSH keys, and configurations across your team and infrastructure.
|
||||
</Text>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationAssignmentTemplate;
|
||||
|
||||
OrganizationAssignmentTemplate.PreviewProps = {
|
||||
organizationName: "Example Organization",
|
||||
inviterFirstName: "Jane",
|
||||
inviterUsername: "[email protected]",
|
||||
siteUrl: "https://infisical.com",
|
||||
callback_url: "https://app.infisical.com"
|
||||
} as OrganizationAssignmentTemplateProps;
|
||||
@@ -9,6 +9,7 @@ export * from "./IntegrationSyncFailedTemplate";
|
||||
export * from "./NewDeviceLoginTemplate";
|
||||
export * from "./OrgAdminBreakglassAccessTemplate";
|
||||
export * from "./OrgAdminProjectGrantAccessTemplate";
|
||||
export * from "./OrganizationAssignmentTemplate";
|
||||
export * from "./OrganizationInvitationTemplate";
|
||||
export * from "./PasswordResetTemplate";
|
||||
export * from "./PasswordSetupTemplate";
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
NewDeviceLoginTemplate,
|
||||
OrgAdminBreakglassAccessTemplate,
|
||||
OrgAdminProjectGrantAccessTemplate,
|
||||
OrganizationAssignmentTemplate,
|
||||
OrganizationInvitationTemplate,
|
||||
PasswordResetTemplate,
|
||||
PasswordSetupTemplate,
|
||||
@@ -61,6 +62,7 @@ export enum SmtpTemplates {
|
||||
// HistoricalSecretList = "historicalSecretLeakIncident", not used anymore?
|
||||
NewDeviceJoin = "newDevice",
|
||||
OrgInvite = "organizationInvitation",
|
||||
OrgAssignment = "organizationAssignment",
|
||||
ResetPassword = "passwordReset",
|
||||
SetupPassword = "passwordSetup",
|
||||
SecretLeakIncident = "secretLeakIncident",
|
||||
@@ -94,6 +96,7 @@ export enum SmtpHost {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const EmailTemplateMap: Record<SmtpTemplates, React.FC<any>> = {
|
||||
[SmtpTemplates.OrgInvite]: OrganizationInvitationTemplate,
|
||||
[SmtpTemplates.OrgAssignment]: OrganizationAssignmentTemplate,
|
||||
[SmtpTemplates.NewDeviceJoin]: NewDeviceLoginTemplate,
|
||||
[SmtpTemplates.SignupEmailVerification]: SignupEmailVerificationTemplate,
|
||||
[SmtpTemplates.EmailMfa]: EmailMfaTemplate,
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { CronJob } from "cron";
|
||||
|
||||
import { IdentityAuthMethod, OrgMembershipRole, TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||
import {
|
||||
IdentityAuthMethod,
|
||||
OrgMembershipRole,
|
||||
OrgMembershipStatus,
|
||||
TSuperAdmin,
|
||||
TSuperAdminUpdate,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import {
|
||||
@@ -13,7 +20,12 @@ import {
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||
import { TokenType } from "@app/services/auth-token/auth-token-types";
|
||||
import { TIdentityDALFactory } from "@app/services/identity/identity-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||
import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||
@@ -43,7 +55,9 @@ import {
|
||||
TAdminGetUsersDTO,
|
||||
TAdminIntegrationConfig,
|
||||
TAdminSignUpDTO,
|
||||
TGetOrganizationsDTO
|
||||
TCreateOrganizationDTO,
|
||||
TGetOrganizationsDTO,
|
||||
TResendOrgInviteDTO
|
||||
} from "./super-admin-types";
|
||||
|
||||
type TSuperAdminServiceFactoryDep = {
|
||||
@@ -59,11 +73,13 @@ type TSuperAdminServiceFactoryDep = {
|
||||
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
|
||||
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization" | "inviteUserToOrganization">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "updateSubscriptionOrgMemberCount">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
||||
invalidateCacheQueue: TInvalidateCacheQueueFactory;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
tokenService: TAuthTokenServiceFactory;
|
||||
};
|
||||
|
||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||
@@ -123,7 +139,9 @@ export const superAdminServiceFactory = ({
|
||||
identityTokenAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
microsoftTeamsService,
|
||||
invalidateCacheQueue
|
||||
invalidateCacheQueue,
|
||||
smtpService,
|
||||
tokenService
|
||||
}: TSuperAdminServiceFactoryDep) => {
|
||||
const initServerCfg = async () => {
|
||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||
@@ -732,6 +750,159 @@ export const superAdminServiceFactory = ({
|
||||
return organizations;
|
||||
};
|
||||
|
||||
const createOrganization = async (
|
||||
{ name, inviteAdminEmails: emails }: TCreateOrganizationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const inviteAdminEmails = [...new Set(emails)];
|
||||
|
||||
if (!appCfg.isDevelopmentMode && appCfg.isCloud)
|
||||
throw new BadRequestError({ message: "This endpoint is not supported for cloud instances" });
|
||||
|
||||
const serverAdmin = await userDAL.findById(actor.id);
|
||||
const plan = licenseService.onPremFeatures;
|
||||
|
||||
const isEmailInvalid = await isDisposableEmail(inviteAdminEmails);
|
||||
if (isEmailInvalid) {
|
||||
throw new BadRequestError({
|
||||
message: "Disposable emails are not allowed",
|
||||
name: "InviteUser"
|
||||
});
|
||||
}
|
||||
|
||||
const { organization, users: usersToEmail } = await orgDAL.transaction(async (tx) => {
|
||||
const org = await orgService.createOrganization(
|
||||
{
|
||||
orgName: name,
|
||||
userEmail: serverAdmin?.email ?? serverAdmin?.username // identities can be server admins so we can't require this
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
const users: Pick<TUsers, "id" | "firstName" | "lastName" | "email" | "username" | "isAccepted">[] = [];
|
||||
|
||||
for await (const inviteeEmail of inviteAdminEmails) {
|
||||
const usersByUsername = await userDAL.findUserByUsername(inviteeEmail, tx);
|
||||
let inviteeUser =
|
||||
usersByUsername?.length > 1
|
||||
? usersByUsername.find((el) => el.username === inviteeEmail)
|
||||
: usersByUsername?.[0];
|
||||
|
||||
// if the user doesn't exist we create the user with the email
|
||||
if (!inviteeUser) {
|
||||
// TODO(carlos): will be removed once the function receives usernames instead of emails
|
||||
const usersByEmail = await userDAL.findUserByEmail(inviteeEmail, tx);
|
||||
if (usersByEmail?.length === 1) {
|
||||
[inviteeUser] = usersByEmail;
|
||||
} else {
|
||||
inviteeUser = await userDAL.create(
|
||||
{
|
||||
isAccepted: false,
|
||||
email: inviteeEmail,
|
||||
username: inviteeEmail,
|
||||
authMethods: [AuthMethod.EMAIL],
|
||||
isGhost: false
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const inviteeUserId = inviteeUser?.id;
|
||||
const existingEncryptionKey = await userDAL.findUserEncKeyByUserId(inviteeUserId, tx);
|
||||
|
||||
// when user is missing the encrytion keys
|
||||
// this could happen either if user doesn't exist or user didn't find step 3 of generating the encryption keys of srp
|
||||
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key
|
||||
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
|
||||
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncryptionKey)) {
|
||||
await userDAL.createUserEncryption(
|
||||
{
|
||||
userId: inviteeUserId,
|
||||
encryptionVersion: 2
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
|
||||
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
|
||||
throw new BadRequestError({
|
||||
name: "InviteUser",
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
|
||||
await orgDAL.createMembership(
|
||||
{
|
||||
userId: inviteeUser.id,
|
||||
inviteEmail: inviteeEmail,
|
||||
orgId: org.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: inviteeUser.isAccepted ? OrgMembershipStatus.Accepted : OrgMembershipStatus.Invited,
|
||||
isActive: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
users.push(inviteeUser);
|
||||
}
|
||||
|
||||
return { organization: org, users };
|
||||
});
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
await Promise.allSettled(
|
||||
usersToEmail.map(async (user) => {
|
||||
if (!user.email) return;
|
||||
|
||||
if (user.isAccepted) {
|
||||
return smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAssignment,
|
||||
subjectLine: "You've been added to an Infisical organization",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
inviterFirstName: serverAdmin?.firstName,
|
||||
inviterUsername: serverAdmin?.email,
|
||||
organizationName: organization.name,
|
||||
email: user.email,
|
||||
organizationId: organization.id,
|
||||
callback_url: `${appCfg.SITE_URL}/login?org_id=${organization.id}`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// new user, send regular invite
|
||||
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: user.id,
|
||||
orgId: organization.id
|
||||
});
|
||||
|
||||
return smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [user.email],
|
||||
substitutions: {
|
||||
inviterFirstName: serverAdmin?.firstName,
|
||||
inviterUsername: serverAdmin?.email,
|
||||
organizationName: organization.name,
|
||||
email: user.email,
|
||||
organizationId: organization.id,
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
return organization;
|
||||
};
|
||||
|
||||
const deleteOrganization = async (organizationId: string) => {
|
||||
const organization = await orgDAL.deleteById(organizationId);
|
||||
return organization;
|
||||
@@ -763,6 +934,86 @@ export const superAdminServiceFactory = ({
|
||||
return organizationMembership;
|
||||
};
|
||||
|
||||
const joinOrganization = async (orgId: string, actor: OrgServiceActor) => {
|
||||
const serverAdmin = await userDAL.findById(actor.id);
|
||||
|
||||
if (!serverAdmin) {
|
||||
throw new NotFoundError({ message: "Could not find server admin user" });
|
||||
}
|
||||
|
||||
const org = await orgDAL.findById(orgId);
|
||||
|
||||
if (!org) {
|
||||
throw new NotFoundError({ message: `Could not organization with ID "${orgId}"` });
|
||||
}
|
||||
|
||||
const existingOrgMembership = await orgMembershipDAL.findOne({ userId: serverAdmin.id, orgId });
|
||||
|
||||
if (existingOrgMembership) {
|
||||
throw new BadRequestError({ message: `You are already a part of the organization with ID ${orgId}` });
|
||||
}
|
||||
|
||||
const orgMembership = await orgDAL.createMembership({
|
||||
userId: serverAdmin.id,
|
||||
orgId: org.id,
|
||||
role: OrgMembershipRole.Admin,
|
||||
status: OrgMembershipStatus.Accepted,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
return orgMembership;
|
||||
};
|
||||
|
||||
const resendOrgInvite = async ({ organizationId, membershipId }: TResendOrgInviteDTO, actor: OrgServiceActor) => {
|
||||
const orgMembership = await orgMembershipDAL.findOne({ id: membershipId, orgId: organizationId });
|
||||
|
||||
if (!orgMembership) {
|
||||
throw new NotFoundError({ name: "Organization Membership", message: "Organization membership not found" });
|
||||
}
|
||||
|
||||
if (orgMembership.status === OrgMembershipStatus.Accepted) {
|
||||
throw new BadRequestError({
|
||||
message: "This user has already accepted their invitation."
|
||||
});
|
||||
}
|
||||
|
||||
if (!orgMembership.userId) {
|
||||
throw new NotFoundError({ message: "Cannot find user associated with Org Membership." });
|
||||
}
|
||||
|
||||
if (!orgMembership.inviteEmail) {
|
||||
throw new BadRequestError({ message: "No invite email associated with user." });
|
||||
}
|
||||
|
||||
const org = await orgDAL.findOrgById(orgMembership.orgId);
|
||||
|
||||
const appCfg = getConfig();
|
||||
const serverAdmin = await userDAL.findById(actor.id);
|
||||
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||
userId: orgMembership.userId,
|
||||
orgId: orgMembership.orgId
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgInvite,
|
||||
subjectLine: "Infisical organization invitation",
|
||||
recipients: [orgMembership.inviteEmail],
|
||||
substitutions: {
|
||||
inviterFirstName: serverAdmin?.firstName,
|
||||
inviterUsername: serverAdmin?.email,
|
||||
organizationName: org?.name,
|
||||
email: orgMembership.inviteEmail,
|
||||
organizationId: orgMembership.orgId,
|
||||
token,
|
||||
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||
}
|
||||
});
|
||||
|
||||
return orgMembership;
|
||||
};
|
||||
|
||||
const getIdentities = async ({ offset, limit, searchTerm }: TAdminGetIdentitiesDTO) => {
|
||||
const identities = await identityDAL.getIdentitiesByFilter({
|
||||
limit,
|
||||
@@ -901,6 +1152,9 @@ export const superAdminServiceFactory = ({
|
||||
initializeEnvConfigSync,
|
||||
getEnvOverrides,
|
||||
getEnvOverridesOrganized,
|
||||
deleteUsers
|
||||
deleteUsers,
|
||||
createOrganization,
|
||||
joinOrganization,
|
||||
resendOrgInvite
|
||||
};
|
||||
};
|
||||
|
||||
@@ -34,6 +34,16 @@ export type TGetOrganizationsDTO = {
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export type TCreateOrganizationDTO = {
|
||||
name: string;
|
||||
inviteAdminEmails: string[];
|
||||
};
|
||||
|
||||
export type TResendOrgInviteDTO = {
|
||||
organizationId: string;
|
||||
membershipId: string;
|
||||
};
|
||||
|
||||
export enum LoginMethod {
|
||||
EMAIL = "email",
|
||||
GOOGLE = "google",
|
||||
|
||||
Reference in New Issue
Block a user