Merge branch 'main' into ENG-3160-2

This commit is contained in:
x032205
2025-09-12 18:20:38 -04:00
174 changed files with 6807 additions and 654 deletions
@@ -4,6 +4,10 @@ import {
AzureProviderListItemSchema,
SanitizedAzureProviderSchema
} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas";
import {
CriblProviderListItemSchema,
SanitizedCriblProviderSchema
} from "@app/ee/services/audit-log-stream/cribl/cribl-provider-schemas";
import {
CustomProviderListItemSchema,
SanitizedCustomProviderSchema
@@ -24,14 +28,16 @@ const SanitizedAuditLogStreamSchema = z.union([
SanitizedCustomProviderSchema,
SanitizedDatadogProviderSchema,
SanitizedSplunkProviderSchema,
SanitizedAzureProviderSchema
SanitizedAzureProviderSchema,
SanitizedCriblProviderSchema
]);
const ProviderOptionsSchema = z.discriminatedUnion("provider", [
CustomProviderListItemSchema,
DatadogProviderListItemSchema,
SplunkProviderListItemSchema,
AzureProviderListItemSchema
AzureProviderListItemSchema,
CriblProviderListItemSchema
]);
export const registerAuditLogStreamRouter = async (server: FastifyZodProvider) => {
@@ -4,6 +4,11 @@ import {
SanitizedAzureProviderSchema,
UpdateAzureProviderLogStreamSchema
} from "@app/ee/services/audit-log-stream/azure/azure-provider-schemas";
import {
CreateCriblProviderLogStreamSchema,
SanitizedCriblProviderSchema,
UpdateCriblProviderLogStreamSchema
} from "@app/ee/services/audit-log-stream/cribl/cribl-provider-schemas";
import {
CreateCustomProviderLogStreamSchema,
SanitizedCustomProviderSchema,
@@ -61,5 +66,14 @@ export const AUDIT_LOG_STREAM_REGISTER_ROUTER_MAP: Record<LogProvider, (server:
createSchema: CreateSplunkProviderLogStreamSchema,
updateSchema: UpdateSplunkProviderLogStreamSchema
});
},
[LogProvider.Cribl]: async (server: FastifyZodProvider) => {
registerAuditLogStreamEndpoints({
server,
provider: LogProvider.Cribl,
sanitizedResponseSchema: SanitizedCriblProviderSchema,
createSchema: CreateCriblProviderLogStreamSchema,
updateSchema: UpdateCriblProviderLogStreamSchema
});
}
};
@@ -84,7 +84,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
dynamicSecret: SanitizedDynamicSecretSchema
dynamicSecret: SanitizedDynamicSecretSchema.extend({
inputs: z.unknown()
})
})
}
},
@@ -151,7 +153,9 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
}),
response: {
200: z.object({
dynamicSecret: SanitizedDynamicSecretSchema
dynamicSecret: SanitizedDynamicSecretSchema.extend({
inputs: z.unknown()
})
})
}
},
+2
View File
@@ -24,6 +24,7 @@ import { registerPITRouter } from "./pit-router";
import { registerProjectRoleRouter } from "./project-role-router";
import { registerProjectRouter } from "./project-router";
import { registerRateLimitRouter } from "./rate-limit-router";
import { registerRelayRouter } from "./relay-router";
import { registerSamlRouter } from "./saml-router";
import { registerScimRouter } from "./scim-router";
import { registerSecretApprovalPolicyRouter } from "./secret-approval-policy-router";
@@ -79,6 +80,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
);
await server.register(registerGatewayRouter, { prefix: "/gateways" });
await server.register(registerRelayRouter, { prefix: "/relays" });
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
await server.register(
+8 -1
View File
@@ -43,6 +43,12 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({ organizationId: z.string().trim() }),
querystring: z.object({
refreshCache: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
response: {
200: z.object({ plan: z.any() })
}
@@ -54,7 +60,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => {
actor: req.permission.type,
actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod,
orgId: req.params.organizationId
orgId: req.params.organizationId,
refreshCache: req.query.refreshCache
});
return { plan };
}
+103
View File
@@ -0,0 +1,103 @@
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerRelayRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig();
server.route({
method: "POST",
url: "/register-instance-relay",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
host: z.string(),
name: slugSchema({ min: 1, max: 32, field: "name" })
}),
response: {
200: z.object({
pki: z.object({
serverCertificate: z.string(),
serverPrivateKey: z.string(),
clientCertificateChain: z.string()
}),
ssh: z.object({
serverCertificate: z.string(),
serverPrivateKey: z.string(),
clientCAPublicKey: z.string()
})
})
}
},
onRequest: (req, _, next) => {
const authHeader = req.headers.authorization;
if (appCfg.RELAY_AUTH_SECRET && authHeader) {
const expectedHeader = `Bearer ${appCfg.RELAY_AUTH_SECRET}`;
if (
authHeader.length === expectedHeader.length &&
crypto.nativeCrypto.timingSafeEqual(Buffer.from(authHeader), Buffer.from(expectedHeader))
) {
return next();
}
}
throw new UnauthorizedError({
message: "Invalid relay auth secret"
});
},
handler: async (req) => {
return server.services.relay.registerRelay({
...req.body
});
}
});
server.route({
method: "POST",
url: "/register-org-relay",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
host: z.string(),
name: slugSchema({ min: 1, max: 32, field: "name" })
}),
response: {
200: z.object({
pki: z.object({
serverCertificate: z.string(),
serverPrivateKey: z.string(),
clientCertificateChain: z.string()
}),
ssh: z.object({
serverCertificate: z.string(),
serverPrivateKey: z.string(),
clientCAPublicKey: z.string()
})
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
throw new BadRequestError({
message: "Org relay registration is not yet supported"
});
return server.services.relay.registerRelay({
...req.body,
identityId: req.permission.id,
orgId: req.permission.orgId
});
}
});
};
+133
View File
@@ -0,0 +1,133 @@
import z from "zod";
import { GatewaysV2Schema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const SanitizedGatewayV2Schema = GatewaysV2Schema.pick({
id: true,
identityId: true,
name: true,
createdAt: true,
updatedAt: true,
heartbeat: true
});
export const registerGatewayV2Router = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
schema: {
body: z.object({
relayName: slugSchema({ min: 1, max: 32, field: "relayName" }),
name: slugSchema({ min: 1, max: 32, field: "name" })
}),
response: {
200: z.object({
gatewayId: z.string(),
relayHost: z.string(),
pki: z.object({
serverCertificate: z.string(),
serverPrivateKey: z.string(),
clientCertificateChain: z.string()
}),
ssh: z.object({
clientCertificate: z.string(),
clientPrivateKey: z.string(),
serverCAPublicKey: z.string()
})
})
}
},
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const gateway = await server.services.gatewayV2.registerGateway({
orgId: req.permission.orgId,
relayName: req.body.relayName,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
name: req.body.name
});
return gateway;
}
});
server.route({
method: "POST",
url: "/heartbeat",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
await server.services.gatewayV2.heartbeat({
orgPermission: req.permission
});
return { message: "Successfully triggered heartbeat" };
}
});
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: SanitizedGatewayV2Schema.extend({
identity: z.object({
name: z.string(),
id: z.string()
})
}).array()
}
},
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const gateways = await server.services.gatewayV2.listGateways({
orgPermission: req.permission
});
return gateways;
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: SanitizedGatewayV2Schema
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateway = await server.services.gatewayV2.deleteGatewayById({
orgPermission: req.permission,
id: req.params.id
});
return gateway;
}
});
};
+3
View File
@@ -7,6 +7,7 @@ import {
SECRET_SCANNING_REGISTER_ROUTER_MAP
} from "@app/ee/routes/v2/secret-scanning-v2-routers";
import { registerGatewayV2Router } from "./gateway-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerProjectRoleRouter } from "./project-role-router";
@@ -23,6 +24,8 @@ export const registerV2EERoutes = async (server: FastifyZodProvider) => {
prefix: "/identity-project-additional-privilege"
});
await server.register(registerGatewayV2Router, { prefix: "/gateways" });
await server.register(
async (secretRotationV2Router) => {
// register generic secret rotation endpoints
@@ -1,6 +1,7 @@
export enum LogProvider {
Azure = "azure",
Cribl = "cribl",
Custom = "custom",
Datadog = "datadog",
Splunk = "splunk",
Custom = "custom"
Splunk = "splunk"
}
@@ -1,6 +1,7 @@
import { LogProvider } from "./audit-log-stream-enums";
import { TAuditLogStreamCredentials, TLogStreamFactory } from "./audit-log-stream-types";
import { AzureProviderFactory } from "./azure/azure-provider-factory";
import { CriblProviderFactory } from "./cribl/cribl-provider-factory";
import { CustomProviderFactory } from "./custom/custom-provider-factory";
import { DatadogProviderFactory } from "./datadog/datadog-provider-factory";
import { SplunkProviderFactory } from "./splunk/splunk-provider-factory";
@@ -11,5 +12,6 @@ export const LOG_STREAM_FACTORY_MAP: Record<LogProvider, TLogStreamFactoryImplem
[LogProvider.Azure]: AzureProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Datadog]: DatadogProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Splunk]: SplunkProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation
[LogProvider.Custom]: CustomProviderFactory as TLogStreamFactoryImplementation,
[LogProvider.Cribl]: CriblProviderFactory as TLogStreamFactoryImplementation
};
@@ -4,6 +4,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TAuditLogStream, TAuditLogStreamCredentials } from "./audit-log-stream-types";
import { getAzureProviderListItem } from "./azure/azure-provider-fns";
import { getCriblProviderListItem } from "./cribl/cribl-provider-fns";
import { getCustomProviderListItem } from "./custom/custom-provider-fns";
import { getDatadogProviderListItem } from "./datadog/datadog-provider-fns";
import { getSplunkProviderListItem } from "./splunk/splunk-provider-fns";
@@ -13,7 +14,8 @@ export const listProviderOptions = () => {
getDatadogProviderListItem(),
getSplunkProviderListItem(),
getCustomProviderListItem(),
getAzureProviderListItem()
getAzureProviderListItem(),
getCriblProviderListItem()
].sort((a, b) => a.name.localeCompare(b.name));
};
@@ -2,17 +2,19 @@ import { TAuditLogs } from "@app/db/schemas";
import { LogProvider } from "./audit-log-stream-enums";
import { TAzureProvider, TAzureProviderCredentials } from "./azure/azure-provider-types";
import { TCriblProvider, TCriblProviderCredentials } from "./cribl/cribl-provider-types";
import { TCustomProvider, TCustomProviderCredentials } from "./custom/custom-provider-types";
import { TDatadogProvider, TDatadogProviderCredentials } from "./datadog/datadog-provider-types";
import { TSplunkProvider, TSplunkProviderCredentials } from "./splunk/splunk-provider-types";
export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider | TAzureProvider;
export type TAuditLogStream = TDatadogProvider | TSplunkProvider | TCustomProvider | TAzureProvider | TCriblProvider;
export type TAuditLogStreamCredentials =
| TDatadogProviderCredentials
| TSplunkProviderCredentials
| TCustomProviderCredentials
| TAzureProviderCredentials;
| TAzureProviderCredentials
| TCriblProviderCredentials;
export type TCreateAuditLogStreamDTO = {
provider: LogProvider;
@@ -0,0 +1,58 @@
import { RawAxiosRequestHeaders } from "axios";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AUDIT_LOG_STREAM_TIMEOUT } from "../../audit-log/audit-log-queue";
import { TLogStreamFactoryStreamLog, TLogStreamFactoryValidateCredentials } from "../audit-log-stream-types";
import { TCriblProviderCredentials } from "./cribl-provider-types";
export const CriblProviderFactory = () => {
const validateCredentials: TLogStreamFactoryValidateCredentials<TCriblProviderCredentials> = async ({
credentials
}) => {
const { url, token } = credentials;
await blockLocalAndPrivateIpAddresses(url);
const streamHeaders: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
};
await request
.post(url, JSON.stringify({ ping: "ok" }), {
headers: streamHeaders,
timeout: AUDIT_LOG_STREAM_TIMEOUT,
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
})
.catch((err) => {
throw new BadRequestError({ message: `Failed to connect with Cribl: ${(err as Error)?.message}` });
});
return credentials;
};
const streamLog: TLogStreamFactoryStreamLog<TCriblProviderCredentials> = async ({ credentials, auditLog }) => {
const { url, token } = credentials;
await blockLocalAndPrivateIpAddresses(url);
const streamHeaders: RawAxiosRequestHeaders = {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
};
await request.post(url, JSON.stringify(auditLog), {
headers: streamHeaders,
timeout: AUDIT_LOG_STREAM_TIMEOUT,
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
});
};
return {
validateCredentials,
streamLog
};
};
@@ -0,0 +1,8 @@
import { LogProvider } from "../audit-log-stream-enums";
export const getCriblProviderListItem = () => {
return {
name: "Cribl" as const,
provider: LogProvider.Cribl as const
};
};
@@ -0,0 +1,34 @@
import { z } from "zod";
import { LogProvider } from "../audit-log-stream-enums";
import { BaseProviderSchema } from "../audit-log-stream-schemas";
export const CriblProviderCredentialsSchema = z.object({
url: z.string().url().trim().min(1).max(255),
token: z.string().trim().min(21).max(255)
});
const BaseCriblProviderSchema = BaseProviderSchema.extend({ provider: z.literal(LogProvider.Cribl) });
export const CriblProviderSchema = BaseCriblProviderSchema.extend({
credentials: CriblProviderCredentialsSchema
});
export const SanitizedCriblProviderSchema = BaseCriblProviderSchema.extend({
credentials: CriblProviderCredentialsSchema.pick({
url: true
})
});
export const CriblProviderListItemSchema = z.object({
name: z.literal("Cribl"),
provider: z.literal(LogProvider.Cribl)
});
export const CreateCriblProviderLogStreamSchema = z.object({
credentials: CriblProviderCredentialsSchema
});
export const UpdateCriblProviderLogStreamSchema = z.object({
credentials: CriblProviderCredentialsSchema
});
@@ -0,0 +1,7 @@
import { z } from "zod";
import { CriblProviderCredentialsSchema, CriblProviderSchema } from "./cribl-provider-schemas";
export type TCriblProvider = z.infer<typeof CriblProviderSchema>;
export type TCriblProviderCredentials = z.infer<typeof CriblProviderCredentialsSchema>;
@@ -46,7 +46,10 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first();
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
.count("*")
.where({ dynamicSecretId })
.first();
return parseInt(doc || "0", 10);
} catch (error) {
throw new DatabaseError({ error, name: "DynamicSecretCountLeases" });
@@ -55,7 +58,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db)(TableName.DynamicSecretLease)
const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease)
.where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id })
.first()
.join(
@@ -19,6 +19,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { TGatewayV2DALFactory } from "../gateway-v2/gateway-v2-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { DynamicSecretStatus, TDynamicSecretServiceFactory } from "./dynamic-secret-types";
@@ -39,6 +40,7 @@ type TDynamicSecretServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
gatewayV2DAL: Pick<TGatewayV2DALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
};
@@ -53,6 +55,7 @@ export const dynamicSecretServiceFactory = ({
projectDAL,
kmsService,
gatewayDAL,
gatewayV2DAL,
resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep): TDynamicSecretServiceFactory => {
const create: TDynamicSecretServiceFactory["create"] = async ({
@@ -70,6 +73,7 @@ export const dynamicSecretServiceFactory = ({
metadata,
usernameTemplate
}) => {
let isGatewayV1 = true;
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
@@ -118,17 +122,22 @@ export const dynamicSecretServiceFactory = ({
const gatewayId = inputs.gatewayId as string;
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway) {
if (!gateway && !gatewayv2) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
if (!gateway) {
isGatewayV1 = false;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
gateway?.orgId ?? gatewayv2?.orgId,
actorAuthMethod,
actorOrgId
);
@@ -138,7 +147,7 @@ export const dynamicSecretServiceFactory = ({
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
selectedGatewayId = gateway?.id ?? gatewayv2?.id;
}
const isConnected = await selectedProvider.validateConnection(provider.inputs, { projectId });
@@ -159,7 +168,8 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
folderId: folder.id,
name,
gatewayId: selectedGatewayId,
gatewayId: isGatewayV1 ? selectedGatewayId : undefined,
gatewayV2Id: isGatewayV1 ? undefined : selectedGatewayId,
usernameTemplate
},
tx
@@ -180,7 +190,7 @@ export const dynamicSecretServiceFactory = ({
return cfg;
});
return dynamicSecretCfg;
return { ...dynamicSecretCfg, inputs };
};
const updateByName: TDynamicSecretServiceFactory["updateByName"] = async ({
@@ -270,20 +280,27 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput, { projectId });
let selectedGatewayId: string | null = null;
let isGatewayV1 = true;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
const gatewayId = updatedInput.gatewayId as string;
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway) {
const [gatewayv2] = await gatewayV2DAL.find({ id: gatewayId, orgId: actorOrgId });
if (!gateway && !gatewayv2) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
if (!gateway) {
isGatewayV1 = false;
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
@@ -293,7 +310,7 @@ export const dynamicSecretServiceFactory = ({
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
selectedGatewayId = gateway?.id ?? gatewayv2?.id;
}
const isConnected = await selectedProvider.validateConnection(newInput, { projectId });
@@ -309,7 +326,8 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
name: newName ?? name,
status: null,
gatewayId: selectedGatewayId,
gatewayId: isGatewayV1 ? selectedGatewayId : null,
gatewayV2Id: isGatewayV1 ? null : selectedGatewayId,
usernameTemplate
},
tx
@@ -337,7 +355,7 @@ export const dynamicSecretServiceFactory = ({
return cfg;
});
return updatedDynamicCfg;
return { ...updatedDynamicCfg, inputs: updatedInput };
};
const deleteByName: TDynamicSecretServiceFactory["deleteByName"] = async ({
@@ -1,6 +1,7 @@
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
@@ -24,12 +25,14 @@ import { VerticaProvider } from "./vertica";
type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
};
export const buildDynamicSecretProviders = ({
gatewayService
gatewayService,
gatewayV2Service
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService, gatewayV2Service }),
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
@@ -44,7 +47,7 @@ export const buildDynamicSecretProviders = ({
[DynamicSecretProviders.Snowflake]: SnowflakeProvider(),
[DynamicSecretProviders.Totp]: TotpProvider(),
[DynamicSecretProviders.SapAse]: SapAseProvider(),
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService }),
[DynamicSecretProviders.Kubernetes]: KubernetesProvider({ gatewayService, gatewayV2Service }),
[DynamicSecretProviders.Vertica]: VerticaProvider({ gatewayService }),
[DynamicSecretProviders.GcpIam]: GcpIamProvider(),
[DynamicSecretProviders.Github]: GithubProvider(),
@@ -5,12 +5,14 @@ import https from "https";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
import {
DynamicSecretKubernetesSchema,
KubernetesAuthMethod,
@@ -26,6 +28,7 @@ const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local";
type TKubernetesProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
};
const generateUsername = (usernameTemplate?: string | null) => {
@@ -38,7 +41,10 @@ const generateUsername = (usernameTemplate?: string | null) => {
});
};
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
export const KubernetesProvider = ({
gatewayService,
gatewayV2Service
}: TKubernetesProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
if (!providerInputs.gatewayId && providerInputs.url) {
@@ -58,6 +64,32 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
},
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
): Promise<T> => {
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId: inputs.gatewayId,
targetHost: inputs.targetHost,
targetPort: inputs.targetPort
});
if (gatewayV2ConnectionDetails) {
const callbackResult = await withGatewayV2Proxy(
async (port) => {
return gatewayCallback(
inputs.reviewTokenThroughGateway ? "http://localhost" : "https://localhost",
port,
inputs.httpsAgent
);
},
{
relayHost: gatewayV2ConnectionDetails.relayHost,
gateway: gatewayV2ConnectionDetails.gateway,
relay: gatewayV2ConnectionDetails.relay,
protocol: inputs.reviewTokenThroughGateway ? GatewayProxyProtocol.Http : GatewayProxyProtocol.Tcp,
httpsAgent: inputs.httpsAgent
}
);
return callbackResult;
}
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
@@ -353,8 +385,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
return true;
} catch (error) {
let errorMessage = error instanceof Error ? error.message : "Unknown error";
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
errorMessage = (error.response?.data as { message: string }).message;
if (axios.isAxiosError(error)) {
if (error.response) {
let { message } = error?.response?.data as unknown as { message?: string };
if (!message && typeof error.response.data === "string") {
message = error.response.data;
}
if (message) {
errorMessage = message;
}
}
}
const sanitizedErrorMessage = sanitizeString({
@@ -603,8 +645,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
};
} catch (error) {
let errorMessage = error instanceof Error ? error.message : "Unknown error";
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
errorMessage = (error.response?.data as { message: string }).message;
if (axios.isAxiosError(error)) {
if (error.response) {
let { message } = error?.response?.data as unknown as { message?: string };
if (!message && typeof error.response.data === "string") {
message = error.response.data;
}
if (message) {
errorMessage = message;
}
}
}
const sanitizedErrorMessage = sanitizeString({
@@ -740,8 +792,18 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
}
} catch (error) {
let errorMessage = error instanceof Error ? error.message : "Unknown error";
if (axios.isAxiosError(error) && (error.response?.data as { message: string })?.message) {
errorMessage = (error.response?.data as { message: string }).message;
if (axios.isAxiosError(error)) {
if (error.response) {
let { message } = error?.response?.data as unknown as { message?: string };
if (!message && typeof error.response.data === "string") {
message = error.response.data;
}
if (message) {
errorMessage = message;
}
}
}
const sanitizedErrorMessage = sanitizeString({
@@ -165,6 +165,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional(),
sslEnabled: z.boolean().optional(),
gatewayId: z.string().nullable().optional()
});
@@ -275,11 +276,11 @@ export const DynamicSecretMongoAtlasSchema = z.object({
export const DynamicSecretMongoDBSchema = z.object({
host: z.string().min(1).trim().toLowerCase(),
port: z.number().optional(),
port: z.number().optional().nullable(),
username: z.string().min(1).trim(),
password: z.string().min(1).trim(),
database: z.string().min(1).trim(),
ca: z.string().min(1).optional(),
ca: z.string().trim().optional().nullable(),
roles: z
.string()
.array()
@@ -44,7 +44,7 @@ export const MongoDBProvider = (): TDynamicProviderFns => {
password: providerInputs.password
},
directConnection: !isSrv,
ca: providerInputs.ca
ca: providerInputs.ca || undefined
});
return client;
};
@@ -1,15 +1,18 @@
import handlebars from "handlebars";
import knex from "knex";
import RE2 from "re2";
import { z } from "zod";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors";
import { sanitizeString } from "@app/lib/fn";
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { TGatewayV2ServiceFactory } from "../../gateway-v2/gateway-v2-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, PasswordRequirements, SqlProviders, TDynamicProviderFns } from "./models";
import { compileUsernameTemplate } from "./templateUtils";
@@ -128,9 +131,13 @@ const generateUsername = (provider: SqlProviders, usernameTemplate?: string | nu
type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
};
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
export const SqlDatabaseProvider = ({
gatewayService,
gatewayV2Service
}: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
@@ -150,19 +157,40 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
return { ...providerInputs, hostIp };
};
const $getClient = async (providerInputs: z.infer<typeof DynamicSecretSqlDBSchema> & { hostIp: string }) => {
const $getClient = async (
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema> & { hostIp: string; originalHost: string }
) => {
const ssl = providerInputs.ca
? { rejectUnauthorized: false, ca: providerInputs.ca, servername: providerInputs.host }
: undefined;
const isMsSQLClient = providerInputs.client === SqlProviders.MsSQL;
/*
We route through the gateway by setting connection.host = "localhost".
Azure SQL identifies the logical server from the TDS login name when the host
isn’t the Azure FQDN. Therefore, when using the gateway, ensure username is
"user@<azure-server-name>" so Azure opens the correct logical server.
Direct connections to the Azure FQDN usually don’t require this suffix.
*/
const isAzureSql = isMsSQLClient && new RE2(/\.database\.windows\.net$/i).test(providerInputs.originalHost);
const azureServerLabel =
isAzureSql && providerInputs.gatewayId ? providerInputs.originalHost?.split(".")[0] : undefined;
const effectiveUser =
isAzureSql && !providerInputs.username.includes("@") && azureServerLabel
? `${providerInputs.username}@${azureServerLabel}`
: providerInputs.username;
const db = knex({
client: providerInputs.client,
connection: {
database: providerInputs.database,
port: providerInputs.port,
host: providerInputs.client === SqlProviders.Postgres ? providerInputs.hostIp : providerInputs.host,
user: providerInputs.username,
host:
providerInputs.client === SqlProviders.Postgres && !providerInputs.gatewayId
? providerInputs.hostIp
: providerInputs.host,
user: effectiveUser,
password: providerInputs.password,
ssl,
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
@@ -170,6 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
options: isMsSQLClient
? {
...(providerInputs.sslEnabled !== undefined ? { encrypt: providerInputs.sslEnabled } : {}),
trustServerCertificate: !providerInputs.ca,
cryptoCredentialsDetails: providerInputs.ca ? { ca: providerInputs.ca } : {}
}
@@ -185,6 +214,26 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void>
) => {
const gatewayV2ConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId: providerInputs.gatewayId as string,
targetHost: providerInputs.host,
targetPort: providerInputs.port
});
if (gatewayV2ConnectionDetails) {
return withGatewayV2Proxy(
async (port) => {
await gatewayCallback("localhost", port);
},
{
relayHost: gatewayV2ConnectionDetails.relayHost,
gateway: gatewayV2ConnectionDetails.gateway,
relay: gatewayV2ConnectionDetails.relay,
protocol: GatewayProxyProtocol.Tcp
}
);
}
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy(
@@ -212,7 +261,13 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const providerInputs = await validateProviderInputs(inputs);
let isConnected = false;
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host, hostIp: providerInputs.hostIp });
const db = await $getClient({
...providerInputs,
port,
host,
hostIp: providerInputs.hostIp,
originalHost: providerInputs.host
});
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
@@ -253,7 +308,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const password = generatePassword(providerInputs.client, providerInputs.passwordRequirements);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
const db = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
try {
const expiration = new Date(expireAt).toISOString();
@@ -296,7 +356,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const username = entityId;
const { database } = providerInputs;
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
const db = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
const queries = revokeStatement.toString().split(";").filter(Boolean);
@@ -331,7 +396,12 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
if (!providerInputs.renewStatement) return { entityId };
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
const db = await $getClient({
...providerInputs,
port,
host,
originalHost: providerInputs.host
});
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
@@ -0,0 +1,2 @@
export const GATEWAY_ROUTING_INFO_OID = "1.3.6.1.4.1.12345.100.1";
export const GATEWAY_ACTOR_OID = "1.3.6.1.4.1.12345.100.2";
@@ -0,0 +1,60 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { GatewaysV2Schema, TableName, TGatewaysV2 } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
export type TGatewayV2DALFactory = ReturnType<typeof gatewayV2DalFactory>;
export const gatewayV2DalFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.GatewayV2);
const find = async (filter: TFindFilter<TGatewaysV2>, { offset, limit, sort, tx }: TFindOpt<TGatewaysV2> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.GatewayV2)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter, TableName.GatewayV2))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.GatewayV2}.identityId`)
.join(
TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.GatewayV2}.identityId`
)
.select(selectAllTableCols(TableName.GatewayV2))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
return docs.map((el) => ({
...GatewaysV2Schema.parse(el),
identity: { id: el.identityId, name: el.identityName }
}));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.GatewayV2}: Find` });
}
};
const findById = async (id: string, tx?: Knex) => {
try {
const doc = await (tx || db.replicaNode())(TableName.GatewayV2)
.join(TableName.Organization, `${TableName.GatewayV2}.orgId`, `${TableName.Organization}.id`)
.where(`${TableName.GatewayV2}.id`, id)
.select(selectAllTableCols(TableName.GatewayV2))
.select(db.ref("name").withSchema(TableName.Organization).as("orgName"))
.first();
return doc;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.GatewayV2}: Find by id` });
}
};
return { ...orm, find, findById };
};
@@ -0,0 +1,656 @@
import net from "node:net";
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { TRelays } from "@app/db/schemas";
import { PgSqlLock } from "@app/keystore/keystore";
import { crypto } from "@app/lib/crypto";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { GatewayProxyProtocol } from "@app/lib/gateway/types";
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { OrgServiceActor } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
import {
createSerialNumber,
keyAlgorithmToAlgCfg
} from "@app/services/certificate-authority/certificate-authority-fns";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TRelayDALFactory } from "../relay/relay-dal";
import { TRelayServiceFactory } from "../relay/relay-service";
import { GATEWAY_ACTOR_OID, GATEWAY_ROUTING_INFO_OID } from "./gateway-v2-constants";
import { TGatewayV2DALFactory } from "./gateway-v2-dal";
import { TOrgGatewayConfigV2DALFactory } from "./org-gateway-config-v2-dal";
type TGatewayV2ServiceFactoryDep = {
orgGatewayConfigV2DAL: Pick<TOrgGatewayConfigV2DALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: TKmsServiceFactory;
relayService: TRelayServiceFactory;
gatewayV2DAL: TGatewayV2DALFactory;
relayDAL: TRelayDALFactory;
permissionService: TPermissionServiceFactory;
};
export type TGatewayV2ServiceFactory = ReturnType<typeof gatewayV2ServiceFactory>;
export const gatewayV2ServiceFactory = ({
orgGatewayConfigV2DAL,
licenseService,
kmsService,
relayService,
gatewayV2DAL,
relayDAL,
permissionService
}: TGatewayV2ServiceFactoryDep) => {
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
const orgLicensePlan = await licenseService.getPlan(orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message:
"Gateway operation failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
});
}
const { permission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
actorId,
orgId,
actorAuthMethod,
orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.CreateGateways,
OrgPermissionSubjects.Gateway
);
};
const $getOrgCAs = async (orgId: string) => {
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
});
const orgCAs = await orgGatewayConfigV2DAL.transaction(async (tx) => {
const orgGatewayConfigV2 = await orgGatewayConfigV2DAL.findOne({ orgId });
if (orgGatewayConfigV2) return orgGatewayConfigV2;
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayV2Init(orgId)]);
// generate root CA
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
const alg = keyAlgorithmToAlgCfg(rootCaKeyAlgorithm);
const rootCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const rootCaSerialNumber = createSerialNumber();
const rootCaSkObj = crypto.nativeCrypto.KeyObject.from(rootCaKeys.privateKey);
const rootCaIssuedAt = new Date();
const rootCaExpiration = new Date(new Date().setFullYear(2045));
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
name: `O=${orgId},CN=Infisical Gateway Root CA`,
serialNumber: rootCaSerialNumber,
notBefore: rootCaIssuedAt,
notAfter: rootCaExpiration,
signingAlgorithm: alg,
keys: rootCaKeys,
extensions: [
// eslint-disable-next-line no-bitwise
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
]
});
// generate server CA
const serverCaSerialNumber = createSerialNumber();
const serverCaIssuedAt = new Date();
const serverCaExpiration = new Date(new Date().setFullYear(2045));
const serverCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const serverCaSkObj = crypto.nativeCrypto.KeyObject.from(serverCaKeys.privateKey);
const serverCaCert = await x509.X509CertificateGenerator.create({
serialNumber: serverCaSerialNumber,
subject: `O=${orgId},CN=Infisical Gateway Server CA`,
issuer: rootCaCert.subject,
notBefore: serverCaIssuedAt,
notAfter: serverCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: serverCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(serverCaKeys.publicKey)
]
});
// generate client CA
const clientCaSerialNumber = createSerialNumber();
const clientCaIssuedAt = new Date();
const clientCaExpiration = new Date(new Date().setFullYear(2045));
const clientCaKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientCaSkObj = crypto.nativeCrypto.KeyObject.from(clientCaKeys.privateKey);
const clientCaCert = await x509.X509CertificateGenerator.create({
serialNumber: clientCaSerialNumber,
subject: `O=${orgId},CN=Infisical Gateway Client CA`,
issuer: rootCaCert.subject,
notBefore: clientCaIssuedAt,
notAfter: clientCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: clientCaKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags.keyCertSign |
x509.KeyUsageFlags.cRLSign |
x509.KeyUsageFlags.digitalSignature |
x509.KeyUsageFlags.keyEncipherment,
true
),
new x509.BasicConstraintsExtension(true, 0, true),
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey)
]
});
const encryptedRootGatewayCaPrivateKey = orgKmsEncryptor({
plainText: Buffer.from(
rootCaSkObj.export({
type: "pkcs8",
format: "der"
})
)
}).cipherTextBlob;
const encryptedRootGatewayCaCertificate = orgKmsEncryptor({
plainText: Buffer.from(rootCaCert.rawData)
}).cipherTextBlob;
const encryptedGatewayServerCaPrivateKey = orgKmsEncryptor({
plainText: Buffer.from(serverCaSkObj.export({ type: "pkcs8", format: "der" }))
}).cipherTextBlob;
const encryptedGatewayServerCaCertificate = orgKmsEncryptor({
plainText: Buffer.from(serverCaCert.rawData)
}).cipherTextBlob;
const encryptedGatewayServerCaCertificateChain = orgKmsEncryptor({
plainText: Buffer.from(constructPemChainFromCerts([rootCaCert]))
}).cipherTextBlob;
const encryptedGatewayClientCaPrivateKey = orgKmsEncryptor({
plainText: Buffer.from(clientCaSkObj.export({ type: "pkcs8", format: "der" }))
}).cipherTextBlob;
const encryptedGatewayClientCaCertificate = orgKmsEncryptor({
plainText: Buffer.from(clientCaCert.rawData)
}).cipherTextBlob;
const encryptedGatewayClientCaCertificateChain = orgKmsEncryptor({
plainText: Buffer.from(constructPemChainFromCerts([rootCaCert]))
}).cipherTextBlob;
return orgGatewayConfigV2DAL.create({
orgId,
encryptedRootGatewayCaPrivateKey,
encryptedRootGatewayCaCertificate,
encryptedGatewayServerCaPrivateKey,
encryptedGatewayServerCaCertificate,
encryptedGatewayServerCaCertificateChain,
encryptedGatewayClientCaPrivateKey,
encryptedGatewayClientCaCertificate,
encryptedGatewayClientCaCertificateChain
});
});
const rootGatewayCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedRootGatewayCaPrivateKey });
const rootGatewayCaCertificate = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedRootGatewayCaCertificate });
const gatewayServerCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayServerCaPrivateKey });
const gatewayServerCaCertificate = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayServerCaCertificate });
const gatewayServerCaCertificateChain = orgKmsDecryptor({
cipherTextBlob: orgCAs.encryptedGatewayServerCaCertificateChain
});
const gatewayClientCaPrivateKey = orgKmsDecryptor({ cipherTextBlob: orgCAs.encryptedGatewayClientCaPrivateKey });
const gatewayClientCaCertificate = orgKmsDecryptor({
cipherTextBlob: orgCAs.encryptedGatewayClientCaCertificate
});
const gatewayClientCaCertificateChain = orgKmsDecryptor({
cipherTextBlob: orgCAs.encryptedGatewayClientCaCertificateChain
});
return {
rootGatewayCaPrivateKey,
rootGatewayCaCertificate,
gatewayServerCaPrivateKey,
gatewayServerCaCertificate,
gatewayServerCaCertificateChain,
gatewayClientCaPrivateKey,
gatewayClientCaCertificate,
gatewayClientCaCertificateChain
};
};
const listGateways = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.ListGateways,
OrgPermissionSubjects.Gateway
);
const gateways = await gatewayV2DAL.find({
orgId: orgPermission.orgId
});
return gateways;
};
const getPlatformConnectionDetailsByGatewayId = async ({
gatewayId,
targetHost,
targetPort
}: {
gatewayId: string;
targetHost: string;
targetPort: number;
}) => {
const gateway = await gatewayV2DAL.findById(gatewayId);
if (!gateway) {
return;
}
const orgGatewayConfig = await orgGatewayConfigV2DAL.findOne({ orgId: gateway.orgId });
if (!orgGatewayConfig) {
throw new NotFoundError({ message: `Gateway Config for org ${gateway.orgId} not found.` });
}
if (!gateway.relayId) {
throw new BadRequestError({
message: "Gateway is not associated with a relay"
});
}
const orgLicensePlan = await licenseService.getPlan(orgGatewayConfig.orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message: "Please upgrade your instance to Infisical's Enterprise plan to use gateways."
});
}
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgGatewayConfig.orgId
});
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const rootGatewayCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedRootGatewayCaCertificate
})
);
const gatewayClientCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaCertificate
})
);
const gatewayServerCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayServerCaCertificate
})
);
const gatewayClientCaPrivateKey = orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayClientCaPrivateKey
});
const gatewayClientCaSkObj = crypto.nativeCrypto.createPrivateKey({
key: gatewayClientCaPrivateKey,
format: "der",
type: "pkcs8"
});
const importedGatewayClientCaPrivateKey = await crypto.nativeCrypto.subtle.importKey(
"pkcs8",
gatewayClientCaSkObj.export({ format: "der", type: "pkcs8" }),
alg,
true,
["sign"]
);
const clientCertIssuedAt = new Date();
const clientCertExpiration = new Date(new Date().getTime() + 5 * 60 * 1000);
const clientKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientCertSerialNumber = createSerialNumber();
const routingInfo = {
targetHost,
targetPort
};
const routingExtension = new x509.Extension(
GATEWAY_ROUTING_INFO_OID,
false,
Buffer.from(JSON.stringify(routingInfo))
);
const actorExtension = new x509.Extension(
GATEWAY_ACTOR_OID,
false,
Buffer.from(JSON.stringify({ type: ActorType.PLATFORM }))
);
const clientCert = await x509.X509CertificateGenerator.create({
serialNumber: clientCertSerialNumber,
subject: `O=${orgGatewayConfig.orgId},OU=gateway-client,CN=${ActorType.PLATFORM}:${gatewayId}`,
issuer: gatewayClientCaCert.subject,
notAfter: clientCertExpiration,
notBefore: clientCertIssuedAt,
signingKey: importedGatewayClientCaPrivateKey,
publicKey: clientKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(gatewayClientCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
routingExtension,
actorExtension
]
});
const gatewayClientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
const relayCredentials = await relayService.getCredentialsForClient({
relayId: gateway.relayId,
orgId: gateway.orgId,
orgName: gateway.orgName,
gatewayId
});
return {
relayHost: relayCredentials.relayHost,
gateway: {
clientCertificate: clientCert.toString("pem"),
clientPrivateKey: gatewayClientCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(),
serverCertificateChain: constructPemChainFromCerts([gatewayServerCaCert, rootGatewayCaCert])
},
relay: {
clientCertificate: relayCredentials.clientCertificate,
clientPrivateKey: relayCredentials.clientPrivateKey,
serverCertificateChain: relayCredentials.serverCertificateChain
}
};
};
const registerGateway = async ({
orgId,
actorId,
actorAuthMethod,
relayName,
name
}: {
orgId: string;
actorId: string;
actorAuthMethod: ActorAuthMethod;
relayName: string;
name: string;
}) => {
await $validateIdentityAccessToGateway(orgId, actorId, actorAuthMethod);
const orgCAs = await $getOrgCAs(orgId);
let relay: TRelays = await relayDAL.findOne({ orgId, name: relayName });
if (!relay) {
relay = await relayDAL.findOne({ name: relayName, orgId: null });
}
if (!relay) {
throw new NotFoundError({ message: `Relay ${relayName} not found` });
}
try {
const [gateway] = await gatewayV2DAL.upsert(
[
{
orgId,
name,
identityId: actorId,
relayId: relay.id
}
],
["identityId"]
);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const gatewayServerCaCert = new x509.X509Certificate(orgCAs.gatewayServerCaCertificate);
const rootGatewayCaCert = new x509.X509Certificate(orgCAs.rootGatewayCaCertificate);
const gatewayClientCaCert = new x509.X509Certificate(orgCAs.gatewayClientCaCertificate);
const gatewayServerCaSkObj = crypto.nativeCrypto.createPrivateKey({
key: orgCAs.gatewayServerCaPrivateKey,
format: "der",
type: "pkcs8"
});
const gatewayServerCaPrivateKey = await crypto.nativeCrypto.subtle.importKey(
"pkcs8",
gatewayServerCaSkObj.export({ format: "der", type: "pkcs8" }),
alg,
true,
["sign"]
);
const gatewayServerKeys = await crypto.nativeCrypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const gatewayServerCertIssuedAt = new Date();
const gatewayServerCertExpireAt = new Date(new Date().setDate(new Date().getDate() + 1));
const gatewayServerCertPrivateKey = crypto.nativeCrypto.KeyObject.from(gatewayServerKeys.privateKey);
const gatewayServerCertExtensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(gatewayServerCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(gatewayServerKeys.publicKey),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
new x509.KeyUsagesExtension(
// eslint-disable-next-line no-bitwise
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
true
),
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
new x509.SubjectAlternativeNameExtension([
{ type: "dns", value: "localhost" },
{ type: "ip", value: "127.0.0.1" },
{ type: "ip", value: "::1" }
])
];
const gatewayServerSerialNumber = createSerialNumber();
const gatewayServerCertificate = await x509.X509CertificateGenerator.create({
serialNumber: gatewayServerSerialNumber,
subject: `O=${orgId},CN=Gateway`,
issuer: gatewayServerCaCert.subject,
notBefore: gatewayServerCertIssuedAt,
notAfter: gatewayServerCertExpireAt,
signingKey: gatewayServerCaPrivateKey,
publicKey: gatewayServerKeys.publicKey,
signingAlgorithm: alg,
extensions: gatewayServerCertExtensions
});
const relayCredentials = await relayService.getCredentialsForGateway({
relayName,
orgId,
gatewayId: gateway.id
});
return {
gatewayId: gateway.id,
relayHost: relayCredentials.relayHost,
pki: {
serverCertificate: gatewayServerCertificate.toString("pem"),
serverPrivateKey: gatewayServerCertPrivateKey.export({ format: "pem", type: "pkcs8" }).toString(),
clientCertificateChain: constructPemChainFromCerts([gatewayClientCaCert, rootGatewayCaCert])
},
ssh: {
clientCertificate: relayCredentials.clientSshCert,
clientPrivateKey: relayCredentials.clientSshPrivateKey,
serverCAPublicKey: relayCredentials.serverCAPublicKey
}
};
} catch (err) {
if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) {
throw new BadRequestError({ message: `Gateway with name "${name}" already exists` });
}
throw err;
}
};
const heartbeat = async ({ orgPermission }: { orgPermission: OrgServiceActor }) => {
await $validateIdentityAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
const gateway = await gatewayV2DAL.findOne({
orgId: orgPermission.orgId,
identityId: orgPermission.id
});
if (!gateway) {
throw new NotFoundError({ message: `Gateway for identity ${orgPermission.id} not found.` });
}
const gatewayV2ConnectionDetails = await getPlatformConnectionDetailsByGatewayId({
gatewayId: gateway.id,
targetHost: "health-check",
targetPort: 443
});
if (!gatewayV2ConnectionDetails) {
throw new NotFoundError({ message: `Gateway connection details for gateway ${gateway.id} not found.` });
}
const isGatewayReachable = await withGatewayV2Proxy(
async (port) => {
return new Promise<boolean>((resolve, reject) => {
const socket = new net.Socket();
let responseReceived = false;
let isResolved = false;
// Set socket timeout
socket.setTimeout(10000);
const cleanup = () => {
if (!socket.destroyed) {
socket.destroy();
}
};
socket.on("data", (data: Buffer) => {
const response = data.toString().trim();
if (response === "PONG" && !isResolved) {
isResolved = true;
responseReceived = true;
cleanup();
resolve(true);
}
});
socket.on("error", (err: Error) => {
if (!isResolved) {
isResolved = true;
cleanup();
reject(new Error(`TCP connection error: ${err.message}`));
}
});
socket.on("timeout", () => {
if (!isResolved) {
isResolved = true;
cleanup();
reject(new Error("TCP connection timeout"));
}
});
socket.on("close", () => {
if (!isResolved && !responseReceived) {
isResolved = true;
cleanup();
reject(new Error("Connection closed without receiving PONG"));
}
});
socket.connect(port, "localhost");
});
},
{
protocol: GatewayProxyProtocol.Ping,
relayHost: gatewayV2ConnectionDetails.relayHost,
gateway: gatewayV2ConnectionDetails.gateway,
relay: gatewayV2ConnectionDetails.relay
}
);
if (!isGatewayReachable) {
throw new BadRequestError({ message: `Gateway ${gateway.id} is not reachable` });
}
await gatewayV2DAL.updateById(gateway.id, { heartbeat: new Date() });
};
const deleteGatewayById = async ({ orgPermission, id }: { orgPermission: OrgServiceActor; id: string }) => {
const gateway = await gatewayV2DAL.findOne({ id, orgId: orgPermission.orgId });
if (!gateway) {
throw new NotFoundError({ message: `Gateway ${id} not found` });
}
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
gateway.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.DeleteGateways,
OrgPermissionSubjects.Gateway
);
return gatewayV2DAL.deleteById(gateway.id);
};
return {
listGateways,
registerGateway,
getPlatformConnectionDetailsByGatewayId,
deleteGatewayById,
heartbeat
};
};
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOrgGatewayConfigV2DALFactory = ReturnType<typeof orgGatewayConfigV2DalFactory>;
export const orgGatewayConfigV2DalFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.OrgGatewayConfigV2);
return orm;
};
@@ -13,7 +13,7 @@ export const gatewayDALFactory = (db: TDbClient) => {
{ offset, limit, sort, tx }: TFindOpt<TGateways> = {}
) => {
try {
const query = (tx || db)(TableName.Gateway)
const query = (tx || db.replicaNode())(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
@@ -23,7 +23,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
.whereIn(`${TableName.ProjectMembership}.projectId`, projectIds)
.pluck(`${TableName.ProjectMembership}.projectId`);
const userGroupMemberships: string[] = await (tx || db)(TableName.UserGroupMembership)
const userGroupMemberships: string[] = await (tx || db.replicaNode())(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.userId`, userId)
.whereNot(`${TableName.UserGroupMembership}.groupId`, groupId)
.join(
@@ -79,7 +79,7 @@ export const userGroupMembershipDALFactory = (db: TDbClient) => {
.pluck(`${TableName.GroupProjectMembership}.groupId`);
// main query
const members = await (tx || db)(TableName.UserGroupMembership)
const members = await (tx || db.replicaNode())(TableName.UserGroupMembership)
.where(`${TableName.UserGroupMembership}.groupId`, groupId)
.where(`${TableName.UserGroupMembership}.isPending`, false)
.join(TableName.Users, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
@@ -5,6 +5,7 @@ import { OrgMembershipStatus, TableName, TLdapConfigsUpdate, TUsers } from "@app
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@@ -127,6 +128,20 @@ export const ldapConfigServiceFactory = ({
message:
"Failed to create LDAP configuration due to plan restriction. Upgrade plan to create LDAP configuration."
});
const org = await orgDAL.findOrgById(orgId);
if (!org) {
throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` });
}
if (org.googleSsoAuthEnforced && isActive) {
throw new BadRequestError({
message:
"You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO."
});
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
@@ -233,6 +248,19 @@ export const ldapConfigServiceFactory = ({
"Failed to update LDAP configuration due to plan restriction. Upgrade plan to update LDAP configuration."
});
const org = await orgDAL.findOrgById(orgId);
if (!org) {
throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` });
}
if (org.googleSsoAuthEnforced && isActive) {
throw new BadRequestError({
message:
"You cannot enable LDAP SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable LDAP SSO."
});
}
const updateQuery: TLdapConfigsUpdate = {
isActive,
url,
@@ -390,14 +418,6 @@ export const ldapConfigServiceFactory = ({
}
});
} else {
const plan = await licenseService.getPlan(orgId);
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({
message: "Failed to create new member via LDAP due to member limit reached. Upgrade plan to add more members."
});
}
userAlias = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined;
newUser = await userDAL.findOne(
@@ -446,6 +466,8 @@ export const ldapConfigServiceFactory = ({
);
if (!orgMembership) {
await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.LDAP);
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
@@ -28,7 +28,7 @@ export const licenseDALFactory = (db: TDbClient) => {
const countOrgUsersAndIdentities = async (orgId: string | null, tx?: Knex) => {
try {
// count org users
const userDoc = await (tx || db)(TableName.OrgMembership)
const userDoc = await (tx || db.replicaNode())(TableName.OrgMembership)
.where({ status: OrgMembershipStatus.Accepted })
.andWhere((bd) => {
if (orgId) {
@@ -42,7 +42,7 @@ export const licenseDALFactory = (db: TDbClient) => {
const userCount = Number(userDoc?.[0].count);
// count org identities
const identityDoc = await (tx || db)(TableName.IdentityOrgMembership)
const identityDoc = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where((bd) => {
if (orgId) {
void bd.where({ orgId });
@@ -1,8 +1,11 @@
import axios, { AxiosError } from "axios";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { UserAliasType } from "@app/services/user-alias/user-alias-types";
import { TFeatureSet } from "./license-types";
@@ -133,3 +136,18 @@ export const setupLicenseRequestWithStore = (
return { request: licenseReq, refreshLicense };
};
export const throwOnPlanSeatLimitReached = async (
licenseService: Pick<TLicenseServiceFactory, "getPlan">,
orgId: string,
type?: UserAliasType
) => {
const plan = await licenseService.getPlan(orgId);
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({
message: `Failed to create new member${type ? ` via ${type.toUpperCase()}` : ""} due to member limit reached. Upgrade plan to add more members.`
});
}
};
@@ -99,6 +99,17 @@ export const licenseServiceFactory = ({
const workspacesUsed = await projectDAL.countOfOrgProjects(null);
currentPlan.workspacesUsed = workspacesUsed;
const usedIdentitySeats = await licenseDAL.countOrgUsersAndIdentities(null);
if (usedIdentitySeats !== currentPlan.identitiesUsed) {
const usedSeats = await licenseDAL.countOfOrgMembers(null);
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, {
usedSeats,
usedIdentitySeats
});
currentPlan.identitiesUsed = usedIdentitySeats;
currentPlan.membersUsed = usedSeats;
}
onPremFeatures = currentPlan;
logger.info("Successfully synchronized license key features");
} catch (error) {
@@ -226,10 +237,13 @@ export const licenseServiceFactory = ({
};
const refreshPlan = async (orgId: string) => {
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
if (instanceType === InstanceType.Cloud) {
await keyStore.deleteItem(FEATURE_CACHE_KEY(orgId));
await getPlan(orgId);
}
if (instanceType === InstanceType.EnterpriseOnPrem) {
await syncLicenseKeyOnPremFeatures(true);
}
};
const generateOrgCustomerId = async (orgName: string, email?: string | null) => {
@@ -296,8 +310,19 @@ export const licenseServiceFactory = ({
return data;
};
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgId, actorAuthMethod, projectId }: TOrgPlanDTO) => {
const getOrgPlan = async ({
orgId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
refreshCache
}: TOrgPlanDTO) => {
await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
if (refreshCache) {
await refreshPlan(orgId);
}
const plan = await getPlan(orgId, projectId);
return plan;
};
@@ -417,6 +442,62 @@ export const licenseServiceFactory = ({
};
};
const calculateUsageValue = (
rowName: string,
field: string,
projectCount: number,
totalIdentities: number
): string => {
if (rowName === BillingPlanRows.WorkspaceLimit.name || field === BillingPlanRows.WorkspaceLimit.field) {
return projectCount.toString();
}
if (rowName === BillingPlanRows.IdentityLimit.name || field === BillingPlanRows.IdentityLimit.field) {
return totalIdentities.toString();
}
return "-";
};
const fetchPlanTableFromServer = async (customerId: string | null | undefined) => {
if (!customerId) {
throw new NotFoundError({ message: "Organization customer ID is required for plan table retrieval" });
}
const baseUrl = `/api/license-server/v1/customers/${customerId}`;
if (instanceType === InstanceType.Cloud) {
const { data } = await licenseServerCloudApi.request.get<{
head: { name: string }[];
rows: { name: string; allowed: boolean }[];
}>(`${baseUrl}/cloud-plan/table`);
return data;
}
if (instanceType === InstanceType.EnterpriseOnPrem) {
const { data } = await licenseServerOnPremApi.request.get<{
head: { name: string }[];
rows: { name: string; allowed: boolean }[];
}>(`${baseUrl}/on-prem-plan/table`);
return data;
}
throw new Error(`Unsupported instance type for server-based plan table: ${instanceType}`);
};
const getUsageMetrics = async (orgId: string) => {
const [orgMembersUsed, identityUsed, projectCount] = await Promise.all([
orgDAL.countAllOrgMembers(orgId),
identityOrgMembershipDAL.countAllOrgIdentities({ orgId }),
projectDAL.countOfOrgProjects(orgId)
]);
return {
orgMembersUsed,
identityUsed,
projectCount,
totalIdentities: identityUsed + orgMembersUsed
};
};
// returns org current plan feature table
const getOrgPlanTable = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
@@ -429,55 +510,25 @@ export const licenseServiceFactory = ({
});
}
const orgMembersUsed = await orgDAL.countAllOrgMembers(orgId);
const identityUsed = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId });
const projects = await projectDAL.find({ orgId });
const projectCount = projects.length;
const { projectCount, totalIdentities } = await getUsageMetrics(orgId);
if (instanceType === InstanceType.Cloud) {
const { data } = await licenseServerCloudApi.request.get<{
head: { name: string }[];
rows: { name: string; allowed: boolean }[];
}>(`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`);
if (instanceType === InstanceType.Cloud || instanceType === InstanceType.EnterpriseOnPrem) {
const tableResponse = await fetchPlanTableFromServer(organization.customerId);
const formattedData = {
head: data.head,
rows: data.rows.map((el) => {
let used = "-";
if (el.name === BillingPlanRows.WorkspaceLimit.name) {
used = projectCount.toString();
} else if (el.name === BillingPlanRows.IdentityLimit.name) {
used = (identityUsed + orgMembersUsed).toString();
}
return {
...el,
used
};
})
return {
head: tableResponse.head,
rows: tableResponse.rows.map((row) => ({
...row,
used: calculateUsageValue(row.name, "", projectCount, totalIdentities)
}))
};
return formattedData;
}
const mappedRows = await Promise.all(
Object.values(BillingPlanRows).map(async ({ name, field }: { name: string; field: string }) => {
const allowed = onPremFeatures[field as keyof TFeatureSet];
let used = "-";
if (field === BillingPlanRows.WorkspaceLimit.field) {
used = projectCount.toString();
} else if (field === BillingPlanRows.IdentityLimit.field) {
used = (identityUsed + orgMembersUsed).toString();
}
return {
name,
allowed,
used
};
})
);
const mappedRows = Object.values(BillingPlanRows).map(({ name, field }) => ({
name,
allowed: onPremFeatures[field as keyof TFeatureSet] || false,
used: calculateUsageValue(name, field, projectCount, totalIdentities)
}));
return {
head: Object.values(BillingPlanTableHead),
@@ -87,6 +87,7 @@ export type TOrgPlansTableDTO = {
export type TOrgPlanDTO = {
projectId?: string;
refreshCache?: boolean;
} & TOrgPermission;
export type TStartOrgTrialDTO = {
@@ -8,6 +8,7 @@ import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/a
import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -294,6 +295,8 @@ export const oidcConfigServiceFactory = ({
);
if (!orgMembership) {
await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.OIDC);
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
@@ -499,6 +502,13 @@ export const oidcConfigServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
if (org.googleSsoAuthEnforced && isActive) {
throw new BadRequestError({
message:
"You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO."
});
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: org.id
@@ -586,6 +596,13 @@ export const oidcConfigServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
if (org.googleSsoAuthEnforced && isActive) {
throw new BadRequestError({
message:
"You cannot enable OIDC SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable OIDC SSO."
});
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: org.id
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TInstanceRelayConfigDALFactory = ReturnType<typeof instanceRelayConfigDalFactory>;
export const instanceRelayConfigDalFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.InstanceRelayConfig);
return orm;
};
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOrgRelayConfigDALFactory = ReturnType<typeof orgRelayConfigDalFactory>;
export const orgRelayConfigDalFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.OrgRelayConfig);
return orm;
};
@@ -0,0 +1,11 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TRelayDALFactory = ReturnType<typeof relayDalFactory>;
export const relayDalFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Relay);
return orm;
};
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import { ForbiddenError } from "@casl/ability";
import { OrgMembershipStatus, TableName, TSamlConfigs, TSamlConfigsUpdate, TUsers } from "@app/db/schemas";
import { throwOnPlanSeatLimitReached } from "@app/ee/services/license/license-fns";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
@@ -82,6 +83,19 @@ export const samlConfigServiceFactory = ({
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to create SSO configuration."
});
const org = await orgDAL.findOrgById(orgId);
if (!org) {
throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` });
}
if (org.googleSsoAuthEnforced && isActive) {
throw new BadRequestError({
message:
"You cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO."
});
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId
@@ -120,6 +134,19 @@ export const samlConfigServiceFactory = ({
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
const org = await orgDAL.findOrgById(orgId);
if (!org) {
throw new NotFoundError({ message: `Could not find organization with ID "${orgId}"` });
}
if (org.googleSsoAuthEnforced && isActive) {
throw new BadRequestError({
message:
"Cannot enable SAML SSO while Google OAuth is enforced. Disable Google OAuth enforcement to enable SAML SSO."
});
}
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive, lastUsed: null };
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
@@ -310,14 +337,6 @@ export const samlConfigServiceFactory = ({
return foundUser;
});
} else {
const plan = await licenseService.getPlan(orgId);
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({
message: "Failed to create new member via SAML due to member limit reached. Upgrade plan to add more members."
});
}
user = await userDAL.transaction(async (tx) => {
let newUser: TUsers | undefined;
newUser = await userDAL.findOne(
@@ -365,6 +384,8 @@ export const samlConfigServiceFactory = ({
);
if (!orgMembership) {
await throwOnPlanSeatLimitReached(licenseService, orgId, UserAliasType.SAML);
const { role, roleId } = await getDefaultOrgMembershipRole(organization.defaultMembershipRole);
await orgMembershipDAL.create(
@@ -345,7 +345,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
const findProjectRequestCount = async (projectId: string, userId: string, policyId?: string, tx?: Knex) => {
try {
const docs = await (tx || db)
const docs = await (tx || db.replicaNode())
.with(
"temp",
(tx || db.replicaNode())(TableName.SecretApprovalRequest)
@@ -494,7 +494,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
.as("inner");
const query = (tx || db)
const query = (tx || db.replicaNode())
.select("*")
.select(db.raw("count(*) OVER() as total_count"))
.from(innerQuery)
@@ -377,7 +377,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => {
// special query for migration to v2 secret
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretApprovalRequestSecret)
const docs = await (tx || db.replicaNode())(TableName.SecretApprovalRequestSecret)
.join(
TableName.SecretApprovalRequest,
`${TableName.SecretApprovalRequest}.id`,
@@ -787,6 +787,7 @@ export const secretApprovalRequestServiceFactory = ({
},
tx
);
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
@@ -976,6 +977,7 @@ export const secretApprovalRequestServiceFactory = ({
},
tx
);
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx);
return {
secrets: { created: newSecrets, updated: updatedSecrets, deleted: deletedSecret },
approval: updatedSecretApproval
@@ -983,7 +985,6 @@ export const secretApprovalRequestServiceFactory = ({
});
}
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
await snapshotService.performSnapshot(folderId);
const [folder] = await folderDAL.findSecretPathByFolderIds(projectId, [folderId]);
if (!folder) {
@@ -509,9 +509,9 @@ export const secretReplicationServiceFactory = ({
tx
);
}
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId, tx);
});
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(projectId);
await secretQueueService.syncSecrets({
projectId,
orgId,
@@ -82,6 +82,7 @@ import {
import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import { awsIamUserSecretRotationFactory } from "./aws-iam-user-secret/aws-iam-user-secret-rotation-fns";
import { oktaClientSecretRotationFactory } from "./okta-client-secret/okta-client-secret-rotation-fns";
import { TSecretRotationV2DALFactory } from "./secret-rotation-v2-dal";
@@ -110,6 +111,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
};
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
@@ -153,7 +155,8 @@ export const secretRotationV2ServiceFactory = ({
queueService,
folderCommitService,
appConnectionDAL,
gatewayService
gatewayService,
gatewayV2Service
}: TSecretRotationV2ServiceFactoryDep) => {
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
const appCfg = getConfig();
@@ -467,7 +470,8 @@ export const secretRotationV2ServiceFactory = ({
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService,
gatewayService
gatewayService,
gatewayV2Service
);
// even though we have a db constraint we want to check before any rotation of credentials is attempted
@@ -831,7 +835,8 @@ export const secretRotationV2ServiceFactory = ({
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService,
gatewayService
gatewayService,
gatewayV2Service
);
const generatedCredentials = await decryptSecretRotationCredentials({
@@ -915,7 +920,8 @@ export const secretRotationV2ServiceFactory = ({
} as TSecretRotationV2WithConnection,
appConnectionDAL,
kmsService,
gatewayService
gatewayService,
gatewayV2Service
);
const updatedRotation = await rotationFactory.rotateCredentials(
@@ -6,6 +6,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { SecretsOrderBy } from "@app/services/secret/secret-types";
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import {
TAuth0ClientSecretRotation,
TAuth0ClientSecretRotationGeneratedCredentials,
@@ -253,7 +254,8 @@ export type TRotationFactory<
secretRotation: T,
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">
) => {
issueCredentials: TRotationFactoryIssueCredentials<C, P>;
revokeCredentials: TRotationFactoryRevokeCredentials<C>;
@@ -41,7 +41,7 @@ const ORACLE_PASSWORD_REQUIREMENTS = {
export const sqlCredentialsRotationFactory: TRotationFactory<
TSqlCredentialsRotationWithConnection,
TSqlCredentialsRotationGeneratedCredentials
> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService) => {
> = (secretRotation, _appConnectionDAL, _kmsService, gatewayService, gatewayV2Service) => {
const {
connection,
parameters: { username1, username2 },
@@ -67,6 +67,7 @@ export const sqlCredentialsRotationFactory: TRotationFactory<
credentials: finalCredentials
},
gatewayService,
gatewayV2Service,
(client) => operation(client)
);
};
@@ -361,9 +361,8 @@ export const secretRotationQueueFactory = ({
},
tx
);
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId, tx);
});
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
} else {
if (!botKey)
throw new NotFoundError({
@@ -265,7 +265,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
// then joins with respective secrets and folder
const findRecursivelySnapshots = async (snapshotId: string, tx?: Knex) => {
try {
const data = await (tx || db)
const data = await (tx || db.replicaNode())
.withRecursive("parent", (qb) => {
void qb
.from(TableName.Snapshot)
@@ -419,7 +419,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
// then joins with respective secrets and folder
const findRecursivelySnapshotsV2Bridge = async (snapshotId: string, tx?: Knex) => {
try {
const data = await (tx || db)
const data = await (tx || db.replicaNode())
.withRecursive("parent", (qb) => {
void qb
.from(TableName.Snapshot)
@@ -581,7 +581,11 @@ export const snapshotDALFactory = (db: TDbClient) => {
const docs = await (tx || db.replicaNode())(TableName.Snapshot)
.where(`${TableName.Snapshot}.folderId`, folderId)
.join<TSecretSnapshots>(
(tx || db)(TableName.Snapshot).groupBy("folderId").max("createdAt").select("folderId").as("latestVersion"),
(tx || db.replicaNode())(TableName.Snapshot)
.groupBy("folderId")
.max("createdAt")
.select("folderId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.Snapshot}.folderId`, "latestVersion.folderId").andOn(
`${TableName.Snapshot}.createdAt`,
@@ -766,7 +770,7 @@ export const snapshotDALFactory = (db: TDbClient) => {
)
.orderBy(`${TableName.Snapshot}.createdAt`, "desc")
.where(`${TableName.Snapshot}.folderId`, folderId);
const data = await (tx || db)
const data = await (tx || db.replicaNode())
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")