1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-29 22:02:57 +00:00

Compare commits

..

1 Commits

82 changed files with 578 additions and 4373 deletions
backend/src
cli
docker-compose.prod.yml
docs
frontend
package-lock.jsonpackage.json
public/lotties
src
components/v2
context/OrgPermissionContext
hooks/api
layouts/OrganizationLayout
OrganizationLayout.tsx
components
MenuIconButton
MinimizedOrgSidebar
main.tsx
pages
organization
public/ShareSecretPage/components
secret-manager
SecretDashboardPage/components
ActionBar/CreateDynamicSecretForm
DynamicSecretListView/EditDynamicSecretForm
PitDrawer
SecretListView
integrations/BitbucketConfigurePage
routeTree.gen.tsroutes.ts
sink

@ -13,7 +13,6 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
@ -229,7 +228,6 @@ declare module "fastify" {
secretSync: TSecretSyncServiceFactory;
kmip: TKmipServiceFactory;
kmipOperation: TKmipOperationServiceFactory;
gateway: TGatewayServiceFactory;
};
// this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer

@ -68,9 +68,6 @@ import {
TExternalKms,
TExternalKmsInsert,
TExternalKmsUpdate,
TGateways,
TGatewaysInsert,
TGatewaysUpdate,
TGitAppInstallSessions,
TGitAppInstallSessionsInsert,
TGitAppInstallSessionsUpdate,
@ -182,9 +179,6 @@ import {
TOrgBots,
TOrgBotsInsert,
TOrgBotsUpdate,
TOrgGatewayConfig,
TOrgGatewayConfigInsert,
TOrgGatewayConfigUpdate,
TOrgMemberships,
TOrgMembershipsInsert,
TOrgMembershipsUpdate,
@ -206,9 +200,6 @@ import {
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate,
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate,
TProjectKeys,
TProjectKeysInsert,
TProjectKeysUpdate,
@ -939,16 +930,5 @@ declare module "knex/types/tables" {
TKmipClientCertificatesInsert,
TKmipClientCertificatesUpdate
>;
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate
>;
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
TOrgGatewayConfig,
TOrgGatewayConfigInsert,
TOrgGatewayConfigUpdate
>;
}
}

@ -1,115 +0,0 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.OrgGatewayConfig))) {
await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("rootCaKeyAlgorithm").notNullable();
t.datetime("rootCaIssuedAt").notNullable();
t.datetime("rootCaExpiration").notNullable();
t.string("rootCaSerialNumber").notNullable();
t.binary("encryptedRootCaCertificate").notNullable();
t.binary("encryptedRootCaPrivateKey").notNullable();
t.datetime("clientCaIssuedAt").notNullable();
t.datetime("clientCaExpiration").notNullable();
t.string("clientCaSerialNumber");
t.binary("encryptedClientCaCertificate").notNullable();
t.binary("encryptedClientCaPrivateKey").notNullable();
t.string("clientCertSerialNumber").notNullable();
t.string("clientCertKeyAlgorithm").notNullable();
t.datetime("clientCertIssuedAt").notNullable();
t.datetime("clientCertExpiration").notNullable();
t.binary("encryptedClientCertificate").notNullable();
t.binary("encryptedClientPrivateKey").notNullable();
t.datetime("gatewayCaIssuedAt").notNullable();
t.datetime("gatewayCaExpiration").notNullable();
t.string("gatewayCaSerialNumber").notNullable();
t.binary("encryptedGatewayCaCertificate").notNullable();
t.binary("encryptedGatewayCaPrivateKey").notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.unique("orgId");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
}
if (!(await knex.schema.hasTable(TableName.Gateway))) {
await knex.schema.createTable(TableName.Gateway, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("name").notNullable();
t.string("serialNumber").notNullable();
t.string("keyAlgorithm").notNullable();
t.datetime("issuedAt").notNullable();
t.datetime("expiration").notNullable();
t.datetime("heartbeat");
t.binary("relayAddress").notNullable();
t.uuid("orgGatewayRootCaId").notNullable();
t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE");
t.uuid("identityId").notNullable();
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.Gateway);
}
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) {
await knex.schema.createTable(TableName.ProjectGateway, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("projectId").notNullable();
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
t.uuid("gatewayId").notNullable();
t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE");
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.ProjectGateway);
}
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
// not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) {
t.uuid("projectGatewayId");
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
}
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
if (doesGatewayColExist) t.dropColumn("projectGatewayId");
});
}
await knex.schema.dropTableIfExists(TableName.ProjectGateway);
await dropOnUpdateTrigger(knex, TableName.ProjectGateway);
await knex.schema.dropTableIfExists(TableName.Gateway);
await dropOnUpdateTrigger(knex, TableName.Gateway);
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
}

@ -1,35 +0,0 @@
import { Knex } from "knex";
import { TableName } from "@app/db/schemas";
export async function up(knex: Knex): Promise<void> {
for await (const tableName of [
TableName.SecretV2,
TableName.SecretVersionV2,
TableName.SecretApprovalRequestSecretV2
]) {
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
if (hasReminderNoteCol) {
await knex.schema.alterTable(tableName, (t) => {
t.string("reminderNote", 1024).alter();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
for await (const tableName of [
TableName.SecretV2,
TableName.SecretVersionV2,
TableName.SecretApprovalRequestSecretV2
]) {
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
if (hasReminderNoteCol) {
await knex.schema.alterTable(tableName, (t) => {
t.string("reminderNote").alter();
});
}
}
}

@ -26,8 +26,7 @@ export const DynamicSecretsSchema = z.object({
statusDetails: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date(),
encryptedInput: zodBuffer,
projectGatewayId: z.string().uuid().nullable().optional()
encryptedInput: zodBuffer
});
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;

@ -1,29 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const GatewaysSchema = z.object({
id: z.string().uuid(),
name: z.string(),
serialNumber: z.string(),
keyAlgorithm: z.string(),
issuedAt: z.date(),
expiration: z.date(),
heartbeat: z.date().nullable().optional(),
relayAddress: zodBuffer,
orgGatewayRootCaId: z.string().uuid(),
identityId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGateways = z.infer<typeof GatewaysSchema>;
export type TGatewaysInsert = Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>;
export type TGatewaysUpdate = Partial<Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>>;

@ -20,7 +20,6 @@ export * from "./certificates";
export * from "./dynamic-secret-leases";
export * from "./dynamic-secrets";
export * from "./external-kms";
export * from "./gateways";
export * from "./git-app-install-sessions";
export * from "./git-app-org";
export * from "./group-project-membership-roles";
@ -58,7 +57,6 @@ export * from "./ldap-group-maps";
export * from "./models";
export * from "./oidc-configs";
export * from "./org-bots";
export * from "./org-gateway-config";
export * from "./org-memberships";
export * from "./org-roles";
export * from "./organizations";
@ -67,7 +65,6 @@ export * from "./pki-collection-items";
export * from "./pki-collections";
export * from "./project-bots";
export * from "./project-environments";
export * from "./project-gateways";
export * from "./project-keys";
export * from "./project-memberships";
export * from "./project-roles";

@ -113,10 +113,6 @@ export enum TableName {
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
ProjectSplitBackfillIds = "project_split_backfill_ids",
// Gateway
OrgGatewayConfig = "org_gateway_config",
Gateway = "gateways",
ProjectGateway = "project_gateways",
// junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction",

@ -1,43 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const OrgGatewayConfigSchema = z.object({
id: z.string().uuid(),
rootCaKeyAlgorithm: z.string(),
rootCaIssuedAt: z.date(),
rootCaExpiration: z.date(),
rootCaSerialNumber: z.string(),
encryptedRootCaCertificate: zodBuffer,
encryptedRootCaPrivateKey: zodBuffer,
clientCaIssuedAt: z.date(),
clientCaExpiration: z.date(),
clientCaSerialNumber: z.string().nullable().optional(),
encryptedClientCaCertificate: zodBuffer,
encryptedClientCaPrivateKey: zodBuffer,
clientCertSerialNumber: z.string(),
clientCertKeyAlgorithm: z.string(),
clientCertIssuedAt: z.date(),
clientCertExpiration: z.date(),
encryptedClientCertificate: zodBuffer,
encryptedClientPrivateKey: zodBuffer,
gatewayCaIssuedAt: z.date(),
gatewayCaExpiration: z.date(),
gatewayCaSerialNumber: z.string(),
encryptedGatewayCaCertificate: zodBuffer,
encryptedGatewayCaPrivateKey: zodBuffer,
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TOrgGatewayConfig = z.infer<typeof OrgGatewayConfigSchema>;
export type TOrgGatewayConfigInsert = Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>;
export type TOrgGatewayConfigUpdate = Partial<Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>>;

@ -1,20 +0,0 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const ProjectGatewaysSchema = z.object({
id: z.string().uuid(),
projectId: z.string(),
gatewayId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TProjectGateways = z.infer<typeof ProjectGatewaysSchema>;
export type TProjectGatewaysInsert = Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>;
export type TProjectGatewaysUpdate = Partial<Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>>;

@ -1,265 +0,0 @@
import { z } from "zod";
import { GatewaysSchema } from "@app/db/schemas";
import { isValidIp } from "@app/lib/ip";
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 SanitizedGatewaySchema = GatewaysSchema.pick({
id: true,
identityId: true,
name: true,
createdAt: true,
updatedAt: true,
issuedAt: true,
serialNumber: true,
heartbeat: true
});
const isValidRelayAddress = (relayAddress: string) => {
const [ip, port] = relayAddress.split(":");
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
};
export const registerGatewayRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/register-identity",
config: {
rateLimit: writeLimit
},
schema: {
response: {
200: z.object({
turnServerUsername: z.string(),
turnServerPassword: z.string(),
turnServerRealm: z.string(),
turnServerAddress: z.string(),
infisicalStaticIp: z.string().optional()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const relayDetails = await server.services.gateway.getGatewayRelayDetails(
req.permission.id,
req.permission.orgId,
req.permission.authMethod
);
return relayDetails;
}
});
server.route({
method: "POST",
url: "/exchange-cert",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" })
}),
response: {
200: z.object({
serialNumber: z.string(),
privateKey: z.string(),
certificate: z.string(),
certificateChain: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
identityOrg: req.permission.orgId,
identityId: req.permission.id,
relayAddress: req.body.relayAddress,
identityOrgAuthMethod: req.permission.authMethod
});
return gatewayCertificates;
}
});
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.gateway.heartbeat({
orgPermission: req.permission
});
return { message: "Successfully registered heartbeat" };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
projectId: z.string().optional()
}),
response: {
200: z.object({
gateways: SanitizedGatewaySchema.extend({
identity: z.object({
name: z.string(),
id: z.string()
}),
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateways = await server.services.gateway.listGateways({
orgPermission: req.permission
});
return { gateways };
}
});
server.route({
method: "GET",
url: "/projects/:projectId",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
projectId: z.string()
}),
response: {
200: z.object({
gateways: SanitizedGatewaySchema.extend({
identity: z.object({
name: z.string(),
id: z.string()
}),
projectGatewayId: z.string()
}).array()
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({
projectId: req.params.projectId,
projectPermission: req.permission
});
return { gateways };
}
});
server.route({
method: "GET",
url: "/:id",
config: {
rateLimit: readLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
gateway: SanitizedGatewaySchema.extend({
identity: z.object({
name: z.string(),
id: z.string()
})
})
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateway = await server.services.gateway.getGatewayById({
orgPermission: req.permission,
id: req.params.id
});
return { gateway };
}
});
server.route({
method: "PATCH",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
name: slugSchema({ field: "name" }).optional(),
projectIds: z.string().array().optional()
}),
response: {
200: z.object({
gateway: SanitizedGatewaySchema
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission,
id: req.params.id,
name: req.body.name,
projectIds: req.body.projectIds
});
return { gateway };
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
gateway: SanitizedGatewaySchema
})
}
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => {
const gateway = await server.services.gateway.deleteGatewayById({
orgPermission: req.permission,
id: req.params.id
});
return { gateway };
}
});
};

@ -7,7 +7,6 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGatewayRouter } from "./gateway-router";
import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerKmipRouter } from "./kmip-router";
@ -68,8 +67,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
{ prefix: "/dynamic-secrets" }
);
await server.register(registerGatewayRouter, { prefix: "/gateways" });
await server.register(
async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });

@ -1,31 +1,20 @@
import crypto from "node:crypto";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { getDbConnectionHost } from "@app/lib/knex";
export const verifyHostInputValidity = (host: string, isGateway = false) => {
export const verifyHostInputValidity = (host: string) => {
const appCfg = getConfig();
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
// no need for validation when it's dev
if (appCfg.NODE_ENV === "development") return;
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
if (
appCfg.isCloud &&
!isGateway &&
// localhost
// internal ips
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
)
throw new BadRequestError({ message: "Invalid db host" });
if (
host === "localhost" ||
host === "127.0.0.1" ||
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
) {
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
throw new BadRequestError({ message: "Invalid db host" });
}
};

@ -16,7 +16,6 @@ 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 { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,
@ -45,7 +44,6 @@ type TDynamicSecretServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@ -59,8 +57,7 @@ export const dynamicSecretServiceFactory = ({
permissionService,
dynamicSecretQueueService,
projectDAL,
kmsService,
projectGatewayDAL
kmsService
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
path,
@ -111,18 +108,6 @@ export const dynamicSecretServiceFactory = ({
const selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
const projectGatewayId = inputs.projectGatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found`
});
selectedGatewayId = projectGateway.id;
}
const isConnected = await selectedProvider.validateConnection(provider.inputs);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
@ -138,8 +123,7 @@ export const dynamicSecretServiceFactory = ({
maxTTL,
defaultTTL,
folderId: folder.id,
name,
projectGatewayId: selectedGatewayId
name
});
return dynamicSecretCfg;
};
@ -211,23 +195,6 @@ export const dynamicSecretServiceFactory = ({
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null;
if (
updatedInput &&
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found`
});
selectedGatewayId = projectGateway.id;
}
const isConnected = await selectedProvider.validateConnection(newInput);
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
@ -237,8 +204,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
name: newName ?? name,
status: null,
statusDetails: null,
projectGatewayId: selectedGatewayId
statusDetails: null
});
return updatedDynamicCfg;

@ -1,6 +1,5 @@
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
import { AwsIamProvider } from "./aws-iam";
import { AzureEntraIDProvider } from "./azure-entra-id";
@ -17,14 +16,8 @@ import { SapHanaProvider } from "./sap-hana";
import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
};
export const buildDynamicSecretProviders = ({
gatewayService
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),

@ -103,8 +103,7 @@ export const DynamicSecretSqlDBSchema = z.object({
creationStatement: z.string().trim(),
revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().nullable().optional()
ca: z.string().optional()
});
export const DynamicSecretCassandraSchema = z.object({

@ -3,10 +3,8 @@ import knex from "knex";
import { customAlphabet } from "nanoid";
import { z } from "zod";
import { withGatewayProxy } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
import { verifyHostInputValidity } from "../dynamic-secret-fns";
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
@ -27,14 +25,10 @@ const generateUsername = (provider: SqlProviders) => {
return alphaNumericNanoId(32);
};
type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
};
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
verifyHostInputValidity(providerInputs.host);
return providerInputs;
};
@ -51,6 +45,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
user: providerInputs.username,
password: providerInputs.password,
ssl,
pool: { min: 0, max: 1 },
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
@ -66,112 +61,61 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
return db;
};
const gatewayProxyWrapper = async (
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void>
) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy(
async (port) => {
await gatewayCallback("localhost", port);
},
{
targetHost: providerInputs.host,
targetPort: providerInputs.port,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey
}
}
);
};
const validateConnection = async (inputs: unknown) => {
const providerInputs = await validateProviderInputs(inputs);
let isConnected = false;
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
const db = await $getClient(providerInputs);
// oracle needs from keyword
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
isConnected = await db.raw(testStatement).then(() => true);
await db.destroy();
};
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
const isConnected = await db.raw(testStatement).then(() => true);
await db.destroy();
return isConnected;
};
const create = async (inputs: unknown, expireAt: number) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await $getClient(providerInputs);
const username = generateUsername(providerInputs.client);
const password = generatePassword(providerInputs.client);
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
try {
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const expiration = new Date(expireAt).toISOString();
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration,
database
});
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
username,
password,
expiration,
database
});
const queries = creationStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
} finally {
await db.destroy();
const queries = creationStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
};
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
});
await db.destroy();
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
};
const revoke = async (inputs: unknown, entityId: string) => {
const providerInputs = await validateProviderInputs(inputs);
const db = await $getClient(providerInputs);
const username = entityId;
const { database } = providerInputs;
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
try {
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
const queries = revokeStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
} finally {
await db.destroy();
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
const queries = revokeStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
};
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
}
});
await db.destroy();
return { entityId: username };
};
@ -179,35 +123,28 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
const providerInputs = await validateProviderInputs(inputs);
if (!providerInputs.renewStatement) return { entityId };
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
const db = await $getClient({ ...providerInputs, port, host });
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const db = await $getClient(providerInputs);
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
expiration,
database
});
try {
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
});
const expiration = new Date(expireAt).toISOString();
const { database } = providerInputs;
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
username: entityId,
expiration,
database
});
if (renewStatement) {
const queries = renewStatement.toString().split(";").filter(Boolean);
await db.transaction(async (tx) => {
for (const query of queries) {
// eslint-disable-next-line
await tx.raw(query);
}
} finally {
await db.destroy();
}
};
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
});
}
await db.destroy();
return { entityId };
};

