feat: switched to projecet gateway, updated icon, cli

This commit is contained in:
=
2025-02-26 20:45:38 +05:30
parent 929a41065c
commit 917573931f
28 changed files with 345 additions and 115 deletions
+8
View File
@@ -206,6 +206,9 @@ import {
TProjectEnvironments, TProjectEnvironments,
TProjectEnvironmentsInsert, TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate, TProjectEnvironmentsUpdate,
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate,
TProjectKeys, TProjectKeys,
TProjectKeysInsert, TProjectKeysInsert,
TProjectKeysUpdate, TProjectKeysUpdate,
@@ -937,6 +940,11 @@ declare module "knex/types/tables" {
TKmipClientCertificatesUpdate TKmipClientCertificatesUpdate
>; >;
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>; [TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate
>;
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType< [TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
TOrgGatewayConfig, TOrgGatewayConfig,
TOrgGatewayConfigInsert, TOrgGatewayConfigInsert,
@@ -68,13 +68,29 @@ export async function up(knex: Knex): Promise<void> {
await createOnUpdateTrigger(knex, TableName.Gateway); 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)) { if (await knex.schema.hasTable(TableName.DynamicSecret)) {
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"); const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
await knex.schema.alterTable(TableName.DynamicSecret, (t) => { await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
// not setting a foreign constraint so that cascade effects are not triggered // not setting a foreign constraint so that cascade effects are not triggered
if (!doesGatewayColExist) { if (!doesGatewayColExist) {
t.uuid("gatewayId"); t.uuid("projectGatewayId");
t.foreign("gatewayId").references("id").inTable(TableName.Identity); t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
} }
}); });
} }
+1 -1
View File
@@ -27,7 +27,7 @@ export const DynamicSecretsSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
encryptedInput: zodBuffer, encryptedInput: zodBuffer,
gatewayId: z.string().uuid().nullable().optional() projectGatewayId: z.string().uuid().nullable().optional()
}); });
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>; export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
+1
View File
@@ -67,6 +67,7 @@ export * from "./pki-collection-items";
export * from "./pki-collections"; export * from "./pki-collections";
export * from "./project-bots"; export * from "./project-bots";
export * from "./project-environments"; export * from "./project-environments";
export * from "./project-gateways";
export * from "./project-keys"; export * from "./project-keys";
export * from "./project-memberships"; export * from "./project-memberships";
export * from "./project-roles"; export * from "./project-roles";
+1
View File
@@ -116,6 +116,7 @@ export enum TableName {
// Gateway // Gateway
OrgGatewayConfig = "org_gateway_config", OrgGatewayConfig = "org_gateway_config",
Gateway = "gateways", Gateway = "gateways",
ProjectGateway = "project_gateways",
// junction tables with tags // junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction", SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction", JnSecretTag = "secret_tag_junction",
@@ -0,0 +1,20 @@
// 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>>;
+45 -11
View File
@@ -17,6 +17,7 @@ const SanitizedGatewaySchema = GatewaysSchema.pick({
serialNumber: true, serialNumber: true,
heartbeat: true heartbeat: true
}); });
const isValidRelayAddress = (relayAddress: string) => { const isValidRelayAddress = (relayAddress: string) => {
const [ip, port] = relayAddress.split(":"); const [ip, port] = relayAddress.split(":");
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000; return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
@@ -120,21 +121,20 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}) }),
projects: z
.object({
name: z.string(),
id: z.string(),
slug: z.string()
})
.array()
}).array() }).array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
if (req.query.projectId) {
const gateways = await server.services.gateway.getProjectGateways({
projectId: req.query.projectId,
projectPermission: req.permission
});
return { gateways };
}
const gateways = await server.services.gateway.listGateways({ const gateways = await server.services.gateway.listGateways({
orgPermission: req.permission orgPermission: req.permission
}); });
@@ -142,6 +142,38 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
} }
}); });
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({ server.route({
method: "GET", method: "GET",
url: "/:id", url: "/:id",
@@ -184,7 +216,8 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
name: slugSchema({ field: "name" }).optional() name: slugSchema({ field: "name" }).optional(),
projectIds: z.string().array().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -197,7 +230,8 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({ const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission, orgPermission: req.permission,
id: req.params.id, id: req.params.id,
name: req.body.name name: req.body.name,
projectIds: req.body.projectIds
}); });
return { gateway }; return { gateway };
} }
@@ -16,8 +16,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal"; import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue"; import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TGatewayDALFactory } from "../gateway/gateway-dal"; import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
import { TOrgGatewayConfigDALFactory } from "../gateway/org-gateway-config-dal";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import {
DynamicSecretStatus, DynamicSecretStatus,
@@ -46,8 +45,7 @@ type TDynamicSecretServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayDAL: Pick<TGatewayDALFactory, "findOne">; projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne">;
}; };
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>; export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -62,8 +60,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService, kmsService,
gatewayDAL, projectGatewayDAL
orgGatewayConfigDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
path, path,
@@ -115,20 +112,15 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs); const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) { if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
const gatewayId = inputs.gatewayId as string; const projectGatewayId = inputs.projectGatewayId as string;
const orgGateway = await orgGatewayConfigDAL.findOne({ orgId: actorOrgId }); const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!orgGateway) if (!projectGateway)
throw new NotFoundError({ throw new NotFoundError({
message: `Gateway with ${gatewayId} not found` message: `Project gateway with ${projectGatewayId} not found`
}); });
const gateway = await gatewayDAL.findOne({ id: gatewayId, orgGatewayRootCaId: orgGateway.id }); selectedGatewayId = projectGateway.id;
if (!gateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -147,7 +139,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
name, name,
gatewayId: selectedGatewayId projectGatewayId: selectedGatewayId
}); });
return dynamicSecretCfg; return dynamicSecretCfg;
}; };
@@ -220,20 +212,20 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput); const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null; let selectedGatewayId: string | null = null;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) { if (
const gatewayId = updatedInput.gatewayId as string; updatedInput &&
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const orgGateway = await orgGatewayConfigDAL.findOne({ orgId: actorOrgId }); const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!orgGateway) if (!projectGateway)
throw new NotFoundError({ throw new NotFoundError({
message: `Gateway with ${gatewayId} not found` message: `Project gateway with ${projectGatewayId} not found`
}); });
const gateway = await gatewayDAL.findOne({ id: gatewayId, orgGatewayRootCaId: orgGateway.id }); selectedGatewayId = projectGateway.id;
if (!gateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
@@ -246,7 +238,7 @@ export const dynamicSecretServiceFactory = ({
name: newName ?? name, name: newName ?? name,
status: null, status: null,
statusDetails: null, statusDetails: null,
gatewayId: selectedGatewayId projectGatewayId: selectedGatewayId
}); });
return updatedDynamicCfg; return updatedDynamicCfg;
@@ -104,7 +104,7 @@ export const DynamicSecretSqlDBSchema = z.object({
revocationStatement: z.string().trim(), revocationStatement: z.string().trim(),
renewStatement: z.string().trim().optional(), renewStatement: z.string().trim().optional(),
ca: z.string().optional(), ca: z.string().optional(),
gatewayId: z.string().nullable().optional() projectGatewayId: z.string().nullable().optional()
}); });
export const DynamicSecretCassandraSchema = z.object({ export const DynamicSecretCassandraSchema = z.object({
@@ -34,7 +34,7 @@ type TSqlDatabaseProviderDTO = {
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => { export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => { const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs); const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId)); verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
return providerInputs; return providerInputs;
}; };
@@ -71,7 +71,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>, providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (port: number) => Promise<void> gatewayCallback: (port: number) => Promise<void>
) => { ) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.gatewayId as string); const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy( await withGatewayProxy(
async (port) => { async (port) => {
@@ -105,7 +105,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.gatewayId) { if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -138,7 +138,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
}); });
await db.destroy(); await db.destroy();
}; };
if (providerInputs.gatewayId) { if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -163,7 +163,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.gatewayId) { if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@@ -198,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.gatewayId) { if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
+43 -11
View File
@@ -1,9 +1,16 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TGateways } from "@app/db/schemas"; import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; import {
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>; export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
@@ -16,8 +23,15 @@ export const gatewayDALFactory = (db: TDbClient) => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter)) .where(buildFindFilter(filter))
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) .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(selectAllTableCols(TableName.Gateway))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName")); .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 (limit) void query.limit(limit);
if (offset) void query.offset(offset); if (offset) void query.offset(offset);
if (sort) { if (sort) {
@@ -25,7 +39,25 @@ export const gatewayDALFactory = (db: TDbClient) => {
} }
const docs = await query; const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } })); 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) { } catch (error) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` }); throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
} }
@@ -35,14 +67,14 @@ export const gatewayDALFactory = (db: TDbClient) => {
try { try {
const query = (tx || db)(TableName.Gateway) const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join( .join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
TableName.IdentityProjectMembership,
`${TableName.Identity}.id`,
`${TableName.IdentityProjectMembership}.identityId`
)
.select(selectAllTableCols(TableName.Gateway)) .select(selectAllTableCols(TableName.Gateway))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName")) .select(
.where({ [`${TableName.IdentityProjectMembership}.projectId` as "projectId"]: projectId }); 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; const docs = await query;
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } })); return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
} catch (error) { } catch (error) {
@@ -1,6 +1,6 @@
import crypto from "node:crypto"; import crypto from "node:crypto";
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { z } from "zod"; import { z } from "zod";
@@ -23,7 +23,6 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TGatewayDALFactory } from "./gateway-dal"; import { TGatewayDALFactory } from "./gateway-dal";
import { import {
TExchangeAllocatedRelayAddressDTO, TExchangeAllocatedRelayAddressDTO,
@@ -34,9 +33,11 @@ import {
TUpdateGatewayByIdDTO TUpdateGatewayByIdDTO
} from "./gateway-types"; } from "./gateway-types";
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal"; import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
type TGatewayServiceFactoryDep = { type TGatewayServiceFactoryDep = {
gatewayDAL: TGatewayDALFactory; gatewayDAL: TGatewayDALFactory;
projectGatewayDAL: TProjectGatewayDALFactory;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">; orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">; licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
@@ -56,7 +57,8 @@ export const gatewayServiceFactory = ({
kmsService, kmsService,
permissionService, permissionService,
orgGatewayConfigDAL, orgGatewayConfigDAL,
keyStore keyStore,
projectGatewayDAL
}: TGatewayServiceFactoryDep) => { }: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => { const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
if (!licenseService.onPremFeatures.gateway) { if (!licenseService.onPremFeatures.gateway) {
@@ -410,7 +412,7 @@ export const gatewayServiceFactory = ({
}).cipherTextBlob, }).cipherTextBlob,
identityId, identityId,
orgGatewayRootCaId: orgGatewayConfig.id, orgGatewayRootCaId: orgGatewayConfig.id,
name: `gateway-${alphaNumericNanoId(6)}` name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
}); });
}); });
@@ -520,7 +522,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => { const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
orgPermission.type, orgPermission.type,
orgPermission.id, orgPermission.id,
@@ -537,6 +539,16 @@ export const gatewayServiceFactory = ({
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name }); const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); 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; return gateway;
}; };
@@ -561,7 +573,7 @@ export const gatewayServiceFactory = ({
}; };
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => { const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
const { permission } = await permissionService.getProjectPermission({ await permissionService.getProjectPermission({
projectId, projectId,
actor: projectPermission.type, actor: projectPermission.type,
actorId: projectPermission.id, actorId: projectPermission.id,
@@ -571,18 +583,16 @@ export const gatewayServiceFactory = ({
}); });
const gateways = await gatewayDAL.findByProjectId(projectId); const gateways = await gatewayDAL.findByProjectId(projectId);
const allowedGateways = gateways.filter((el) => return gateways;
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: el.identityId })
)
);
return allowedGateways;
}; };
// this has no permission check and used for dynamic secrets directly // this has no permission check and used for dynamic secrets directly
// assumes permission check is already done // assumes permission check is already done
const fnGetGatewayClientTls = async (gatewayId: string) => { 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); const gateway = await gatewayDAL.findById(gatewayId);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` }); if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
@@ -20,6 +20,7 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = { export type TUpdateGatewayByIdDTO = {
id: string; id: string;
name?: string; name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor; orgPermission: OrgServiceActor;
}; };
@@ -0,0 +1,10 @@
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;
};
+5 -3
View File
@@ -30,6 +30,7 @@ import { externalKmsServiceFactory } from "@app/ee/services/external-kms/externa
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal"; import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; 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 { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -398,6 +399,7 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db); const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db); const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const permissionService = permissionServiceFactory({ const permissionService = permissionServiceFactory({
permissionDAL, permissionDAL,
@@ -1312,7 +1314,8 @@ export const registerRoutes = async (
kmsService, kmsService,
licenseService, licenseService,
orgGatewayConfigDAL, orgGatewayConfigDAL,
keyStore keyStore,
projectGatewayDAL
}); });
const dynamicSecretProviders = buildDynamicSecretProviders({ const dynamicSecretProviders = buildDynamicSecretProviders({
@@ -1336,8 +1339,7 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
gatewayDAL, projectGatewayDAL
orgGatewayConfigDAL
}); });
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
@@ -52,12 +52,32 @@ You can easily deploy a gateway using the Infisical CLI by following these steps
</Note> </Note>
</Step> </Step>
<Step title="Verification"> <Step title="Verify Gateway Deployment">
1. Check the gateway log to have **Gateway started successfully** 1. Check gateway deployment status:
- Look for the message "**Gateway started successfully**" in the gateway logs
2. Navigate to **Gateways** list page in Org Access Control - This confirms your gateway is running correctly
![list-gateway](../../../images/platform/gateways/gateway-list.png)
</Step> 2. Verify gateway registration:
- Open your Infisical dashboard
- Navigate to **Organization Access Control**
- Select the **Gateways** tab
- Your newly deployed gateway should appear in the list
![Gateway list in Organization Access Control](../../../images/platform/gateways/gateway-list.png)
</Step>
<Step title="Link Gateway to Projects">
1. Access gateway settings:
- Find your gateway in the list
- Click the options menu (⋮)
- Select **Edit Details**
![Edit gateway option](../../../images/platform/gateways/edit-gateway.png)
2. Configure project access:
- In the edit modal, you'll see a list of available projects
- Select the projects you want to grant gateway access to
- Click Save to apply your changes
![Project assignment modal](../../../images/platform/gateways/assign-project.png)
</Step>
</Steps> </Steps>
## Using Your Gateway ## Using Your Gateway
Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -12,7 +12,7 @@ export const useDeleteGatewayById = () => {
return apiRequest.delete(`/api/v1/gateways/${id}`); return apiRequest.delete(`/api/v1/gateways/${id}`);
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list({})); queryClient.invalidateQueries(gatewaysQueryKeys.list());
} }
}); });
}; };
@@ -20,11 +20,11 @@ export const useDeleteGatewayById = () => {
export const useUpdateGatewayById = () => { export const useUpdateGatewayById = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation({ return useMutation({
mutationFn: ({ id, name }: TUpdateGatewayDTO) => { mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name }); return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds });
}, },
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list({})); queryClient.invalidateQueries(gatewaysQueryKeys.list());
} }
}); });
}; };
+16 -7
View File
@@ -2,22 +2,31 @@ import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TGateway, TListGatewayDTO } from "./types"; import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types";
export const gatewaysQueryKeys = { export const gatewaysQueryKeys = {
allKey: () => ["gateways"], allKey: () => ["gateways"],
listKey: ({ projectId }: TListGatewayDTO) => [ 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(), ...gatewaysQueryKeys.allKey(),
"list", "list",
{ projectId } { projectId }
], ],
list: ({ projectId }: TListGatewayDTO = {}) => listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
queryOptions({ queryOptions({
queryKey: gatewaysQueryKeys.listKey({ projectId }), queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways", { const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
params: { projectId } `/api/v1/gateways/projects/${projectId}`
}); );
return data.gateways; return data.gateways;
} }
}) })
+24 -2
View File
@@ -11,17 +11,39 @@ export type TGateway = {
name: string; name: string;
id: 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 = { export type TUpdateGatewayDTO = {
id: string; id: string;
name?: string; name?: string;
projectIds?: string[];
}; };
export type TDeleteGatewayDTO = { export type TDeleteGatewayDTO = {
id: string; id: string;
}; };
export type TListGatewayDTO = { export type TListProjectGatewayDTO = {
projectId?: string; projectId: string;
}; };
@@ -103,7 +103,7 @@ export const OrganizationLayout = () => {
</Link> </Link>
<Link to="/organization/gateways"> <Link to="/organization/gateways">
{({ isActive }) => ( {({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle"> <MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
Gateways Gateways
</MenuItem> </MenuItem>
)} )}
@@ -32,6 +32,7 @@ import {
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
Tag,
TBody, TBody,
Td, Td,
Th, Th,
@@ -127,6 +128,7 @@ export const GatewayListPage = withPermission(
<Tr> <Tr>
<Th className="w-1/3">Name</Th> <Th className="w-1/3">Name</Th>
<Th>Cert Issued At</Th> <Th>Cert Issued At</Th>
<Th>Projects</Th>
<Th>Identity</Th> <Th>Identity</Th>
<Th> <Th>
Health Check Health Check
@@ -149,6 +151,13 @@ export const GatewayListPage = withPermission(
<Tr key={el.id}> <Tr key={el.id}>
<Td>{el.name}</Td> <Td>{el.name}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</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.identity.name}</Td>
<Td> <Td>
{el.heartbeat {el.heartbeat
@@ -3,9 +3,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2"; import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
import { useUpdateGatewayById } from "@app/hooks/api"; import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api";
import { TGateway } from "@app/hooks/api/gateways/types"; import { TGateway } from "@app/hooks/api/gateways/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
type Props = { type Props = {
gatewayDetails: TGateway; gatewayDetails: TGateway;
@@ -13,7 +14,13 @@ type Props = {
}; };
const schema = z.object({ const schema = z.object({
name: z.string() name: z.string(),
projects: z
.object({
id: z.string(),
name: z.string()
})
.array()
}); });
export type FormData = z.infer<typeof schema>; export type FormData = z.infer<typeof schema>;
@@ -31,13 +38,20 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
}); });
const updateGatewayById = useUpdateGatewayById(); const updateGatewayById = useUpdateGatewayById();
// when gateway goes to other products switch to all
const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces(
{
type: ProjectType.SecretManager
}
);
const onFormSubmit = ({ name }: FormData) => { const onFormSubmit = ({ name, projects }: FormData) => {
if (isSubmitting) return; if (isSubmitting) return;
updateGatewayById.mutate( updateGatewayById.mutate(
{ {
id: gatewayDetails.id, id: gatewayDetails.id,
name name,
projectIds: projects.map((el) => el.id)
}, },
{ {
onSuccess: () => { onSuccess: () => {
@@ -62,6 +76,30 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="projects"
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl
className="w-full"
label="Projects"
tooltipText="Select the project that you wish to assign to the gateway."
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"> <div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}> <Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
Update Update
@@ -74,7 +74,7 @@ type Props = {
title: string; title: string;
formName: keyof Omit< formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>, Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console" | "kmip" "workspace" | "organization-admin-console" | "kmip" | "gateway"
>; >;
setValue: UseFormSetValue<TFormSchema>; setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>; control: Control<TFormSchema>;
@@ -143,7 +143,7 @@ export const SqlDatabaseInputForm = ({
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.list({ projectId: currentWorkspace.id }) gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
); );
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
@@ -36,7 +36,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1), revocationStatement: z.string().min(1),
renewStatement: z.string().optional(), renewStatement: z.string().optional(),
ca: z.string().optional(), ca: z.string().optional(),
gatewayId: z.string().optional() projectGatewayId: z.string().optional()
}) })
.partial(), .partial(),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
@@ -100,12 +100,13 @@ export const EditDynamicSecretSqlProviderForm = ({
}); });
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.list({ projectId: currentWorkspace.id }) gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
); );
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
const selectedGatewayId = watch("inputs.gatewayId"); const selectedProjectGatewayId = watch("inputs.projectGatewayId");
const isGatewayInActive = projectGateways?.findIndex((el) => el.id === selectedGatewayId) === -1; const isGatewayInActive =
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
// wait till previous request is finished // wait till previous request is finished
@@ -119,7 +120,10 @@ export const EditDynamicSecretSqlProviderForm = ({
data: { data: {
maxTTL: maxTTL || undefined, maxTTL: maxTTL || undefined,
defaultTTL, defaultTTL,
inputs: { ...inputs, gatewayId: isGatewayInActive ? null : inputs.gatewayId }, inputs: {
...inputs,
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
},
newName: newName === dynamicSecret.name ? undefined : newName newName: newName === dynamicSecret.name ? undefined : newName
} }
}); });
@@ -189,14 +193,14 @@ export const EditDynamicSecretSqlProviderForm = ({
<div> <div>
<Controller <Controller
control={control} control={control}
name="inputs.gatewayId" name="inputs.projectGatewayId"
defaultValue="" defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
isError={Boolean(error?.message) || isGatewayInActive} isError={Boolean(error?.message) || isGatewayInActive}
errorText={ errorText={
isGatewayInActive && selectedGatewayId isGatewayInActive && selectedProjectGatewayId
? `Gateway ${selectedGatewayId} is removed` ? `Project Gateway ${selectedProjectGatewayId} is removed`
: error?.message : error?.message
} }
label="Gateway" label="Gateway"
@@ -215,7 +219,7 @@ export const EditDynamicSecretSqlProviderForm = ({
Internet Gateway Internet Gateway
</SelectItem> </SelectItem>
{projectGateways?.map((el) => ( {projectGateways?.map((el) => (
<SelectItem value={el.id} key={el.id}> <SelectItem value={el.projectGatewayId} key={el.id}>
{el.name} {el.name}
</SelectItem> </SelectItem>
))} ))}