feat: switched to projecet gateway, updated icon, cli

This commit is contained in:
=
2025-02-21 23:39:35 +05:30
parent 929a41065c
commit 917573931f
28 changed files with 345 additions and 115 deletions

View File

@@ -206,6 +206,9 @@ import {
TProjectEnvironments,
TProjectEnvironmentsInsert,
TProjectEnvironmentsUpdate,
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate,
TProjectKeys,
TProjectKeysInsert,
TProjectKeysUpdate,
@@ -937,6 +940,11 @@ declare module "knex/types/tables" {
TKmipClientCertificatesUpdate
>;
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
TProjectGateways,
TProjectGatewaysInsert,
TProjectGatewaysUpdate
>;
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
TOrgGatewayConfig,
TOrgGatewayConfigInsert,

View File

@@ -68,13 +68,29 @@ export async function up(knex: Knex): Promise<void> {
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("gatewayId");
t.foreign("gatewayId").references("id").inTable(TableName.Identity);
t.uuid("projectGatewayId");
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
}
});
}

View File

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

View File

@@ -67,6 +67,7 @@ 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";

View File

@@ -116,6 +116,7 @@ export enum TableName {
// Gateway
OrgGatewayConfig = "org_gateway_config",
Gateway = "gateways",
ProjectGateway = "project_gateways",
// junction tables with tags
SecretV2JnTag = "secret_v2_tag_junction",
JnSecretTag = "secret_tag_junction",

View File

@@ -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>>;

View File

@@ -17,6 +17,7 @@ const SanitizedGatewaySchema = GatewaysSchema.pick({
serialNumber: true,
heartbeat: true
});
const isValidRelayAddress = (relayAddress: string) => {
const [ip, port] = relayAddress.split(":");
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
@@ -120,21 +121,20 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
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) => {
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({
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({
method: "GET",
url: "/:id",
@@ -184,7 +216,8 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
id: z.string()
}),
body: z.object({
name: slugSchema({ field: "name" }).optional()
name: slugSchema({ field: "name" }).optional(),
projectIds: z.string().array().optional()
}),
response: {
200: z.object({
@@ -197,7 +230,8 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
const gateway = await server.services.gateway.updateGatewayById({
orgPermission: req.permission,
id: req.params.id,
name: req.body.name
name: req.body.name,
projectIds: req.body.projectIds
});
return { gateway };
}

View File

@@ -16,8 +16,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { TOrgGatewayConfigDALFactory } from "../gateway/org-gateway-config-dal";
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,
@@ -46,8 +45,7 @@ type TDynamicSecretServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayDAL: Pick<TGatewayDALFactory, "findOne">;
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
};
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
@@ -62,8 +60,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService,
projectDAL,
kmsService,
gatewayDAL,
orgGatewayConfigDAL
projectGatewayDAL
}: TDynamicSecretServiceFactoryDep) => {
const create = async ({
path,
@@ -115,20 +112,15 @@ export const dynamicSecretServiceFactory = ({
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
let selectedGatewayId: string | null = null;
if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const gatewayId = inputs.gatewayId as string;
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
const projectGatewayId = inputs.projectGatewayId as string;
const orgGateway = await orgGatewayConfigDAL.findOne({ orgId: actorOrgId });
if (!orgGateway)
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
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 });
if (!gateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
selectedGatewayId = gateway.id;
selectedGatewayId = projectGateway.id;
}
const isConnected = await selectedProvider.validateConnection(provider.inputs);
@@ -147,7 +139,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL,
folderId: folder.id,
name,
gatewayId: selectedGatewayId
projectGatewayId: selectedGatewayId
});
return dynamicSecretCfg;
};
@@ -220,20 +212,20 @@ export const dynamicSecretServiceFactory = ({
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
let selectedGatewayId: string | null = null;
if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
const gatewayId = updatedInput.gatewayId as string;
if (
updatedInput &&
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const orgGateway = await orgGatewayConfigDAL.findOne({ orgId: actorOrgId });
if (!orgGateway)
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
if (!projectGateway)
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 });
if (!gateway)
throw new NotFoundError({
message: `Gateway with ${gatewayId} not found`
});
selectedGatewayId = gateway.id;
selectedGatewayId = projectGateway.id;
}
const isConnected = await selectedProvider.validateConnection(newInput);
@@ -246,7 +238,7 @@ export const dynamicSecretServiceFactory = ({
name: newName ?? name,
status: null,
statusDetails: null,
gatewayId: selectedGatewayId
projectGatewayId: selectedGatewayId
});
return updatedDynamicCfg;

View File

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

View File

@@ -34,7 +34,7 @@ type TSqlDatabaseProviderDTO = {
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
const validateProviderInputs = async (inputs: unknown) => {
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
return providerInputs;
};
@@ -71,7 +71,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
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(":");
await withGatewayProxy(
async (port) => {
@@ -105,7 +105,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
};
if (providerInputs.gatewayId) {
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -138,7 +138,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
});
await db.destroy();
};
if (providerInputs.gatewayId) {
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -163,7 +163,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
};
if (providerInputs.gatewayId) {
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();
@@ -198,7 +198,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy();
};
if (providerInputs.gatewayId) {
if (providerInputs.projectGatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else {
await gatewayCallback();

View File

@@ -1,9 +1,16 @@
import { Knex } from "knex";
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 { 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>;
@@ -16,8 +23,15 @@ export const gatewayDALFactory = (db: TDbClient) => {
// 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"));
.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) {
@@ -25,7 +39,25 @@ export const gatewayDALFactory = (db: TDbClient) => {
}
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) {
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
}
@@ -35,14 +67,14 @@ export const gatewayDALFactory = (db: TDbClient) => {
try {
const query = (tx || db)(TableName.Gateway)
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
.join(
TableName.IdentityProjectMembership,
`${TableName.Identity}.id`,
`${TableName.IdentityProjectMembership}.identityId`
)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
.select(selectAllTableCols(TableName.Gateway))
.select(db.ref("name").withSchema(TableName.Identity).as("identityName"))
.where({ [`${TableName.IdentityProjectMembership}.projectId` as "projectId"]: projectId });
.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) {

View File

@@ -1,6 +1,6 @@
import crypto from "node:crypto";
import { ForbiddenError, subject } from "@casl/ability";
import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509";
import { z } from "zod";
@@ -23,7 +23,6 @@ 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 { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
import { TGatewayDALFactory } from "./gateway-dal";
import {
TExchangeAllocatedRelayAddressDTO,
@@ -34,9 +33,11 @@ import {
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">;
@@ -56,7 +57,8 @@ export const gatewayServiceFactory = ({
kmsService,
permissionService,
orgGatewayConfigDAL,
keyStore
keyStore,
projectGatewayDAL
}: TGatewayServiceFactoryDep) => {
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
if (!licenseService.onPremFeatures.gateway) {
@@ -410,7 +412,7 @@ export const gatewayServiceFactory = ({
}).cipherTextBlob,
identityId,
orgGatewayRootCaId: orgGatewayConfig.id,
name: `gateway-${alphaNumericNanoId(6)}`
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
});
});
@@ -520,7 +522,7 @@ export const gatewayServiceFactory = ({
return gateway;
};
const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
@@ -537,6 +539,16 @@ export const gatewayServiceFactory = ({
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;
};
@@ -561,7 +573,7 @@ export const gatewayServiceFactory = ({
};
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
const { permission } = await permissionService.getProjectPermission({
await permissionService.getProjectPermission({
projectId,
actor: projectPermission.type,
actorId: projectPermission.id,
@@ -571,18 +583,16 @@ export const gatewayServiceFactory = ({
});
const gateways = await gatewayDAL.findByProjectId(projectId);
const allowedGateways = gateways.filter((el) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: el.identityId })
)
);
return allowedGateways;
return gateways;
};
// this has no permission check and used for dynamic secrets directly
// 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);
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });

View File

@@ -20,6 +20,7 @@ export type TGetGatewayByIdDTO = {
export type TUpdateGatewayByIdDTO = {
id: string;
name?: string;
projectIds?: string[];
orgPermission: OrgServiceActor;
};

View File

@@ -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;
};

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 { 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";
@@ -398,6 +399,7 @@ export const registerRoutes = async (
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const permissionService = permissionServiceFactory({
permissionDAL,
@@ -1312,7 +1314,8 @@ export const registerRoutes = async (
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
keyStore,
projectGatewayDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({
@@ -1336,8 +1339,7 @@ export const registerRoutes = async (
permissionService,
licenseService,
kmsService,
gatewayDAL,
orgGatewayConfigDAL
projectGatewayDAL
});
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({

View File

@@ -52,12 +52,32 @@ You can easily deploy a gateway using the Infisical CLI by following these steps
</Note>
</Step>
<Step title="Verification">
1. Check the gateway log to have **Gateway started successfully**
2. Navigate to **Gateways** list page in Org Access Control
![list-gateway](../../../images/platform/gateways/gateway-list.png)
</Step>
<Step title="Verify Gateway Deployment">
1. Check gateway deployment status:
- Look for the message "**Gateway started successfully**" in the gateway logs
- This confirms your gateway is running correctly
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>
## 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

View File

@@ -12,7 +12,7 @@ export const useDeleteGatewayById = () => {
return apiRequest.delete(`/api/v1/gateways/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list({}));
queryClient.invalidateQueries(gatewaysQueryKeys.list());
}
});
};
@@ -20,11 +20,11 @@ export const useDeleteGatewayById = () => {
export const useUpdateGatewayById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, name }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name });
mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => {
return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds });
},
onSuccess: () => {
queryClient.invalidateQueries(gatewaysQueryKeys.list({}));
queryClient.invalidateQueries(gatewaysQueryKeys.list());
}
});
};

View File

@@ -2,22 +2,31 @@ import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TGateway, TListGatewayDTO } from "./types";
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types";
export const gatewaysQueryKeys = {
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(),
"list",
{ projectId }
],
list: ({ projectId }: TListGatewayDTO = {}) =>
listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
queryOptions({
queryKey: gatewaysQueryKeys.listKey({ projectId }),
queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
queryFn: async () => {
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways", {
params: { projectId }
});
const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
`/api/v1/gateways/projects/${projectId}`
);
return data.gateways;
}
})

View File

@@ -11,17 +11,39 @@ export type TGateway = {
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 TListGatewayDTO = {
projectId?: string;
export type TListProjectGatewayDTO = {
projectId: string;
};

View File

@@ -103,7 +103,7 @@ export const OrganizationLayout = () => {
</Link>
<Link to="/organization/gateways">
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="jigsaw-puzzle">
<MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
Gateways
</MenuItem>
)}

View File

@@ -32,6 +32,7 @@ import {
Table,
TableContainer,
TableSkeleton,
Tag,
TBody,
Td,
Th,
@@ -127,6 +128,7 @@ export const GatewayListPage = withPermission(
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Cert Issued At</Th>
<Th>Projects</Th>
<Th>Identity</Th>
<Th>
Health Check
@@ -149,6 +151,13 @@ export const GatewayListPage = withPermission(
<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

View File

@@ -3,9 +3,10 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUpdateGatewayById } from "@app/hooks/api";
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;
@@ -13,7 +14,13 @@ type Props = {
};
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>;
@@ -31,13 +38,20 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
});
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;
updateGatewayById.mutate(
{
id: gatewayDetails.id,
name
name,
projectIds: projects.map((el) => el.id)
},
{
onSuccess: () => {
@@ -62,6 +76,30 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
</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">
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
Update

View File

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

View File

@@ -143,7 +143,7 @@ export const SqlDatabaseInputForm = ({
const createDynamicSecret = useCreateDynamicSecret();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.list({ projectId: currentWorkspace.id })
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {

View File

@@ -36,7 +36,7 @@ const formSchema = z.object({
revocationStatement: z.string().min(1),
renewStatement: z.string().optional(),
ca: z.string().optional(),
gatewayId: z.string().optional()
projectGatewayId: z.string().optional()
})
.partial(),
defaultTTL: z.string().superRefine((val, ctx) => {
@@ -100,12 +100,13 @@ export const EditDynamicSecretSqlProviderForm = ({
});
const { currentWorkspace } = useWorkspace();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.list({ projectId: currentWorkspace.id })
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const updateDynamicSecret = useUpdateDynamicSecret();
const selectedGatewayId = watch("inputs.gatewayId");
const isGatewayInActive = projectGateways?.findIndex((el) => el.id === selectedGatewayId) === -1;
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
@@ -119,7 +120,10 @@ export const EditDynamicSecretSqlProviderForm = ({
data: {
maxTTL: maxTTL || undefined,
defaultTTL,
inputs: { ...inputs, gatewayId: isGatewayInActive ? null : inputs.gatewayId },
inputs: {
...inputs,
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
},
newName: newName === dynamicSecret.name ? undefined : newName
}
});
@@ -189,14 +193,14 @@ export const EditDynamicSecretSqlProviderForm = ({
<div>
<Controller
control={control}
name="inputs.gatewayId"
name="inputs.projectGatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message) || isGatewayInActive}
errorText={
isGatewayInActive && selectedGatewayId
? `Gateway ${selectedGatewayId} is removed`
isGatewayInActive && selectedProjectGatewayId
? `Project Gateway ${selectedProjectGatewayId} is removed`
: error?.message
}
label="Gateway"
@@ -215,7 +219,7 @@ export const EditDynamicSecretSqlProviderForm = ({
Internet Gateway
</SelectItem>
{projectGateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
<SelectItem value={el.projectGatewayId} key={el.id}>
{el.name}
</SelectItem>
))}