@ -1,86 +0,0 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import {
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
export const gatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Gateway);
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
try {
const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("name").withSchema(TableName.Project).as("projectName"),
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
db.ref("id").withSchema(TableName.Project).as("projectId")
);
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 sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (data) => ({
...GatewaysSchema.parse(data),
identity: { id: data.identityId, name: data.identityName }
}),
childrenMapper: [
{
key: "projectId",
label: "projects" as const,
mapper: ({ projectId, projectName, projectSlug }) => ({
id: projectId,
name: projectName,
slug: projectSlug
})
}
]
});
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
}
};
const findByProjectId = async (projectId: string, tx?: Knex) => {
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(
db.ref("name").withSchema(TableName.Identity).as("identityName"),
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
)
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
}
};
return { ...orm, find, findByProjectId };
};

@ -1,652 +0,0 @@
import crypto from "node:crypto";
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { pingGatewayAndVerify } from "@app/lib/gateway";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { getTurnCredentials } from "@app/lib/turn/credentials";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
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";
import { TGatewayDALFactory } from "./gateway-dal";
import {
TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO,
TListGatewaysDTO,
TUpdateGatewayByIdDTO
} from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
};
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
username: z.string(),
password: z.string()
});
export const gatewayServiceFactory = ({
gatewayDAL,
licenseService,
kmsService,
permissionService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
}: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
// if (!licenseService.onPremFeatures.gateway) {
// throw new BadRequestError({
// message:
// "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
// });
// }
const orgLicensePlan = await licenseService.getPlan(orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message:
"Gateway handshake 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 getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes
const envCfg = getConfig();
await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) {
throw new BadRequestError({
message: "Gateway handshake failed due to missing instance configuration."
});
}
let turnServerUsername = "";
let turnServerPassword = "";
// keep it in redis for 5mins to avoid generating so many credentials
const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
if (previousCredential) {
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(
JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString())
);
turnServerUsername = el.username;
turnServerPassword = el.password;
} else {
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
await keyStore.setItemWithExpiry(
KeyStorePrefixes.GatewayIdentityCredential(actorId),
TURN_CRED_EXPIRY,
encryptor({
plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password }))
}).cipherTextBlob.toString("hex")
);
turnServerUsername = el.username;
turnServerPassword = el.password;
}
return {
turnServerUsername,
turnServerPassword,
turnServerRealm: envCfg.GATEWAY_RELAY_REALM,
turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS,
infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS
};
};
const exchangeAllocatedRelayAddress = async ({
identityId,
identityOrg,
relayAddress,
identityOrgAuthMethod
}: TExchangeAllocatedRelayAddressDTO) => {
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identityOrg
});
const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]);
const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg });
if (existingGatewayConfig) return existingGatewayConfig;
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
// generate root CA
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const rootCaSerialNumber = createSerialNumber();
const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey);
const rootCaIssuedAt = new Date();
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
const rootCaExpiration = new Date(new Date().setFullYear(2045));
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
name: `O=${identityOrg},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 client ca
const clientCaSerialNumber = createSerialNumber();
const clientCaIssuedAt = new Date();
const clientCaExpiration = new Date(new Date().setFullYear(2045));
const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey);
const clientCaCert = await x509.X509CertificateGenerator.create({
serialNumber: clientCaSerialNumber,
subject: `O=${identityOrg},CN=Client Intermediate 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 clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const clientCertSerialNumber = createSerialNumber();
const clientCert = await x509.X509CertificateGenerator.create({
serialNumber: clientCertSerialNumber,
subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
issuer: clientCaCert.subject,
notAfter: clientCaExpiration,
notBefore: clientCaIssuedAt,
signingKey: clientCaKeys.privateKey,
publicKey: clientKeys.publicKey,
signingAlgorithm: alg,
extensions: [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, 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)
]
});
const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey);
// generate gateway ca
const gatewayCaSerialNumber = createSerialNumber();
const gatewayCaIssuedAt = new Date();
const gatewayCaExpiration = new Date(new Date().setFullYear(2045));
const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey);
const gatewayCaCert = await x509.X509CertificateGenerator.create({
serialNumber: gatewayCaSerialNumber,
subject: `O=${identityOrg},CN=Gateway CA`,
issuer: rootCaCert.subject,
notBefore: gatewayCaIssuedAt,
notAfter: gatewayCaExpiration,
signingKey: rootCaKeys.privateKey,
publicKey: gatewayCaKeys.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(gatewayCaKeys.publicKey)
]
});
return orgGatewayConfigDAL.create({
orgId: identityOrg,
rootCaIssuedAt,
rootCaExpiration,
rootCaSerialNumber,
rootCaKeyAlgorithm,
encryptedRootCaPrivateKey: orgKmsEncryptor({
plainText: rootCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
clientCaIssuedAt,
clientCaExpiration,
clientCaSerialNumber,
encryptedClientCaPrivateKey: orgKmsEncryptor({
plainText: clientCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
encryptedClientCaCertificate: orgKmsEncryptor({
plainText: Buffer.from(clientCaCert.rawData)
}).cipherTextBlob,
clientCertIssuedAt: clientCaIssuedAt,
clientCertExpiration: clientCaExpiration,
clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048,
clientCertSerialNumber,
encryptedClientPrivateKey: orgKmsEncryptor({
plainText: clientSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
encryptedClientCertificate: orgKmsEncryptor({
plainText: Buffer.from(clientCert.rawData)
}).cipherTextBlob,
gatewayCaIssuedAt,
gatewayCaExpiration,
gatewayCaSerialNumber,
encryptedGatewayCaPrivateKey: orgKmsEncryptor({
plainText: gatewayCaSkObj.export({
type: "pkcs8",
format: "der"
})
}).cipherTextBlob,
encryptedGatewayCaCertificate: orgKmsEncryptor({
plainText: Buffer.from(gatewayCaCert.rawData)
}).cipherTextBlob
});
});
const rootCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
})
);
const clientCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate
})
);
const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm);
const gatewayCaSkObj = crypto.createPrivateKey({
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }),
format: "der",
type: "pkcs8"
});
const gatewayCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
})
);
const gatewayCaPrivateKey = await crypto.subtle.importKey(
"pkcs8",
gatewayCaSkObj.export({ format: "der", type: "pkcs8" }),
gatewayCaAlg,
true,
["sign"]
);
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
const certIssuedAt = new Date();
// then need to periodically init
const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1));
const extensions: x509.Extension[] = [
new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false),
await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.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),
// san
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false)
];
const serialNumber = createSerialNumber();
const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey);
const gatewayCertificate = await x509.X509CertificateGenerator.create({
serialNumber,
subject: `CN=${identityId},O=${identityOrg},OU=Gateway`,
issuer: gatewayCaCert.subject,
notBefore: certIssuedAt,
notAfter: certExpireAt,
signingKey: gatewayCaPrivateKey,
publicKey: gatewayKeys.publicKey,
signingAlgorithm: alg,
extensions
});
const appCfg = getConfig();
// just for local development
const formatedRelayAddress =
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
await gatewayDAL.transaction(async (tx) => {
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
if (existingGateway) {
return gatewayDAL.updateById(existingGateway.id, {
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
issuedAt: certIssuedAt,
expiration: certExpireAt,
serialNumber,
relayAddress: orgKmsEncryptor({
plainText: Buffer.from(formatedRelayAddress)
}).cipherTextBlob
});
}
return gatewayDAL.create({
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
issuedAt: certIssuedAt,
expiration: certExpireAt,
serialNumber,
relayAddress: orgKmsEncryptor({
plainText: Buffer.from(formatedRelayAddress)
}).cipherTextBlob,
identityId,
orgGatewayRootCaId: orgGatewayConfig.id,
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
});
});
const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim();
return {
serialNumber,
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
certificate: gatewayCertificate.toString("pem"),
certificateChain: gatewayCertificateChain
};
};
const heartbeat = async ({ orgPermission }: THeartBeatDTO) => {
await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` });
const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` });
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgGatewayConfig.orgId
});
const rootCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
})
);
const gatewayCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
})
);
const clientCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
})
);
const privateKey = crypto
.createPrivateKey({
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
format: "der",
type: "pkcs8"
})
.export({ type: "pkcs8", format: "pem" });
const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString();
const [relayHost, relayPort] = relayAddress.split(":");
await pingGatewayAndVerify({
relayHost,
relayPort: Number(relayPort),
tlsOptions: {
key: privateKey,
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
cert: clientCert.toString("pem")
},
identityId: orgPermission.id,
orgId: orgPermission.orgId
});
await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() });
};
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.ListGateways,
OrgPermissionSubjects.Gateway
);
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
if (!orgGatewayConfig) return [];
const gateways = await gatewayDAL.find({
orgGatewayRootCaId: orgGatewayConfig.id
});
return gateways;
};
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.ListGateways,
OrgPermissionSubjects.Gateway
);
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
return gateway;
};
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.EditGateways,
OrgPermissionSubjects.Gateway
);
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
if (projectIds) {
await projectGatewayDAL.transaction(async (tx) => {
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
await projectGatewayDAL.insertMany(
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
tx
);
});
}
return gateway;
};
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionGatewayActions.DeleteGateways,
OrgPermissionSubjects.Gateway
);
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
return gateway;
};
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
actorOrgId: projectPermission.orgId,
actorAuthMethod: projectPermission.authMethod,
actionProjectType: ActionProjectType.Any
});
const gateways = await gatewayDAL.findByProjectId(projectId);
return gateways;
};
// this has no permission check and used for dynamic secrets directly
// assumes permission check is already done
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
const { gatewayId } = projectGateway;
const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgGatewayConfig.orgId
});
const rootCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
})
);
const gatewayCaCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
})
);
const clientCert = new x509.X509Certificate(
orgKmsDecryptor({
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
})
);
const clientSkObj = crypto.createPrivateKey({
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
format: "der",
type: "pkcs8"
});
return {
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
certificate: clientCert.toString("pem"),
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
identityId: gateway.identityId,
orgId: orgGatewayConfig.orgId
};
};
return {
getGatewayRelayDetails,
exchangeAllocatedRelayAddress,
listGateways,
getGatewayById,
updateGatewayById,
deleteGatewayById,
getProjectGateways,
fnGetGatewayClientTls,
heartbeat
};
};

@ -1,39 +0,0 @@
import { OrgServiceActor } from "@app/lib/types";
import { ActorAuthMethod } from "@app/services/auth/auth-type";
export type TExchangeAllocatedRelayAddressDTO = {
identityId: string;
identityOrg: string;
identityOrgAuthMethod: ActorAuthMethod;
relayAddress: string;
};
export type TListGatewaysDTO = {
orgPermission: OrgServiceActor;
};
export type TGetGatewayByIdDTO = {
id: string;
orgPermission: OrgServiceActor;
};
export type TUpdateGatewayByIdDTO = {
id: string;
name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor;
};
export type TDeleteGatewayByIdDTO = {
id: string;
orgPermission: OrgServiceActor;
};
export type TGetProjectGatewayByIdDTO = {
projectId: string;
projectPermission: OrgServiceActor;
};
export type THeartBeatDTO = {
orgPermission: OrgServiceActor;
};

@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TOrgGatewayConfigDALFactory = ReturnType<typeof orgGatewayConfigDALFactory>;
export const orgGatewayConfigDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.OrgGatewayConfig);
return orm;
};

@ -1,10 +0,0 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
export const projectGatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectGateway);
return orm;
};

@ -51,8 +51,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
pkiEst: false,
enforceMfa: false,
projectTemplates: false,
kmip: false,
gateway: false
kmip: false
});
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {

@ -69,7 +69,6 @@ export type TFeatureSet = {
enforceMfa: boolean;
projectTemplates: false;
kmip: false;
gateway: false;
};
export type TOrgPlansTableDTO = {

@ -32,14 +32,6 @@ export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects"
}
export enum OrgPermissionGatewayActions {
// is there a better word for this. This mean can an identity be a gateway
CreateGateways = "create-gateways",
ListGateways = "list-gateways",
EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@ -58,8 +50,7 @@ export enum OrgPermissionSubjects {
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway"
Kmip = "kmip"
}
export type AppConnectionSubjectFields = {
@ -82,7 +73,6 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
| [
OrgPermissionAppConnectionActions,
(
@ -190,12 +180,6 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
"Describe what action an entity can take."
)
})
]);
@ -280,11 +264,6 @@ const buildAdminPermission = () => {
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
@ -321,8 +300,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
return rules;
};

@ -1,15 +1,12 @@
import { Redis } from "ioredis";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { Redlock, Settings } from "@app/lib/red-lock";
export const PgSqlLock = {
BootUpMigration: 2023,
SuperAdminInit: 2024,
KmsRootKeyInit: 2025,
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
} as const;
export enum PgSqlLock {
BootUpMigration = 2023,
SuperAdminInit = 2024,
KmsRootKeyInit = 2025
}
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
@ -36,8 +33,7 @@ export const KeyStorePrefixes = {
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
};
export const KeyStoreTtls = {

@ -184,14 +184,6 @@ const envSchema = z
USE_PG_QUEUE: zodStrBool.default("false"),
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
/* Gateway----------------------------------------------------------------------------- */
GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()),
GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()),
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
/* ----------------------------------------------------------------------------- */
/* App Connections ----------------------------------------------------------------------------- */
// aws

@ -1,29 +0,0 @@
// used for postgres lock
// this is something postgres does under the hood
// convert any string to a unique number
export const hashtext = (text: string) => {
// Convert text to UTF8 bytes array for consistent behavior with PostgreSQL
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
// Implementation of hash_any
let result = 0;
for (let i = 0; i < bytes.length; i += 1) {
// eslint-disable-next-line no-bitwise
result = ((result << 5) + result) ^ bytes[i];
// Keep within 32-bit integer range
// eslint-disable-next-line no-bitwise
result >>>= 0;
}
// Convert to signed 32-bit integer like PostgreSQL
// eslint-disable-next-line no-bitwise
return result | 0;
};
export const pgAdvisoryLockHashText = (text: string) => {
const hash = hashtext(text);
// Ensure positive value within PostgreSQL integer range
return Math.abs(hash) % 2 ** 31;
};

@ -1,264 +0,0 @@
/* eslint-disable no-await-in-loop */
import net from "node:net";
import tls from "node:tls";
import { BadRequestError } from "../errors";
import { logger } from "../logger";
const DEFAULT_MAX_RETRIES = 3;
const DEFAULT_RETRY_DELAY = 1000; // 1 second
const createTLSConnection = (relayHost: string, relayPort: number, tlsOptions: tls.TlsOptions = {}) => {
return new Promise<tls.TLSSocket>((resolve, reject) => {
// @ts-expect-error this is resolved in next connect
const socket = new tls.TLSSocket(null, {
rejectUnauthorized: true,
...tlsOptions
});
const cleanup = () => {
socket.removeAllListeners();
socket.end();
};
socket.once("error", (err) => {
cleanup();
reject(err);
});
socket.connect(relayPort, relayHost, () => {
resolve(socket);
});
});
};
type TPingGatewayAndVerifyDTO = {
relayHost: string;
relayPort: number;
tlsOptions: tls.TlsOptions;
maxRetries?: number;
identityId: string;
orgId: string;
};
export const pingGatewayAndVerify = async ({
relayHost,
relayPort,
tlsOptions = {},
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
}: TPingGatewayAndVerifyDTO) => {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
try {
const socket = await createTLSConnection(relayHost, relayPort, tlsOptions);
socket.setTimeout(2000);
const pingResult = await new Promise((resolve, reject) => {
socket.once("timeout", () => {
socket.destroy();
reject(new Error("Timeout"));
});
socket.once("close", () => {
socket.destroy();
});
socket.once("end", () => {
socket.destroy();
});
socket.once("error", (err) => {
reject(err);
});
socket.write(Buffer.from("PING\n"), () => {
socket.once("data", (data) => {
const response = (data as string).toString();
const certificate = socket.getPeerCertificate();
if (certificate.subject.CN !== identityId || certificate.subject.O !== orgId) {
throw new BadRequestError({
message: `Invalid gateway. Certificate not found for ${identityId} in organization ${orgId}`
});
}
if (response === "PONG") {
resolve(true);
} else {
reject(new Error(`Unexpected response: ${response}`));
}
});
});
});
socket.end();
return pingResult;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
await new Promise((resolve) => {
setTimeout(resolve, DEFAULT_RETRY_DELAY);
});
}
}
}
logger.error(lastError);
throw new BadRequestError({
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
});
};
interface TProxyServer {
server: net.Server;
port: number;
cleanup: () => void;
}
const setupProxyServer = ({
targetPort,
targetHost,
tlsOptions = {},
relayHost,
relayPort
}: {
targetHost: string;
targetPort: number;
relayPort: number;
relayHost: string;
tlsOptions: tls.TlsOptions;
}): Promise<TProxyServer> => {
return new Promise((resolve, reject) => {
const server = net.createServer();
// eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientSocket) => {
try {
const targetSocket = await createTLSConnection(relayHost, relayPort, tlsOptions);
targetSocket.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`), () => {
clientSocket.on("data", (data) => {
const flushed = targetSocket.write(data);
if (!flushed) {
clientSocket.pause();
targetSocket.once("drain", () => {
clientSocket.resume();
});
}
});
targetSocket.on("data", (data) => {
const flushed = clientSocket.write(data as string);
if (!flushed) {
targetSocket.pause();
clientSocket.once("drain", () => {
targetSocket.resume();
});
}
});
});
const cleanup = () => {
clientSocket?.unpipe();
clientSocket?.end();
targetSocket?.unpipe();
targetSocket?.end();
};
clientSocket.on("error", (err) => {
logger.error(err, "Client socket error");
cleanup();
reject(err);
});
targetSocket.on("error", (err) => {
logger.error(err, "Target socket error");
cleanup();
reject(err);
});
clientSocket.on("end", cleanup);
targetSocket.on("end", cleanup);
} catch (err) {
logger.error(err, "Failed to establish target connection:");
clientSocket.end();
reject(err);
}
});
server.on("error", (err) => {
reject(err);
});
server.listen(0, () => {
const address = server.address();
if (!address || typeof address === "string") {
server.close();
reject(new Error("Failed to get server port"));
return;
}
logger.info("Gateway proxy started");
resolve({
server,
port: address.port,
cleanup: () => {
server.close();
}
});
});
});
};
interface ProxyOptions {
targetHost: string;
targetPort: number;
relayHost: string;
relayPort: number;
tlsOptions?: tls.TlsOptions;
maxRetries?: number;
identityId: string;
orgId: string;
}
export const withGatewayProxy = async (
callback: (port: number) => Promise<void>,
options: ProxyOptions
): Promise<void> => {
const {
relayHost,
relayPort,
targetHost,
targetPort,
tlsOptions = {},
maxRetries = DEFAULT_MAX_RETRIES,
identityId,
orgId
} = options;
// First, try to ping the gateway
await pingGatewayAndVerify({
relayHost,
relayPort,
tlsOptions,
maxRetries,
identityId,
orgId
});
// Setup the proxy server
const { port, cleanup } = await setupProxyServer({ targetHost, targetPort, relayPort, relayHost, tlsOptions });
try {
// Execute the callback with the allocated port
await callback(port);
} catch (err) {
logger.error(err, "Failed to proxy");
throw new BadRequestError({ message: (err as Error)?.message });
} finally {
// Ensure cleanup happens regardless of success or failure
cleanup();
}
};

@ -1,16 +0,0 @@
import crypto from "node:crypto";
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
const timestamp = Math.floor((Date.now() + ttl) / 1000);
const username = `${timestamp}:${id}`;
const hmac = crypto.createHmac("sha1", authSecret);
hmac.update(username);
const password = hmac.digest("base64");
return {
username,
password
};
};

@ -27,10 +27,6 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@ -397,10 +393,6 @@ export const registerRoutes = async (
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@ -1308,19 +1300,7 @@ export const registerRoutes = async (
kmsService
});
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({
gatewayService
});
const dynamicSecretProviders = buildDynamicSecretProviders();
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
queueService,
dynamicSecretLeaseDAL,
@ -1338,10 +1318,8 @@ export const registerRoutes = async (
folderDAL,
permissionService,
licenseService,
kmsService,
projectGatewayDAL
kmsService
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
projectDAL,
permissionService,
@ -1579,8 +1557,7 @@ export const registerRoutes = async (
appConnection: appConnectionService,
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService,
gateway: gatewayService
kmipOperation: kmipOperationService
});
const cronJobs: CronJob[] = [];

@ -537,12 +537,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
.optional()
.nullable()
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
secretReminderNote: z
.string()
.max(1024, "Secret reminder note cannot exceed 1024 characters")
.optional()
.nullable()
.describe(RAW_SECRETS.CREATE.secretReminderNote)
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote)
}),
response: {
200: z.union([
@ -645,12 +640,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
metadata: z.record(z.string()).optional(),
secretMetadata: ResourceMetadataSchema.optional(),
secretReminderNote: z
.string()
.max(1024, "Secret reminder note cannot exceed 1024 characters")
.optional()
.nullable()
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderRepeatDays: z
.number()
.optional()
@ -2063,12 +2053,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
secretReminderNote: z
.string()
.max(1024, "Secret reminder note cannot exceed 1024 characters")
.optional()
.nullable()
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
secretMetadata: ResourceMetadataSchema.optional(),
secretReminderRepeatDays: z
.number()

@ -18,16 +18,14 @@ require (
github.com/muesli/reflow v0.3.0
github.com/muesli/roff v0.1.0
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
github.com/pion/logging v0.2.3
github.com/pion/turn/v4 v4.0.0
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
github.com/rs/cors v1.11.0
github.com/rs/zerolog v1.26.1
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.33.0
golang.org/x/term v0.29.0
golang.org/x/crypto v0.31.0
golang.org/x/term v0.27.0
gopkg.in/yaml.v2 v2.4.0
)
@ -83,10 +81,6 @@ require (
github.com/muesli/termenv v0.15.2 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pion/dtls/v3 v3.0.4 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
@ -94,7 +88,6 @@ require (
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect
go.opencensus.io v0.24.0 // indirect
@ -105,9 +98,9 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.6.0 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect

@ -347,18 +347,6 @@ github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5d
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -422,8 +410,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
@ -472,8 +458,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -574,8 +560,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -624,11 +610,11 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -638,8 +624,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

@ -544,59 +544,3 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN
return nil
}
func CallRegisterGatewayIdentityV1(httpClient *resty.Client) (*GetRelayCredentialsResponseV1, error) {
var resBody GetRelayCredentialsResponseV1
response, err := httpClient.
R().
SetResult(&resBody).
SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/gateways/register-identity", config.INFISICAL_URL))
if err != nil {
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return &resBody, nil
}
func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCertRequestV1) (*ExchangeRelayCertResponseV1, error) {
var resBody ExchangeRelayCertResponseV1
response, err := httpClient.
R().
SetResult(&resBody).
SetBody(request).
SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/gateways/exchange-cert", config.INFISICAL_URL))
if err != nil {
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return &resBody, nil
}
func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
response, err := httpClient.
R().
SetHeader("User-Agent", USER_AGENT).
Post(fmt.Sprintf("%v/v1/gateways/heartbeat", config.INFISICAL_URL))
if err != nil {
return fmt.Errorf("CallGatewayHeartBeatV1: Unable to complete api request [err=%w]", err)
}
if response.IsError() {
return fmt.Errorf("CallGatewayHeartBeatV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
}
return nil
}

@ -629,22 +629,3 @@ type GetRawSecretV3ByNameResponse struct {
} `json:"secret"`
ETag string
}
type GetRelayCredentialsResponseV1 struct {
TurnServerUsername string `json:"turnServerUsername"`
TurnServerPassword string `json:"turnServerPassword"`
TurnServerRealm string `json:"turnServerRealm"`
TurnServerAddress string `json:"turnServerAddress"`
InfisicalStaticIp string `json:"infisicalStaticIp"`
}
type ExchangeRelayCertRequestV1 struct {
RelayAddress string `json:"relayAddress"`
}
type ExchangeRelayCertResponseV1 struct {
SerialNumber string `json:"serialNumber"`
PrivateKey string `json:"privateKey"`
Certificate string `json:"certificate"`
CertificateChain string `json:"certificateChain"`
}

@ -1,120 +0,0 @@
package cmd
import (
// "fmt"
// "github.com/Infisical/infisical-merge/packages/api"
// "github.com/Infisical/infisical-merge/packages/models"
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/Infisical/infisical-merge/packages/gateway"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/rs/zerolog/log"
// "github.com/Infisical/infisical-merge/packages/visualize"
// "github.com/rs/zerolog/log"
// "github.com/go-resty/resty/v2"
"github.com/posthog/posthog-go"
"github.com/spf13/cobra"
)
var gatewayCmd = &cobra.Command{
Example: `infisical gateway`,
Short: "Used to infisical gateway",
Use: "gateway",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if token == nil {
util.HandleError(fmt.Errorf("Token not found"))
}
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
sigStopCh := make(chan bool, 1)
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
go func() {
<-sigCh
close(sigStopCh)
cancel()
// If we get a second signal, force exit
<-sigCh
log.Warn().Msgf("Force exit triggered")
os.Exit(1)
}()
// Main gateway retry loop with proper context handling
retryTicker := time.NewTicker(5 * time.Second)
defer retryTicker.Stop()
for {
if ctx.Err() != nil {
log.Info().Msg("Shutting down gateway")
return
}
gatewayInstance, err := gateway.NewGateway(token.Token)
if err != nil {
util.HandleError(err)
}
if err = gatewayInstance.ConnectWithRelay(); err != nil {
if ctx.Err() != nil {
log.Info().Msg("Shutting down gateway")
return
}
log.Error().Msgf("Gateway connection error with relay: %s", err)
log.Info().Msg("Retrying connection in 5 seconds...")
select {
case <-retryTicker.C:
continue
case <-ctx.Done():
log.Info().Msg("Shutting down gateway")
return
}
}
err = gatewayInstance.Listen(ctx)
if ctx.Err() != nil {
log.Info().Msg("Gateway shutdown complete")
return
}
log.Error().Msgf("Gateway listen error: %s", err)
log.Info().Msg("Retrying connection in 5 seconds...")
select {
case <-retryTicker.C:
continue
case <-ctx.Done():
log.Info().Msg("Shutting down gateway")
return
}
}
},
}
func init() {
gatewayCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
command.Flags().MarkHidden("domain")
command.Parent().HelpFunc()(command, strings)
})
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
rootCmd.AddCommand(gatewayCmd)
}

@ -1,107 +0,0 @@
package gateway
import (
"bufio"
"bytes"
"errors"
"io"
"net"
"sync"
"github.com/rs/zerolog/log"
)
func handleConnection(conn net.Conn) {
defer conn.Close()
log.Info().Msgf("New connection from: %s", conn.RemoteAddr().String())
// Use buffered reader for better handling of fragmented data
reader := bufio.NewReader(conn)
for {
msg, err := reader.ReadBytes('\n')
if err != nil {
if errors.Is(err, io.EOF) {
return
}
log.Error().Msgf("Error reading command: %s", err)
return
}
cmd := bytes.ToUpper(bytes.TrimSpace(bytes.Split(msg, []byte(" "))[0]))
args := bytes.TrimSpace(bytes.TrimPrefix(msg, cmd))
switch string(cmd) {
case "FORWARD-TCP":
proxyAddress := string(bytes.Split(args, []byte(" "))[0])
destTarget, err := net.Dial("tcp", proxyAddress)
if err != nil {
log.Error().Msgf("Failed to connect to target: %v", err)
return
}
defer destTarget.Close()
// Handle buffered data
buffered := reader.Buffered()
if buffered > 0 {
bufferedData := make([]byte, buffered)
_, err := reader.Read(bufferedData)
if err != nil {
log.Error().Msgf("Error reading buffered data: %v", err)
return
}
if _, err = destTarget.Write(bufferedData); err != nil {
log.Error().Msgf("Error writing buffered data: %v", err)
return
}
}
CopyData(conn, destTarget)
return
case "PING":
if _, err := conn.Write([]byte("PONG")); err != nil {
log.Error().Msgf("Error writing PONG response: %v", err)
}
return
default:
log.Error().Msgf("Unknown command: %s", string(cmd))
return
}
}
}
type CloseWrite interface {
CloseWrite() error
}
func CopyData(src, dst net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
copyAndClose := func(dst, src net.Conn, done chan<- bool) {
defer wg.Done()
_, err := io.Copy(dst, src)
if err != nil && !errors.Is(err, io.EOF) {
log.Error().Msgf("Copy error: %v", err)
}
// Signal we're done writing
done <- true
// Half close the connection if possible
if c, ok := dst.(CloseWrite); ok {
c.CloseWrite()
}
}
done1 := make(chan bool, 1)
done2 := make(chan bool, 1)
go copyAndClose(dst, src, done1)
go copyAndClose(src, dst, done2)
// Wait for both copies to complete
<-done1
<-done2
wg.Wait()
}

@ -1,350 +0,0 @@
package gateway
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/go-resty/resty/v2"
"github.com/pion/logging"
"github.com/pion/turn/v4"
"github.com/rs/zerolog/log"
)
type GatewayConfig struct {
TurnServerUsername string
TurnServerPassword string
TurnServerAddress string
InfisicalStaticIp string
SerialNumber string
PrivateKey string
Certificate string
CertificateChain string
}
type Gateway struct {
httpClient *resty.Client
config *GatewayConfig
client *turn.Client
}
func NewGateway(identityToken string) (Gateway, error) {
httpClient := resty.New()
httpClient.SetAuthToken(identityToken)
return Gateway{
httpClient: httpClient,
config: &GatewayConfig{},
}, nil
}
func (g *Gateway) ConnectWithRelay() error {
relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient)
if err != nil {
return err
}
relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1]
var conn net.Conn
// Dial TURN Server
if relayPort == "5349" {
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{
InsecureSkipVerify: false,
ServerName: relayAddress,
})
} else {
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
peerAddr, err := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress)
if err != nil {
return fmt.Errorf("Failed to parse turn server address: %w", err)
}
conn, err = net.DialTCP("tcp", nil, peerAddr)
}
if err != nil {
return fmt.Errorf("Failed to connect with relay server: %w", err)
}
// Start a new TURN Client and wrap our net.Conn in a STUNConn
// This allows us to simulate datagram based communication over a net.Conn
cfg := &turn.ClientConfig{
STUNServerAddr: relayDetails.TurnServerAddress,
TURNServerAddr: relayDetails.TurnServerAddress,
Conn: turn.NewSTUNConn(conn),
Username: relayDetails.TurnServerUsername,
Password: relayDetails.TurnServerPassword,
Realm: relayDetails.TurnServerRealm,
LoggerFactory: logging.NewDefaultLoggerFactory(),
}
client, err := turn.NewClient(cfg)
if err != nil {
return fmt.Errorf("Failed to create relay client: %w", err)
}
g.config = &GatewayConfig{
TurnServerUsername: relayDetails.TurnServerUsername,
TurnServerPassword: relayDetails.TurnServerPassword,
TurnServerAddress: relayDetails.TurnServerAddress,
InfisicalStaticIp: relayDetails.InfisicalStaticIp,
}
// if port not specific allow all port
if relayDetails.InfisicalStaticIp != "" && !strings.Contains(relayDetails.InfisicalStaticIp, ":") {
g.config.InfisicalStaticIp = g.config.InfisicalStaticIp + ":0"
}
g.client = client
return nil
}
func (g *Gateway) Listen(ctx context.Context) error {
defer g.client.Close()
err := g.client.Listen()
if err != nil {
return fmt.Errorf("Failed to listen to relay server: %w", err)
}
log.Info().Msg("Connected with relay")
// Allocate a relay socket on the TURN server. On success, it
// will return a net.PacketConn which represents the remote
// socket.
relayNonTlsConn, err := g.client.AllocateTCP()
if err != nil {
return fmt.Errorf("Failed to allocate relay connection: %w", err)
}
log.Info().Msg(relayNonTlsConn.Addr().String())
defer func() {
if closeErr := relayNonTlsConn.Close(); closeErr != nil {
log.Error().Msgf("Failed to close connection: %s", closeErr)
}
}()
gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{
RelayAddress: relayNonTlsConn.Addr().String(),
})
if err != nil {
return err
}
g.config.SerialNumber = gatewayCert.SerialNumber
g.config.PrivateKey = gatewayCert.PrivateKey
g.config.Certificate = gatewayCert.Certificate
g.config.CertificateChain = gatewayCert.CertificateChain
shutdownCh := make(chan bool, 1)
if g.config.InfisicalStaticIp != "" {
log.Info().Msgf("Found static ip from Infisical: %s. Creating permission IP lifecycle", g.config.InfisicalStaticIp)
peerAddr, err := net.ResolveTCPAddr("tcp", g.config.InfisicalStaticIp)
if err != nil {
return fmt.Errorf("Failed to parse infisical static ip: %w", err)
}
g.registerPermissionLifecycle(func() error {
err := relayNonTlsConn.CreatePermissions(peerAddr)
return err
}, shutdownCh)
}
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
if err != nil {
return fmt.Errorf("failed to parse cert: %s", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain))
relayConn := tls.NewListener(relayNonTlsConn, &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
})
errCh := make(chan error, 1)
log.Info().Msg("Gateway started successfully")
g.registerHeartBeat(errCh, shutdownCh)
g.registerRelayIsActive(relayNonTlsConn.Addr().String(), errCh, shutdownCh)
// Create a WaitGroup to track active connections
var wg sync.WaitGroup
go func() {
for {
if relayDeadlineConn, ok := relayConn.(*net.TCPListener); ok {
relayDeadlineConn.SetDeadline(time.Now().Add(1 * time.Second))
}
select {
case <-ctx.Done():
return
case <-shutdownCh:
return
default:
// Accept new relay connection
conn, err := relayConn.Accept()
if err != nil {
// Check if it's a timeout error (which we expect due to our deadline)
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
if !strings.Contains(err.Error(), "data contains incomplete STUN or TURN frame") {
log.Error().Msgf("Failed to accept connection: %v", err)
}
continue
}
tlsConn, ok := conn.(*tls.Conn)
if !ok {
log.Error().Msg("Failed to convert to TLS connection")
conn.Close()
continue
}
// Set a deadline for the handshake to prevent hanging
tlsConn.SetDeadline(time.Now().Add(10 * time.Second))
err = tlsConn.Handshake()
// Clear the deadline after handshake
tlsConn.SetDeadline(time.Time{})
if err != nil {
log.Error().Msgf("TLS handshake failed: %v", err)
conn.Close()
continue
}
// Get connection state which contains certificate information
state := tlsConn.ConnectionState()
if len(state.PeerCertificates) > 0 {
organizationUnit := state.PeerCertificates[0].Subject.OrganizationalUnit
commonName := state.PeerCertificates[0].Subject.CommonName
if organizationUnit[0] != "gateway-client" || commonName != "cloud" {
log.Error().Msgf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName)
conn.Close()
continue
}
}
// Handle the connection in a goroutine
wg.Add(1)
go func(c net.Conn) {
defer wg.Done()
defer c.Close()
// Monitor parent context to close this connection when needed
go func() {
select {
case <-ctx.Done():
c.Close() // Force close connection when context is canceled
case <-shutdownCh:
c.Close() // Force close connection when accepting loop is done
}
}()
handleConnection(c)
}(conn)
}
}
}()
select {
case <-ctx.Done():
log.Info().Msg("Shutting down gateway...")
case err = <-errCh:
}
// Signal the accept loop to stop
close(shutdownCh)
// Set a timeout for waiting on connections to close
waitCh := make(chan struct{})
go func() {
wg.Wait()
close(waitCh)
}()
select {
case <-waitCh:
// All connections closed normally
case <-time.After(5 * time.Second):
log.Warn().Msg("Timeout waiting for connections to close gracefully")
}
return err
}
func (g *Gateway) registerHeartBeat(errCh chan error, done chan bool) {
ticker := time.NewTicker(1 * time.Hour)
go func() {
time.Sleep(10 * time.Second)
log.Info().Msg("Registering first heart beat")
err := api.CallGatewayHeartBeatV1(g.httpClient)
if err != nil {
log.Error().Msgf("Failed to register heartbeat: %s", err)
}
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
log.Info().Msg("Registering heart beat")
err := api.CallGatewayHeartBeatV1(g.httpClient)
errCh <- err
}
}
}()
}
func (g *Gateway) registerPermissionLifecycle(permissionFn func() error, done chan bool) {
ticker := time.NewTicker(3 * time.Minute)
go func() {
// wait for 5 mins
permissionFn()
log.Printf("Created permission for incoming connections")
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
permissionFn()
}
}
}()
}
func (g *Gateway) registerRelayIsActive(serverAddr string, errCh chan error, done chan bool) {
ticker := time.NewTicker(10 * time.Second)
go func() {
time.Sleep(5 * time.Second)
for {
select {
case <-done:
ticker.Stop()
return
case <-ticker.C:
conn, err := net.Dial("tcp", serverAddr)
if err != nil {
errCh <- err
return
}
if conn != nil {
conn.Close()
}
}
}
}()
}

@ -56,3 +56,4 @@ volumes:
networks:
infisical:

@ -11,6 +11,13 @@ To interact with the Infisical API, you will need to obtain an access token. Fol
**FAQ**
<AccordionGroup>
<Accordion title="What happened to the Service Token and API Key authentication modes?">
The Service Token and API Key authentication modes are being deprecated out in favor of [Identities](/documentation/platform/identity).
We expect to make a deprecation notice in the coming months alongside a larger deprecation initiative planned for Q1/Q2 2024.
With identities, we're improving significantly over the shortcomings of Service Tokens and API Keys. Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based
permission system used by users, and comes with ample more configurable security measures.
</Accordion>
<Accordion title="Why can I not create, read, update, or delete an identity?">
There are a few reasons for why this might happen:

@ -1,81 +0,0 @@
---
title: "Gateway"
sidebarTitle: "Overview"
description: "How to access private network resources from Infisical"
---
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
Common use cases include generating dynamic credentials or rotating credentials for private databases.
<Info>
**Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is
available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please
contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an
enterprise license.
</Info>
## How It Works
The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network.
Its a lightweight daemon packaged within the Infisical CLI, making it easy to deploy and manage. Once set up, the Gateway establishes a connection with a relay server, ensuring that all communication between Infisical and your Gateway is fully end-to-end encrypted.
This setup guarantees that only the platform and your Gateway can decrypt the transmitted information, keeping communication with your resources secure, private and isolated.
## Deployment
The Infisical Gateway is seamlessly integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage.
You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary.
For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview).
To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway.
Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable.
### Deployment process
<Steps>
<Step title="Create a Gateway Identity">
1. Navigate to **Organization Access Control** in your Infisical dashboard.
2. Create a dedicated machine identity for your Gateway.
3. **Best Practice:** Assign a unique identity to each Gateway for better security and management.
![Create Gateway Identity](../../../images/platform/gateways/create-identity-for-gateway.png)
</Step>
<Step title="Configure Authentication Method">
You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities).
</Step>
<Step title="Deploy the Gateway">
Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method:
```bash
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
```
Alternatively, if you already have the token, use it directly with the `--token` flag:
```bash
infisical gateway --token <your-machine-identity-token>
```
Or set it as an environment variable:
```bash
export INFISICAL_TOKEN=<your-machine-identity-token>
infisical gateway
```
<Note>
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
</Note>
</Step>
<Step title="Verify Gateway Deployment">
To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list.
![Gateway List](../../../images/platform/gateways/gateway-list.png)
</Step>
<Step title="Link Gateway to Projects">
To enable Infisical features like dynamic secrets or secret rotation to access private resources through the Gateway, you need to link the Gateway to the relevant projects.
Start by accessing the **Gateway settings** then locate the Gateway in the list, click the options menu (**:**), and select **Edit Details**.
![Edit Gateway Option](../../../images/platform/gateways/edit-gateway.png)
In the edit modal that appears, choose the projects you want the Gateway to access and click **Save** to confirm your selections.
![Project Assignment Modal](../../../images/platform/gateways/assign-project.png)
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
</Step>
</Steps>

Binary file not shown.

Before

(image error) Size: 338 KiB

Binary file not shown.

Before

(image error) Size: 375 KiB

Binary file not shown.

Before

(image error) Size: 384 KiB

Binary file not shown.

Before

(image error) Size: 365 KiB

Binary file not shown.

Before

(image error) Size: 433 KiB

@ -1,32 +0,0 @@
---
title: "AB Initio"
description: "How to use Infisical secrets in AB Initio."
---
## Prerequisites
- Set up and add envars to [Infisical](https://app.infisical.com).
- Install the [Infisical CLI](https://infisical.com/docs/cli/overview) to your server.
## Setup
<Steps>
<Step title="Authorize Infisical for AB Initio">
Create a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) in Infisical and give it the appropriate read permissions for the desired project and secret paths.
</Step>
<Step title="Add Infisical CLI to your workflow">
Update your AB Initio workflows to use Infisical CLI to inject Infisical secrets as environment variables.
```bash
# Login using the machine identity. Modify this accordingly based on the authentication method used.
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain)
# Fetch secrets from Infisical
infisical export --projectId="<>" --env="prod" > infisical.env
# Inject secrets as environment variables
source infisical.env
```
</Step>
</Steps>

@ -201,10 +201,6 @@
"documentation/platform/dynamic-secrets/totp"
]
},
{
"group": "Gateway",
"pages": ["documentation/platform/gateways/overview"]
},
"documentation/platform/project-templates",
{
"group": "Workflow Integrations",
@ -519,8 +515,7 @@
"integrations/frameworks/laravel",
"integrations/frameworks/rails",
"integrations/frameworks/dotnet",
"integrations/platforms/pm2",
"integrations/frameworks/ab-initio"
"integrations/platforms/pm2"
]
}
]

@ -23,7 +23,7 @@
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2",
"@octokit/rest": "^21.1.1",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",
@ -1605,16 +1605,16 @@
}
},
"node_modules/@octokit/core": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
"integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz",
"integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^8.0.0",
"@octokit/request": "^9.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.0.0",
"@octokit/graphql": "^8.1.2",
"@octokit/request": "^9.2.1",
"@octokit/request-error": "^6.1.7",
"@octokit/types": "^13.6.2",
"before-after-hook": "^3.0.2",
"universal-user-agent": "^7.0.0"
},
@ -1623,12 +1623,12 @@
}
},
"node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"version": "10.1.3",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
"integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.0.0",
"@octokit/types": "^13.6.2",
"universal-user-agent": "^7.0.2"
},
"engines": {
@ -1636,13 +1636,13 @@
}
},
"node_modules/@octokit/graphql": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
"integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.1.tgz",
"integrity": "sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^9.0.0",
"@octokit/types": "^13.0.0",
"@octokit/request": "^9.2.2",
"@octokit/types": "^13.8.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
@ -1650,18 +1650,18 @@
}
},
"node_modules/@octokit/openapi-types": {
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
"version": "23.0.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
"license": "MIT"
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "11.3.6",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz",
"integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==",
"version": "11.4.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.3.tgz",
"integrity": "sha512-tBXaAbXkqVJlRoA/zQVe9mUdb8rScmivqtpv3ovsC5xhje/a+NOCivs7eUhWBwCApJVsR4G5HMeaLbq7PxqZGA==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.6.2"
"@octokit/types": "^13.7.0"
},
"engines": {
"node": ">= 18"
@ -1683,12 +1683,12 @@
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "13.2.6",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz",
"integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==",
"version": "13.3.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz",
"integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.6.1"
"@octokit/types": "^13.8.0"
},
"engines": {
"node": ">= 18"
@ -1698,14 +1698,15 @@
}
},
"node_modules/@octokit/request": {
"version": "9.1.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
"integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^10.0.0",
"@octokit/request-error": "^6.0.1",
"@octokit/types": "^13.1.0",
"@octokit/endpoint": "^10.1.3",
"@octokit/request-error": "^6.1.7",
"@octokit/types": "^13.6.2",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
@ -1713,39 +1714,39 @@
}
},
"node_modules/@octokit/request-error": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
"version": "6.1.7",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
"integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.0.0"
"@octokit/types": "^13.6.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest": {
"version": "21.0.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz",
"integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==",
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
"integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
"license": "MIT",
"dependencies": {
"@octokit/core": "^6.1.2",
"@octokit/plugin-paginate-rest": "^11.0.0",
"@octokit/core": "^6.1.4",
"@octokit/plugin-paginate-rest": "^11.4.2",
"@octokit/plugin-request-log": "^5.3.1",
"@octokit/plugin-rest-endpoint-methods": "^13.0.0"
"@octokit/plugin-rest-endpoint-methods": "^13.3.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/types": {
"version": "13.6.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz",
"integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==",
"version": "13.8.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^22.2.0"
"@octokit/openapi-types": "^23.0.1"
}
},
"node_modules/@peculiar/asn1-cms": {
@ -6905,6 +6906,22 @@
"safe-buffer": "^5.1.1"
}
},
"node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",

@ -27,7 +27,7 @@
"@headlessui/react": "^1.7.19",
"@hookform/resolvers": "^3.9.1",
"@lottiefiles/dotlottie-react": "^0.12.0",
"@octokit/rest": "^21.0.2",
"@octokit/rest": "^21.1.1",
"@peculiar/x509": "^1.12.3",
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.3",

File diff suppressed because one or more lines are too long

@ -20,7 +20,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
sideOffset={-8}
sideOffset={10}
{...props}
ref={forwardedRef}
className={twMerge(

@ -45,11 +45,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
)}
style={{ maxHeight: "90%" }}
>
{title && (
<DialogPrimitive.Title>
<CardTitle subTitle={subTitle}>{title}</CardTitle>
</DialogPrimitive.Title>
)}
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
<CardBody
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
style={{ maxHeight: "90%" }}

@ -7,14 +7,6 @@ export enum OrgPermissionActions {
Delete = "delete"
}
export enum OrgGatewayPermissionActions {
// is there a better word for this. This mean can an identity be a gateway
CreateGateways = "create-gateways",
ListGateways = "list-gateways",
EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@ -33,8 +25,7 @@ export enum OrgPermissionSubjects {
AuditLogs = "audit-logs",
ProjectTemplates = "project-templates",
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway"
Kmip = "kmip"
}
export enum OrgPermissionAdminConsoleAction {
@ -77,8 +68,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway];
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
// TODO(scott): add back once org UI refactored
// | [
// OrgPermissionAppConnectionActions,

@ -54,7 +54,6 @@ export type TDynamicSecretProvider =
revocationStatement: string;
renewStatement?: string;
ca?: string | undefined;
gatewayId?: string;
};
}
| {

@ -1,2 +0,0 @@
export { useDeleteGatewayById, useUpdateGatewayById } from "./mutation";
export { gatewaysQueryKeys } from "./queries";

@ -1,30 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { gatewaysQueryKeys } from "./queries";
import { TUpdateGatewayDTO } from "./types";
export const useDeleteGatewayById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => {
return apiRequest.delete(`/api/v1/gateways/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list());
}
});
};
export const useUpdateGatewayById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds });
},
onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list());
}
});
};

@ -1,33 +0,0 @@
import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types";
export const gatewaysQueryKeys = {
allKey: () => ["gateways"],
listKey: () => [...gatewaysQueryKeys.allKey(), "list"],
list: () =>
queryOptions({
queryKey: gatewaysQueryKeys.listKey(),
queryFn: async () => {
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
return data.gateways;
}
}),
listProjectGatewayKey: ({ projectId }: TListProjectGatewayDTO) => [
...gatewaysQueryKeys.allKey(),
"list",
{ projectId }
],
listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
queryOptions({
queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
`/api/v1/gateways/projects/${projectId}`
);
return data.gateways;
}
})
};

@ -1,49 +0,0 @@
export type TGateway = {
id: string;
identityId: string;
name: string;
createdAt: string;
updatedAt: string;
issuedAt: string;
serialNumber: string;
heartbeat: string;
identity: {
name: string;
id: string;
};
projects: {
name: string;
id: string;
slug: string;
}[];
};
export type TProjectGateway = {
id: string;
identityId: string;
name: string;
createdAt: string;
updatedAt: string;
issuedAt: string;
serialNumber: string;
heartbeat: string;
projectGatewayId: string;
identity: {
name: string;
id: string;
};
};
export type TUpdateGatewayDTO = {
id: string;
name?: string;
projectIds?: string[];
};
export type TDeleteGatewayDTO = {
id: string;
};
export type TListProjectGatewayDTO = {
projectId: string;
};

@ -10,7 +10,6 @@ export * from "./certificates";
export * from "./certificateTemplates";
export * from "./dynamicSecret";
export * from "./dynamicSecretLease";
export * from "./gateways";
export * from "./groups";
export * from "./identities";
export * from "./identityProjectAdditionalPrivilege";

@ -42,7 +42,6 @@ export type SubscriptionPlan = {
has_used_trial: boolean;
caCrl: boolean;
instanceUserManagement: boolean;
gateway: boolean;
externalKms: boolean;
pkiEst: boolean;
enforceMfa: boolean;

@ -101,13 +101,6 @@ export const OrganizationLayout = () => {
</MenuItem>
)}
</Link>
<Link to="/organization/gateways">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
Gateways
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">

@ -20,46 +20,44 @@ export const MenuIconButton = <T extends ElementType = "button">({
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null);
return (
<div className={!isSelected ? "hover:px-1" : ""}>
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded my-1 p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
<Item
type="button"
role="menuitem"
className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
isDisabled && "cursor-not-allowed hover:bg-transparent",
className
)}
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
ref={inputRef}
{...props}
>
<div
className={`${
isSelected ? "opacity-100" : "opacity-0"
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
/>
{icon && (
<div className="my-auto mb-2 h-6 w-6">
<DotLottieReact
dotLottieRefCallback={(el) => {
iconRef.current = el;
}}
src={`/lotties/${icon}.json`}
loop
className="h-full w-full"
mode={lottieIconMode}
/>
</div>
</Item>
</div>
)}
<div
className="flex-grow justify-center break-words text-center"
style={{ fontSize: "10px" }}
>
{children}
</div>
</Item>
);
};

@ -5,7 +5,6 @@ import {
faBook,
faCheck,
faCog,
faDoorClosed,
faEnvelope,
faInfinity,
faInfo,
@ -85,10 +84,6 @@ export const MinimizedOrgSidebar = () => {
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription();
const [open, setOpen] = useState(false);
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
@ -165,29 +160,23 @@ export const MinimizedOrgSidebar = () => {
>
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div>
<div className="flex items-center hover:bg-mineshaft-700">
<DropdownMenu open={openOrg} onOpenChange={setOpenOrg} modal>
<DropdownMenuTrigger
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
asChild
>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pb-5 pt-6 transition-all">
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
<DropdownMenu modal>
<DropdownMenuTrigger asChild>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)}
</div>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
align="start"
side="right"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
className="p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "320px" }}
>
<div className="px-0.5 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800 p-1 transition-all duration-300">
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{currentOrg?.name.charAt(0)}
</div>
@ -252,7 +241,7 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="space-y-1">
<div className="space-y-1 px-1">
<Link to="/organization/secret-manager/overview">
{({ isActive }) => (
<MenuIconButton
@ -262,7 +251,7 @@ export const MinimizedOrgSidebar = () => {
}
icon="sliding-carousel"
>
Secrets
Secret Manager
</MenuIconButton>
)}
</Link>
@ -275,7 +264,7 @@ export const MinimizedOrgSidebar = () => {
}
icon="note"
>
PKI
Cert Manager
</MenuIconButton>
)}
</Link>
@ -307,41 +296,31 @@ export const MinimizedOrgSidebar = () => {
<Link to="/organization/secret-scanning">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan">
Scanner
Secret Scanning
</MenuIconButton>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed">
Share
Secret Sharing
</MenuIconButton>
)}
</Link>
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
asChild
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div className="w-full">
<MenuIconButton
lottieIconMode="reverse"
icon="settings-cog"
isSelected={isMoreSelected}
>
Admin
Org Controls
</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
align="start"
side="right"
className="p-1"
>
<DropdownMenuContent align="start" side="right" className="p-1">
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
@ -353,13 +332,6 @@ export const MinimizedOrgSidebar = () => {
App Connections
</DropdownMenuItem>
</Link>
<Link to="/organization/gateways">
<DropdownMenuItem
icon={<FontAwesomeIcon className="w-3" icon={faDoorClosed} />}
>
Gateways
</DropdownMenuItem>
</Link>
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://eu.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && (
@ -392,24 +364,14 @@ export const MinimizedOrgSidebar = () => {
: "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
>
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
className="w-full"
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full">
<MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support
</MenuIconButton>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
align="end"
side="right"
className="p-1"
>
<DropdownMenuContent align="start" className="p-1">
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}>
<a
@ -457,28 +419,17 @@ export const MinimizedOrgSidebar = () => {
</button>
</Tooltip>
)}
<DropdownMenu open={openUser} onOpenChange={setOpenUser}>
<DropdownMenuTrigger
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
className="w-full"
asChild
>
<DropdownMenu>
<DropdownMenuTrigger className="w-full" asChild>
<div>
<MenuIconButton icon="user">User</MenuIconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
side="right"
align="end"
className="p-1"
>
<div className="cursor-default px-1 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
<div className="p-1 pr-3">
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
<DropdownMenuContent align="start" className="p-1">
<div className="px-2 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
<div className="p-2">
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
</div>
<div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
@ -526,11 +477,11 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuItem>
</Link>
)}
<div className="mt-1 border-t border-mineshaft-600 pt-1">
<Link to="/organization/admin">
<DropdownMenuItem>Organization Admin Console</DropdownMenuItem>
</Link>
</div>
<Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
Organization Admin Console
</DropdownMenuItem>
</Link>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out

@ -19,12 +19,7 @@ import "./index.css";
import "./translation";
// don't want to use this?
// have a look at the Quick start guide
// for passing in lng and translations on init/
// https://vite.dev/guide/build#load-error-handling
window.addEventListener("vite:preloadError", () => {
window.location.reload(); // for example, refresh the page
});
// for passing in lng and translations on init
// Create a new router instance
NProgress.configure({ showSpinner: false });

@ -1,260 +0,0 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import {
faArrowUpRightFromSquare,
faBookOpen,
faEdit,
faEllipsisV,
faInfoCircle,
faMagnifyingGlass,
faPlug,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { format, formatRelative } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Modal,
ModalContent,
PageHeader,
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgGatewayPermissionActions,
OrgPermissionAppConnectionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
export const GatewayListPage = withPermission(
() => {
const [search, setSearch] = useState("");
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"deleteGateway",
"editDetails"
] as const);
const deleteGatewayById = useDeleteGatewayById();
const handleDeleteGateway = async () => {
await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, {
onSuccess: () => {
handlePopUpToggle("deleteGateway");
createNotification({
type: "success",
text: "Successfully delete gateway"
});
}
});
};
const filteredGateway = gateways?.filter((el) =>
el.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="bg-bunker-800">
<Helmet>
<title>Infisical | Gateways</title>
<meta property="og:image" content="/images/message.png" />
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader
className="w-full"
title={
<div className="flex w-full items-center">
<span>Gateways</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/documentation/platform/gateways/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
}
description="Create and configure gateway to access private network resources from Infisical"
/>
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div>
<div className="flex gap-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search gateway..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Cert Issued At</Th>
<Th>Projects</Th>
<Th>Identity</Th>
<Th>
Health Check
<Tooltip
asChild={false}
className="normal-case"
content="The last known healthcheck. Triggers every 1 hour."
>
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
</Tooltip>
</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isGatewayLoading && (
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
)}
{filteredGateway?.map((el) => (
<Tr key={el.id}>
<Td>{el.name}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
<Td>
{el.projects.map((projectDetails) => (
<Tag key={projectDetails.id} size="xs">
{projectDetails.name}
</Tag>
))}
</Td>
<Td>{el.identity.name}</Td>
<Td>
{el.heartbeat
? formatRelative(new Date(el.heartbeat), new Date())
: "-"}
</Td>
<Td className="w-5">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<OrgPermissionCan
I={OrgGatewayPermissionActions.EditGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => handlePopUpOpen("editDetails", el)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
className="text-red"
onClick={() => handlePopUpOpen("deleteGateway", el)}
>
Delete Gateway
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
))}
</TBody>
</Table>
<Modal
isOpen={popUp.editDetails.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
>
<ModalContent title="Edit Gateway">
<EditGatewayDetailsModal
gatewayDetails={popUp.editDetails.data}
onClose={() => handlePopUpToggle("editDetails")}
/>
</ModalContent>
</Modal>
{!isGatewayLoading && !filteredGateway?.length && (
<EmptyState
title={
gateways?.length
? "No Gateways match search..."
: "No Gateways have been configured"
}
icon={gateways?.length ? faSearch : faPlug}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleDeleteGateway()}
/>
</TableContainer>
</div>
</div>
</div>
</div>
</div>
);
},
{
action: OrgPermissionAppConnectionActions.Read,
subject: OrgPermissionSubjects.AppConnections
}
);

@ -1,113 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api";
import { TGateway } from "@app/hooks/api/gateways/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = {
gatewayDetails: TGateway;
onClose: () => void;
};
const schema = z.object({
name: z.string(),
projects: z
.object({
id: z.string(),
name: z.string()
})
.array()
});
export type FormData = z.infer<typeof schema>;
export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: gatewayDetails?.name
}
});
const updateGatewayById = useUpdateGatewayById();
// when gateway goes to other products switch to all
const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces(
{
type: ProjectType.SecretManager
}
);
const onFormSubmit = ({ name, projects }: FormData) => {
if (isSubmitting) return;
updateGatewayById.mutate(
{
id: gatewayDetails.id,
name,
projectIds: projects.map((el) => el.id)
},
{
onSuccess: () => {
createNotification({
type: "success",
text: "Successfully updated gateway"
});
onClose();
}
}
);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message} isRequired>
<Input {...field} placeholder="db-subnet-1" />
</FormControl>
)}
/>
<Controller
control={control}
name="projects"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Projects"
tooltipText="Select the project(s) that you'd like to add this gateway to"
errorText={error?.message}
isError={Boolean(error)}
>
<FilterableSelect
options={secretManagerWorkspaces}
placeholder="Select projects..."
value={value}
onChange={onChange}
isMulti
isLoading={isSecretManagerLoading}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
</FormControl>
)}
/>
<div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
Update
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
Cancel
</Button>
</div>
</form>
);
};

@ -1,23 +0,0 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { GatewayListPage } from "./GatewayListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
)({
component: GatewayListPage,
context: () => ({
breadcrumbs: [
{
label: "Home",
icon: () => <FontAwesomeIcon icon={faHome} />,
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Gateways"
}
]
})
});

@ -3,7 +3,6 @@ import { z } from "zod";
import { OrgPermissionSubjects } from "@app/context";
import {
OrgGatewayPermissionActions,
OrgPermissionAppConnectionActions,
OrgPermissionKmipActions
} from "@app/context/OrgPermissionContext/types";
@ -35,15 +34,6 @@ const kmipPermissionSchema = z
})
.optional();
const orgGatewayPermissionSchema = z
.object({
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional()
})
.optional();
const adminConsolePermissionSchmea = z
.object({
"access-all-projects": z.boolean().optional()
@ -82,8 +72,7 @@ export const formSchema = z.object({
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema,
gateway: orgGatewayPermissionSchema
kmip: kmipPermissionSchema
})
.optional()
});

@ -1,182 +0,0 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [
{ action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" },
{ action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" },
{ action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" },
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" }
] as const;
export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.gateway"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.[OrgGatewayPermissionActions.ListGateways]) return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.FullAccess:
setValue(
"permissions.gateway",
{
[OrgGatewayPermissionActions.ListGateways]: true,
[OrgGatewayPermissionActions.EditGateways]: true,
[OrgGatewayPermissionActions.CreateGateways]: true,
[OrgGatewayPermissionActions.DeleteGateways]: true
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.gateway",
{
[OrgGatewayPermissionActions.ListGateways]: true,
[OrgGatewayPermissionActions.EditGateways]: false,
[OrgGatewayPermissionActions.CreateGateways]: false,
[OrgGatewayPermissionActions.DeleteGateways]: false
},
{ shouldDirty: true }
);
break;
case Permission.NoAccess:
default:
setValue(
"permissions.gateway",
{
[OrgGatewayPermissionActions.ListGateways]: false,
[OrgGatewayPermissionActions.EditGateways]: false,
[OrgGatewayPermissionActions.CreateGateways]: false,
[OrgGatewayPermissionActions.DeleteGateways]: false
},
{ shouldDirty: true }
);
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td>
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td>Gateways</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="w-40 bg-mineshaft-600"
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td
colSpan={3}
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
>
<div className="grid grid-cols-3 gap-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.gateway.${action}`}
key={`permissions.gateway.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={Boolean(field.value)}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.gateways.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};

@ -74,7 +74,7 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console" | "kmip" | "gateway"
"workspace" | "organization-admin-console" | "kmip"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;

@ -14,7 +14,6 @@ import {
TFormSchema
} from "../OrgRoleModifySection.utils";
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@ -172,11 +171,6 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgGatewayPermissionRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgRoleWorkspaceRow
control={control}
setValue={setValue}

@ -120,7 +120,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="API Key" type="text" autoComplete="off" autoCorrect="off" spellCheck="false" />
<Input {...field} placeholder="API Key" type="text" />
</FormControl>
)}
/>
@ -155,7 +155,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
errorText={error?.message}
isOptional
>
<Input {...field} placeholder="Password" type="password" autoComplete="new-password" autoCorrect="off" spellCheck="false" aria-autocomplete="none" data-form-type="other" />
<Input {...field} placeholder="Password" type="password" />
</FormControl>
)}
/>

@ -1,6 +1,5 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import ms from "ms";
import { z } from "zod";
@ -19,8 +18,7 @@ import {
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
const formSchema = z.object({
@ -34,8 +32,7 @@ const formSchema = z.object({
creationStatement: z.string().min(1),
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().optional()
ca: z.string().optional()
}),
defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val);
@ -127,8 +124,6 @@ export const SqlDatabaseInputForm = ({
secretPath,
projectSlug
}: Props) => {
const { currentWorkspace } = useWorkspace();
const {
control,
setValue,
@ -142,9 +137,6 @@ export const SqlDatabaseInputForm = ({
});
const createDynamicSecret = useCreateDynamicSecret();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
// wait till previous request is finished
@ -230,42 +222,6 @@ export const SqlDatabaseInputForm = ({
/>
</div>
</div>
<div>
<Controller
control={control}
name="provider.projectGatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{projectGateways?.map((el) => (
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration

@ -1,6 +1,5 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import ms from "ms";
import { z } from "zod";
@ -19,8 +18,7 @@ import {
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
const formSchema = z.object({
@ -35,8 +33,7 @@ const formSchema = z.object({
creationStatement: z.string().min(1),
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
ca: z.string().optional(),
projectGatewayId: z.string().optional()
ca: z.string().optional()
})
.partial(),
defaultTTL: z.string().superRefine((val, ctx) => {
@ -84,7 +81,6 @@ export const EditDynamicSecretSqlProviderForm = ({
}: Props) => {
const {
control,
watch,
formState: { isSubmitting },
handleSubmit
} = useForm<TForm>({
@ -98,15 +94,8 @@ export const EditDynamicSecretSqlProviderForm = ({
}
}
});
const { currentWorkspace } = useWorkspace();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const updateDynamicSecret = useUpdateDynamicSecret();
const selectedProjectGatewayId = watch("inputs.projectGatewayId");
const isGatewayInActive =
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
// wait till previous request is finished
@ -120,10 +109,7 @@ export const EditDynamicSecretSqlProviderForm = ({
data: {
maxTTL: maxTTL || undefined,
defaultTTL,
inputs: {
...inputs,
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
},
inputs,
newName: newName === dynamicSecret.name ? undefined : newName
}
});
@ -190,44 +176,6 @@ export const EditDynamicSecretSqlProviderForm = ({
/>
</div>
</div>
<div>
<Controller
control={control}
name="inputs.projectGatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message) || isGatewayInActive}
errorText={
isGatewayInActive && selectedProjectGatewayId
? `Project Gateway ${selectedProjectGatewayId} is removed`
: error?.message
}
label="Gateway"
helperText=""
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet Gateway"
position="popper"
>
<SelectItem value={null as unknown as string} onClick={() => onChange(undefined)}>
Internet Gateway
</SelectItem>
{projectGateways?.map((el) => (
<SelectItem value={el.projectGatewayId} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div className="flex flex-col">

@ -59,12 +59,7 @@ export const PitDrawer = ({
onClick={() => onSelectSnapshot(id)}
>
<div className="flex w-full justify-between">
<div>
{(() => {
const distance = formatDistance(new Date(createdAt), new Date());
return distance.charAt(0).toUpperCase() + distance.slice(1) + " ago";
})()}
</div>
<div>{formatDistance(new Date(createdAt), new Date())}</div>
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
</div>
</Button>
@ -75,7 +70,7 @@ export const PitDrawer = ({
<Button
className="mt-8 px-4 py-3 text-sm"
isFullWidth
variant="outline_bg"
variant="star"
isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage}
onClick={fetchNextPage}

@ -1,11 +1,11 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import {
faArrowRotateRight,
faCheckCircle,
faCircle,
faCircleDot,
faClock,
faEyeSlash,
faPlus,
faShare,
faTag,
@ -236,102 +236,44 @@ export const SecretDetailSidebar = ({
}}
isOpen={isOpen}
>
<DrawerContent title={`Secret ${secret?.key}`} className="thin-scrollbar">
<DrawerContent title="Secret">
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
<div className="flex h-full flex-col">
<div className="flex flex-row">
<div className="w-full">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Controller
name="value"
key="secret-value"
control={control}
render={({ field }) => (
<FormControl label="Value">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
</div>
<div className="ml-1 mt-1.5 flex items-center">
<Button
className="w-full px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</div>
</div>
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="mb-4 px-4">
<FormControl label="Key">
<Input isDisabled {...register("key")} />
</FormControl>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Controller
name="value"
key="secret-value"
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<div className="flex items-center justify-between">
<span className="w-max text-sm text-mineshaft-300">
Multi-line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</span>
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center justify-between"
/>
</div>
)}
</ProjectPermissionCan>
render={({ field }) => (
<FormControl label="Value">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
</div>
<div
className={`mb-4 w-full border-t border-mineshaft-600 ${isOverridden ? "block" : "hidden"}`}
/>
)}
</ProjectPermissionCan>
<div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
@ -342,243 +284,194 @@ export const SecretDetailSidebar = ({
})}
>
{(isAllowed) => (
<div className="flex items-center justify-between px-4 pb-4">
<span className="w-max text-sm text-mineshaft-300">
Override with a personal value
<Tooltip
content="Override the secret value with a personal value that does not get shared with other users and machines."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</span>
<Switch
isDisabled={!isAllowed}
id="personal-override"
onCheckedChange={handleOverrideClick}
isChecked={isOverridden}
className="justify-start"
/>
</div>
<Switch
isDisabled={!isAllowed}
id="personal-override"
onCheckedChange={handleOverrideClick}
isChecked={isOverridden}
>
Override with a personal value
</Switch>
)}
</ProjectPermissionCan>
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Override Value" className="px-4">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
)}
/>
)}
</div>
<div className="mb-4 mt-2 flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div
className={`flex justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
<div
className={`text-sm text-mineshaft-300 ${tagFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
>
Tags
</div>
<div>
<FormControl>
<div
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
>
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="start" side="right" className="z-[100]">
<DropdownMenuLabel className="pl-2">
Add tags to this secret
</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<div className="p-2">
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</div>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Value Override">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
</div>
</div>
<div
className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}
)}
/>
<div
className={`flex justify-between px-4 text-mineshaft-100 ${metadataFormFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
<div
className={`text-sm text-mineshaft-300 ${metadataFormFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
>
Metadata
</div>
<FormControl>
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
<IconButton
ariaLabel="Add Key"
variant="outline_bg"
size="xs"
className="rounded-md"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
)}
<FormControl label="Metadata">
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</FormControl>
))}
<div className="mt-2">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</div>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 bg-bunker-800 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<FormControl>
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<DropdownMenuItem asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormControl label="Reminder">
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
<div className="flex items-center justify-between px-2">
<div className="ml-1 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-2">
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
<span className="text-sm text-bunker-300">
@ -597,9 +490,9 @@ export const SecretDetailSidebar = ({
</div>
</div>
) : (
<div className="ml-1 flex items-center space-x-2">
<div className="ml-1 mt-2 flex items-center space-x-2">
<Button
className="w-full px-2 py-2 font-normal"
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()}
@ -610,147 +503,92 @@ export const SecretDetailSidebar = ({
</div>
)}
</FormControl>
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
<div className="flex flex-row">
<div key={id} className="flex w-full flex-col space-y-1">
<div className="flex items-center">
<div className="w-10">
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
v{version}
</div>
</div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div>
<div className="flex w-full cursor-default">
<div className="relative w-10">
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
</div>
<div className="flex flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
</div>
<div className="group break-all pl-1 font-mono">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<button
type="button"
className="select-none"
onClick={(e) => {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
{secretValue}
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}
}}
>
<FontAwesomeIcon icon={faEyeSlash} />
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget.closest(".group")?.classList.add("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.currentTarget
.closest(".group")
?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div>
</div>
</div>
<div
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<div className="my-2 mb-4 border-b border-mineshaft-600 pb-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Multi line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<div className="ml-1 flex items-center space-x-4">
<Button
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share Secret
</Button>
</div>
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div>
<div>{format(new Date(createdAt), "Pp")}</div>
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{secretValue}</div>
</div>
</div>
))}
</div>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2 mt-4">
<div className="mb-2">
Access List
<Tooltip
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</div>
{isPending && (
@ -770,22 +608,14 @@ export const SecretDetailSidebar = ({
</Button>
)}
{!isPending && secretAccessList && (
<div className="mb-4 flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretAccessList.users.length > 0 && (
<div className="pb-3">
<div className="mb-2 font-bold">Users</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.users.map((user) => (
<div className="rounded-md bg-bunker-500">
<Tooltip
content={user.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
className="z-[100]"
>
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]">
<Link
to={
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
@ -794,7 +624,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id,
membershipId: user.membershipId
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 text-sm hover:text-primary"
>
{user.name}
</Link>
@ -809,14 +639,9 @@ export const SecretDetailSidebar = ({
<div className="mb-2 font-bold">Identities</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.identities.map((identity) => (
<div className="rounded-md bg-bunker-500">
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip
content={identity.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
content={identity.allowedActions.join(", ")}
className="z-[100]"
>
<Link
@ -827,7 +652,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id,
identityId: identity.id
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 text-sm hover:text-primary"
>
{identity.name}
</Link>
@ -842,14 +667,9 @@ export const SecretDetailSidebar = ({
<div className="mb-2 font-bold">Groups</div>
<div className="flex flex-wrap gap-2">
{secretAccessList.groups.map((group) => (
<div className="rounded-md bg-bunker-500">
<div className="rounded-md bg-bunker-500 px-1">
<Tooltip
content={group.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
content={group.allowedActions.join(", ")}
className="z-[100]"
>
<Link
@ -857,7 +677,7 @@ export const SecretDetailSidebar = ({
params={{
groupId: group.id
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 text-sm hover:text-primary"
>
{group.name}
</Link>
@ -871,7 +691,7 @@ export const SecretDetailSidebar = ({
)}
</div>
<div className="flex flex-col space-y-4">
<div className="mb-4 flex items-center space-x-4">
<div className="mb-2 flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
@ -885,7 +705,6 @@ export const SecretDetailSidebar = ({
<Button
isFullWidth
type="submit"
variant="outline_bg"
isDisabled={isSubmitting || !isDirty || !isAllowed}
isLoading={isSubmitting}
>
@ -903,17 +722,9 @@ export const SecretDetailSidebar = ({
})}
>
{(isAllowed) => (
<IconButton
colorSchema="danger"
ariaLabel="Delete Secret"
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
isDisabled={!isAllowed}
onClick={onDeleteSecret}
>
<Tooltip content="Delete Secret">
<FontAwesomeIcon icon={faTrash} />
</Tooltip>
</IconButton>
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>

@ -45,33 +45,17 @@ const formSchema = z
.object({
secretPath: z.string().default("/"),
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
targetRepo: z.object({ name: z.string(), appId: z.string().optional() }).nullish(),
targetRepo: z.object({ name: z.string(), appId: z.string() }),
targetWorkspace: z.object({ name: z.string(), slug: z.string() }),
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).nullish(),
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).optional(),
scope: z.object({ label: z.string(), value: z.nativeEnum(BitbucketScope) })
})
.superRefine((val, ctx) => {
if (!val.targetWorkspace) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetWorkspace"],
message: "Bitbucket Workspace required"
});
}
if (!val.targetRepo) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetRepo"],
message: "Bitbucket Repo required"
});
}
if (val.scope.value === BitbucketScope.Env && !val.targetEnvironment) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["targetEnvironment"],
message: "Bitbucket Deployment Environment required"
message: "Required"
});
}
});
@ -82,7 +66,7 @@ export const BitbucketConfigurePage = () => {
const navigate = useNavigate();
const createIntegration = useCreateIntegration();
const { watch, control, handleSubmit, setValue, reset } = useForm<TFormData>({
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
resolver: zodResolver(formSchema),
defaultValues: {
secretPath: "/"
@ -106,15 +90,14 @@ export const BitbucketConfigurePage = () => {
workspaceSlug: bitBucketWorkspace?.slug
});
const { data: bitbucketEnvironments, isPending: isBitbucketEnvironmentsLoading } =
useGetIntegrationAuthBitBucketEnvironments(
{
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug ?? "",
repoSlug: bitBucketRepo?.appId ?? ""
},
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
);
const { data: bitbucketEnvironments } = useGetIntegrationAuthBitBucketEnvironments(
{
integrationAuthId: (integrationAuthId as string) ?? "",
workspaceSlug: bitBucketWorkspace?.slug,
repoSlug: bitBucketRepo?.appId
},
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
);
const onSubmit = async ({
targetRepo,
@ -124,8 +107,6 @@ export const BitbucketConfigurePage = () => {
targetEnvironment,
scope
}: TFormData) => {
if (!targetRepo || !targetWorkspace) return;
try {
await createIntegration.mutateAsync({
integrationAuthId,
@ -166,14 +147,7 @@ export const BitbucketConfigurePage = () => {
};
useEffect(() => {
if (
bitBucketWorkspace ||
bitBucketRepo ||
!bitbucketRepos ||
!bitbucketWorkspaces ||
!currentWorkspace
)
return;
if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return;
reset({
targetRepo: bitbucketRepos[0],
@ -250,17 +224,12 @@ export const BitbucketConfigurePage = () => {
getOptionValue={(option) => option.slug}
value={value}
getOptionLabel={(option) => option.name}
onChange={(option) => {
onChange(option);
setValue("targetRepo", null);
setValue("targetEnvironment", null);
}}
onChange={onChange}
options={bitbucketWorkspaces}
placeholder={
bitbucketWorkspaces?.length ? "Select a workspace..." : "No workspaces found..."
}
isLoading={isBitbucketWorkspacesLoading}
isDisabled={!bitbucketWorkspaces?.length || isBitbucketWorkspacesLoading}
isDisabled={!bitbucketWorkspaces?.length}
/>
</FormControl>
)}
@ -274,16 +243,12 @@ export const BitbucketConfigurePage = () => {
getOptionValue={(option) => option.appId!}
value={value}
getOptionLabel={(option) => option.name}
onChange={(option) => {
onChange(option);
setValue("targetEnvironment", null);
}}
onChange={onChange}
options={bitbucketRepos}
placeholder={
bitbucketRepos?.length ? "Select a repository..." : "No repositories found..."
}
isLoading={isBitbucketReposLoading}
isDisabled={!bitbucketRepos?.length || isBitbucketReposLoading}
isDisabled={!bitbucketRepos?.length}
/>
</FormControl>
)}
@ -325,8 +290,7 @@ export const BitbucketConfigurePage = () => {
? "Select an environment..."
: "No environments found..."
}
isLoading={isBitbucketEnvironmentsLoading && Boolean(bitBucketRepo)}
isDisabled={!bitbucketEnvironments?.length || isBitbucketEnvironmentsLoading}
isDisabled={!bitbucketEnvironments?.length}
/>
</FormControl>
)}

@ -60,7 +60,6 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
@ -210,10 +209,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/gateways',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
@ -459,14 +454,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
getParentRoute: () => organizationLayoutRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport.update({
id: '/gateways',
path: '/gateways',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
id: '/app-connections',
@ -638,14 +625,6 @@ const organizationCertManagerOverviewPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const organizationGatewaysGatewayListPageRouteRoute =
organizationGatewaysGatewayListPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute,
} as any)
const organizationAppConnectionsAppConnectionsPageRouteRoute =
organizationAppConnectionsAppConnectionsPageRouteImport.update({
id: '/',
@ -1907,13 +1886,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
path: '/gateways'
fullPath: '/organization/gateways'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
path: '/secret-manager/$projectId'
@ -1935,13 +1907,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
path: '/'
fullPath: '/organization/gateways/'
preLoaderRoute: typeof organizationGatewaysGatewayListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
path: '/cert-manager/overview'
@ -2935,21 +2900,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren {
organizationGatewaysGatewayListPageRouteRoute: typeof organizationGatewaysGatewayListPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren =
{
organizationGatewaysGatewayListPageRouteRoute:
organizationGatewaysGatewayListPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
@ -2959,7 +2909,6 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
@ -2984,8 +2933,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
organizationCertManagerOverviewPageRouteRoute:
organizationCertManagerOverviewPageRouteRoute,
organizationGroupDetailsByIDPageRouteRoute:
@ -3661,11 +3608,9 @@ export interface FileRoutesByFullPath {
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@ -3835,7 +3780,6 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@ -4012,11 +3956,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
@ -4194,11 +4136,9 @@ export interface FileRouteTypes {
| '/cert-manager/$projectId'
| '/kms/$projectId'
| '/organization/app-connections'
| '/organization/gateways'
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections/'
| '/organization/gateways/'
| '/organization/cert-manager/overview'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
@ -4367,7 +4307,6 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId'
| '/ssh/$projectId'
| '/organization/app-connections'
| '/organization/gateways'
| '/organization/cert-manager/overview'
| '/organization/groups/$groupId'
| '/organization/identities/$identityId'
@ -4542,11 +4481,9 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
@ -4911,7 +4848,6 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
@ -4983,13 +4919,6 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/gateways": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
]
},
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout",
@ -5008,10 +4937,6 @@ export const routeTree = rootRoute
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
},
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/": {
"filePath": "organization/Gateways/GatewayListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/gateways"
},
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
"filePath": "organization/CertManagerOverviewPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"

@ -29,8 +29,7 @@ const organizationRoutes = route("/organization", [
"/$appConnection/oauth/callback",
"organization/AppConnections/OauthCallbackPage/route.tsx"
)
]),
route("/gateways", [index("organization/Gateways/GatewayListPage/route.tsx")])
])
]);
const secretManagerRoutes = route("/secret-manager/$projectId", [