Compare commits

..

19 Commits

Author SHA1 Message Date
be26dc9872 requested changes 2025-05-15 16:55:36 +04:00
aaeb6e73fe requested changes 2025-05-15 16:06:20 +04:00
cd028ae133 Update 20250212191958_create-gateway.ts 2025-05-14 16:01:07 +04:00
63c71fabcd fix: migrate project gateway 2025-05-14 16:00:27 +04:00
e90166f1f0 Merge branch 'heads/main' into daniel/k8s-auth-gateway 2025-05-14 14:26:05 +04:00
5a3fbc0401 Merge pull request #3599 from Infisical/misc/updated-custom-cert-to-be-crt-formawt
misc: update custom cert to be crt format for docs
2025-05-14 14:24:29 +08:00
7c52e000cd misc: update custom cert to be crt format for docs 2025-05-14 14:12:08 +08:00
2dd407b136 Merge pull request #3596 from Infisical/pulumi-documentation-update
Adding Pulumi documentation
2025-05-13 22:21:33 -06:00
d397002704 Update pulumi.mdx 2025-05-13 20:29:06 -06:00
f5b1f671e3 Update pulumi.mdx 2025-05-13 20:17:23 -06:00
0597c5f0c0 Adding Pulumi documentation 2025-05-13 20:14:08 -06:00
eb3afc8034 Merge pull request #3595 from Infisical/remove-legacy-native-integrations-notice
improvement(native-integrations): Remove legacy badge/banner from native integrations UI
2025-05-13 18:51:03 -07:00
b67457fe93 chore: remove unused imports 2025-05-13 18:46:53 -07:00
75abdbe938 remove legacy badge/banner from native integrations UI 2025-05-13 18:41:14 -07:00
8adf4787b9 Update 20250513081738_remove-gateway-project-link.ts 2025-05-13 15:31:13 +04:00
a12522db55 requested changes 2025-05-13 15:18:23 +04:00
49ab487dc2 Update organization-permissions.mdx 2025-05-13 15:04:21 +04:00
daf0731580 feat(gateways): decouple gateways from projects 2025-05-13 14:59:58 +04:00
fb2b64cb19 feat(identities/k8s): gateway support 2025-05-12 15:19:42 +04:00
41 changed files with 760 additions and 471 deletions

View File

@ -0,0 +1,25 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (!hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
const hasGatewayIdColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "gatewayId");
if (hasGatewayIdColumn) {
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}

View File

@ -0,0 +1,110 @@
import { Knex } from "knex";
import { inMemoryKeyStore } from "@app/keystore/memory";
import { selectAllTableCols } from "@app/lib/knex";
import { initLogger } from "@app/lib/logger";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TableName } from "../schemas";
import { getMigrationEnvConfig } from "./utils/env-config";
import { getMigrationEncryptionServices } from "./utils/services";
// Note(daniel): We aren't dropping tables or columns in this migrations so we can easily rollback if needed.
// In the future we need to drop the projectGatewayId on the dynamic secrets table, and drop the project_gateways table entirely.
const BATCH_SIZE = 500;
export async function up(knex: Knex): Promise<void> {
// eslint-disable-next-line no-param-reassign
knex.replicaNode = () => {
return knex;
};
if (!(await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId"))) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.uuid("gatewayId").nullable();
table.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("SET NULL");
table.index("gatewayId");
});
const existingDynamicSecretsWithProjectGatewayId = await knex(TableName.DynamicSecret)
.select(selectAllTableCols(TableName.DynamicSecret))
.whereNotNull(`${TableName.DynamicSecret}.projectGatewayId`)
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.id`, `${TableName.DynamicSecret}.projectGatewayId`)
.whereNotNull(`${TableName.ProjectGateway}.gatewayId`)
.select(
knex.ref("projectId").withSchema(TableName.ProjectGateway).as("projectId"),
knex.ref("gatewayId").withSchema(TableName.ProjectGateway).as("projectGatewayGatewayId")
);
initLogger();
const envConfig = getMigrationEnvConfig();
const keyStore = inMemoryKeyStore();
const { kmsService } = await getMigrationEncryptionServices({ envConfig, keyStore, db: knex });
const updatedDynamicSecrets = await Promise.all(
existingDynamicSecretsWithProjectGatewayId.map(async (existingDynamicSecret) => {
if (!existingDynamicSecret.projectGatewayGatewayId) {
const result = {
...existingDynamicSecret,
gatewayId: null
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
}
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: existingDynamicSecret.projectId
});
let decryptedStoredInput = JSON.parse(
secretManagerDecryptor({ cipherTextBlob: Buffer.from(existingDynamicSecret.encryptedInput) }).toString()
) as object;
// We're not removing the existing projectGatewayId from the input so we can easily rollback without having to re-encrypt the input
decryptedStoredInput = {
...decryptedStoredInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const encryptedInput = secretManagerEncryptor({
plainText: Buffer.from(JSON.stringify(decryptedStoredInput))
}).cipherTextBlob;
const result = {
...existingDynamicSecret,
encryptedInput,
gatewayId: existingDynamicSecret.projectGatewayGatewayId
};
const { projectId, projectGatewayGatewayId, ...rest } = result;
return rest;
})
);
for (let i = 0; i < updatedDynamicSecrets.length; i += BATCH_SIZE) {
// eslint-disable-next-line no-await-in-loop
await knex(TableName.DynamicSecret)
.insert(updatedDynamicSecrets.slice(i, i + BATCH_SIZE))
.onConflict("id")
.merge();
}
}
}
export async function down(knex: Knex): Promise<void> {
// no re-encryption needed as we keep the old projectGatewayId in the input
if (await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId")) {
await knex.schema.alterTable(TableName.DynamicSecret, (table) => {
table.dropForeign("gatewayId");
table.dropColumn("gatewayId");
});
}
}

View File

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

View File

@ -29,7 +29,8 @@ export const IdentityKubernetesAuthsSchema = z.object({
allowedNames: z.string(), allowedNames: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(), encryptedKubernetesTokenReviewerJwt: zodBuffer.nullable().optional(),
encryptedKubernetesCaCertificate: zodBuffer.nullable().optional() encryptedKubernetesCaCertificate: zodBuffer.nullable().optional(),
gatewayId: z.string().uuid().nullable().optional()
}); });
export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>; export type TIdentityKubernetesAuths = z.infer<typeof IdentityKubernetesAuthsSchema>;

View File

@ -121,14 +121,7 @@ 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()
}) })
} }
@ -158,17 +151,15 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => {
identity: z.object({ identity: z.object({
name: z.string(), name: z.string(),
id: z.string() id: z.string()
}), })
projectGatewayId: z.string()
}).array() }).array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]), onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const gateways = await server.services.gateway.getProjectGateways({ const gateways = await server.services.gateway.listGateways({
projectId: req.params.projectId, orgPermission: req.permission
projectPermission: req.permission
}); });
return { gateways }; return { gateways };
} }
@ -216,8 +207,7 @@ 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({
@ -230,8 +220,7 @@ 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 };
} }

View File

@ -17,7 +17,8 @@ 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 { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal"; import { TGatewayDALFactory } from "../gateway/gateway-dal";
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal"; import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import { import {
DynamicSecretStatus, DynamicSecretStatus,
@ -44,9 +45,9 @@ type TDynamicSecretServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">; folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findBySecretPathMultiEnv">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">; projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">; gatewayDAL: Pick<TGatewayDALFactory, "findOne" | "find">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
}; };
@ -62,7 +63,7 @@ export const dynamicSecretServiceFactory = ({
dynamicSecretQueueService, dynamicSecretQueueService,
projectDAL, projectDAL,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}: TDynamicSecretServiceFactoryDep) => { }: TDynamicSecretServiceFactoryDep) => {
const create = async ({ const create = async ({
@ -117,15 +118,31 @@ 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" && "projectGatewayId" in inputs && inputs.projectGatewayId) { if (inputs && typeof inputs === "object" && "gatewayId" in inputs && inputs.gatewayId) {
const projectGatewayId = inputs.projectGatewayId as string; const gatewayId = inputs.gatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!projectGateway)
if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(provider.inputs); const isConnected = await selectedProvider.validateConnection(provider.inputs);
@ -146,7 +163,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
folderId: folder.id, folderId: folder.id,
name, name,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );
@ -255,20 +272,30 @@ 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 ( if (updatedInput && typeof updatedInput === "object" && "gatewayId" in updatedInput && updatedInput?.gatewayId) {
updatedInput && const gatewayId = updatedInput.gatewayId as string;
typeof updatedInput === "object" &&
"projectGatewayId" in updatedInput &&
updatedInput?.projectGatewayId
) {
const projectGatewayId = updatedInput.projectGatewayId as string;
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId }); const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: actorOrgId });
if (!projectGateway) if (!gateway) {
throw new NotFoundError({ throw new NotFoundError({
message: `Project gateway with ${projectGatewayId} not found` message: `Gateway with ID ${gatewayId} not found`
}); });
selectedGatewayId = projectGateway.id; }
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
gateway.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
selectedGatewayId = gateway.id;
} }
const isConnected = await selectedProvider.validateConnection(newInput); const isConnected = await selectedProvider.validateConnection(newInput);
@ -284,7 +311,7 @@ export const dynamicSecretServiceFactory = ({
defaultTTL, defaultTTL,
name: newName ?? name, name: newName ?? name,
status: null, status: null,
projectGatewayId: selectedGatewayId gatewayId: selectedGatewayId
}, },
tx tx
); );

View File

@ -18,7 +18,7 @@ import { SqlDatabaseProvider } from "./sql-database";
import { TotpProvider } from "./totp"; import { TotpProvider } from "./totp";
type TBuildDynamicSecretProviderDTO = { type TBuildDynamicSecretProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
export const buildDynamicSecretProviders = ({ export const buildDynamicSecretProviders = ({

View File

@ -137,7 +137,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(),
projectGatewayId: z.string().nullable().optional() gatewayId: z.string().nullable().optional()
}); });
export const DynamicSecretCassandraSchema = z.object({ export const DynamicSecretCassandraSchema = z.object({

View File

@ -112,14 +112,14 @@ const generateUsername = (provider: SqlProviders) => {
}; };
type TSqlDatabaseProviderDTO = { type TSqlDatabaseProviderDTO = {
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">; gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
}; };
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);
const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId)); const [hostIp] = await verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.gatewayId));
validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, { validateHandlebarTemplate("SQL creation", providerInputs.creationStatement, {
allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val) allowedExpressions: (val) => ["username", "password", "expiration", "database"].includes(val)
}); });
@ -168,7 +168,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>, providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
gatewayCallback: (host: string, port: number) => Promise<void> gatewayCallback: (host: string, port: number) => Promise<void>
) => { ) => {
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string); const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(providerInputs.gatewayId as string);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":"); const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
await withGatewayProxy( await withGatewayProxy(
async (port) => { async (port) => {
@ -202,7 +202,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@ -238,7 +238,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@ -265,7 +265,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();
@ -301,7 +301,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
await db.destroy(); await db.destroy();
} }
}; };
if (providerInputs.projectGatewayId) { if (providerInputs.gatewayId) {
await gatewayProxyWrapper(providerInputs, gatewayCallback); await gatewayProxyWrapper(providerInputs, gatewayCallback);
} else { } else {
await gatewayCallback(); await gatewayCallback();

View File

@ -1,37 +1,34 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { GatewaysSchema, 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 { import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
buildFindFilter,
ormify,
selectAllTableCols,
sqlNestRelationships,
TFindFilter,
TFindOpt
} from "@app/lib/knex";
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>; export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
export const gatewayDALFactory = (db: TDbClient) => { export const gatewayDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.Gateway); const orm = ormify(db, TableName.Gateway);
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => { const find = async (
filter: TFindFilter<TGateways> & { orgId?: string },
{ offset, limit, sort, tx }: TFindOpt<TGateways> = {}
) => {
try { try {
const query = (tx || db)(TableName.Gateway) const query = (tx || db)(TableName.Gateway)
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter)) .where(buildFindFilter(filter, TableName.Gateway, ["orgId"]))
.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`) .join(
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`) TableName.IdentityOrgMembership,
`${TableName.IdentityOrgMembership}.identityId`,
`${TableName.Gateway}.identityId`
)
.select(selectAllTableCols(TableName.Gateway)) .select(selectAllTableCols(TableName.Gateway))
.select( .select(db.ref("orgId").withSchema(TableName.IdentityOrgMembership).as("identityOrgId"))
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"), if (filter.orgId) {
db.ref("id").withSchema(TableName.Project).as("projectId") void query.where(`${TableName.IdentityOrgMembership}.orgId`, filter.orgId);
); }
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) {
@ -39,48 +36,16 @@ export const gatewayDALFactory = (db: TDbClient) => {
} }
const docs = await query; const docs = await query;
return sqlNestRelationships({
data: docs, return docs.map((el) => ({
key: "id", ...GatewaysSchema.parse(el),
parentMapper: (data) => ({ orgId: el.identityOrgId as string, // todo(daniel): figure out why typescript is not inferring this as a string
...GatewaysSchema.parse(data), identity: { id: el.identityId, name: el.identityName }
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` });
} }
}; };
const findByProjectId = async (projectId: string, tx?: Knex) => { return { ...orm, find };
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 };
}; };

View File

@ -4,7 +4,6 @@ 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";
import { ActionProjectType } from "@app/db/schemas";
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore"; import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@ -27,17 +26,14 @@ import { TGatewayDALFactory } from "./gateway-dal";
import { import {
TExchangeAllocatedRelayAddressDTO, TExchangeAllocatedRelayAddressDTO,
TGetGatewayByIdDTO, TGetGatewayByIdDTO,
TGetProjectGatewayByIdDTO,
THeartBeatDTO, THeartBeatDTO,
TListGatewaysDTO, TListGatewaysDTO,
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">;
@ -57,8 +53,7 @@ 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) {
@ -526,7 +521,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => { const updateGatewayById = async ({ orgPermission, id, name }: TUpdateGatewayByIdDTO) => {
const { permission } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
orgPermission.type, orgPermission.type,
orgPermission.id, orgPermission.id,
@ -543,15 +538,6 @@ 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;
}; };
@ -576,27 +562,7 @@ export const gatewayServiceFactory = ({
return gateway; return gateway;
}; };
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => { const fnGetGatewayClientTlsByGatewayId = async (gatewayId: string) => {
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); 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.` });
@ -645,8 +611,7 @@ export const gatewayServiceFactory = ({
getGatewayById, getGatewayById,
updateGatewayById, updateGatewayById,
deleteGatewayById, deleteGatewayById,
getProjectGateways, fnGetGatewayClientTlsByGatewayId,
fnGetGatewayClientTls,
heartbeat heartbeat
}; };
}; };

View File

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

View File

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

View File

@ -41,7 +41,8 @@ export enum OrgPermissionGatewayActions {
CreateGateways = "create-gateways", CreateGateways = "create-gateways",
ListGateways = "list-gateways", ListGateways = "list-gateways",
EditGateways = "edit-gateways", EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
} }
export enum OrgPermissionIdentityActions { export enum OrgPermissionIdentityActions {
@ -337,6 +338,7 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
@ -378,6 +380,7 @@ const buildMemberPermission = () => {
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
return rules; return rules;
}; };

View File

@ -358,6 +358,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience: allowedAudience:
"The optional audience claim that the service account JWT token must have to authenticate with Infisical.", "The optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.", accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@ -374,6 +375,7 @@ export const KUBERNETES_AUTH = {
allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.", allowedNames: "The new comma-separated list of trusted service account names that can authenticate with Infisical.",
allowedAudience: allowedAudience:
"The new optional audience claim that the service account JWT token must have to authenticate with Infisical.", "The new optional audience claim that the service account JWT token must have to authenticate with Infisical.",
gatewayId: "The ID of the gateway to use when performing kubernetes API requests.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.", accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an acccess token in seconds.", accessTokenTTL: "The new lifetime for an acccess token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an acccess token in seconds.",

View File

@ -174,6 +174,8 @@ const setupProxyServer = async ({
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = net.createServer(); const server = net.createServer();
let streamClosed = false;
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
server.on("connection", async (clientConn) => { server.on("connection", async (clientConn) => {
try { try {
@ -202,9 +204,15 @@ const setupProxyServer = async ({
// Handle client connection close // Handle client connection close
clientConn.on("end", () => { clientConn.on("end", () => {
writer.close().catch((err) => { if (!streamClosed) {
logger.error(err); try {
}); writer.close().catch((err) => {
logger.debug(err, "Error closing writer (already closed)");
});
} catch (error) {
logger.debug(error, "Error in writer close");
}
}
}); });
clientConn.on("error", (clientConnErr) => { clientConn.on("error", (clientConnErr) => {
@ -249,14 +257,29 @@ const setupProxyServer = async ({
setupCopy(); setupCopy();
// Handle connection closure // Handle connection closure
clientConn.on("close", () => { clientConn.on("close", () => {
stream.destroy().catch((err) => { if (!streamClosed) {
proxyErrorMsg.push((err as Error)?.message); streamClosed = true;
}); stream.destroy().catch((err) => {
logger.debug(err, "Stream already destroyed during close event");
});
}
}); });
const cleanup = async () => { const cleanup = async () => {
clientConn?.destroy(); try {
await stream.destroy(); clientConn?.destroy();
} catch (err) {
logger.debug(err, "Error destroying client connection");
}
if (!streamClosed) {
streamClosed = true;
try {
await stream.destroy();
} catch (err) {
logger.debug(err, "Error destroying stream (might be already closed)");
}
}
}; };
clientConn.on("error", (clientConnErr) => { clientConn.on("error", (clientConnErr) => {
@ -301,8 +324,17 @@ const setupProxyServer = async ({
server, server,
port: address.port, port: address.port,
cleanup: async () => { cleanup: async () => {
server.close(); try {
await quicClient?.destroy(); server.close();
} catch (err) {
logger.debug(err, "Error closing server");
}
try {
await quicClient?.destroy();
} catch (err) {
logger.debug(err, "Error destroying QUIC client");
}
}, },
getProxyError: () => proxyErrorMsg.join(",") getProxyError: () => proxyErrorMsg.join(",")
}); });
@ -320,10 +352,10 @@ interface ProxyOptions {
orgId: string; orgId: string;
} }
export const withGatewayProxy = async ( export const withGatewayProxy = async <T>(
callback: (port: number) => Promise<void>, callback: (port: number) => Promise<T>,
options: ProxyOptions options: ProxyOptions
): Promise<void> => { ): Promise<T> => {
const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options; const { relayHost, relayPort, targetHost, targetPort, tlsOptions, identityId, orgId } = options;
// Setup the proxy server // Setup the proxy server
@ -339,7 +371,7 @@ export const withGatewayProxy = async (
try { try {
// Execute the callback with the allocated port // Execute the callback with the allocated port
await callback(port); return await callback(port);
} catch (err) { } catch (err) {
const proxyErrorMessage = getProxyError(); const proxyErrorMessage = getProxyError();
if (proxyErrorMessage) { if (proxyErrorMessage) {

View File

@ -32,13 +32,13 @@ export const buildFindFilter =
<R extends object = object>( <R extends object = object>(
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>, { $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
tableName?: TableName, tableName?: TableName,
excludeKeys?: Array<keyof R> excludeKeys?: string[]
) => ) =>
(bd: Knex.QueryBuilder<R, R>) => { (bd: Knex.QueryBuilder<R, R>) => {
const processedFilter = tableName const processedFilter = tableName
? Object.fromEntries( ? Object.fromEntries(
Object.entries(filter) Object.entries(filter)
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R)) .filter(([key]) => !excludeKeys || !excludeKeys.includes(key))
.map(([key, value]) => [`${tableName}.${key}`, value]) .map(([key, value]) => [`${tableName}.${key}`, value])
) )
: filter; : filter;

View File

@ -32,7 +32,6 @@ 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 { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal"; import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal";
@ -436,7 +435,6 @@ 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 secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db); const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
@ -1419,12 +1417,24 @@ export const registerRoutes = async (
identityUaDAL, identityUaDAL,
licenseService licenseService
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore
});
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({ const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
identityKubernetesAuthDAL, identityKubernetesAuthDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
licenseService, licenseService,
gatewayService,
gatewayDAL,
kmsService kmsService
}); });
const identityGcpAuthService = identityGcpAuthServiceFactory({ const identityGcpAuthService = identityGcpAuthServiceFactory({
@ -1479,16 +1489,6 @@ export const registerRoutes = async (
identityDAL identityDAL
}); });
const gatewayService = gatewayServiceFactory({
permissionService,
gatewayDAL,
kmsService,
licenseService,
orgGatewayConfigDAL,
keyStore,
projectGatewayDAL
});
const dynamicSecretProviders = buildDynamicSecretProviders({ const dynamicSecretProviders = buildDynamicSecretProviders({
gatewayService gatewayService
}); });
@ -1510,7 +1510,7 @@ export const registerRoutes = async (
permissionService, permissionService,
licenseService, licenseService,
kmsService, kmsService,
projectGatewayDAL, gatewayDAL,
resourceMetadataDAL resourceMetadataDAL
}); });

View File

@ -3,6 +3,7 @@ import { z } from "zod";
import { IdentityKubernetesAuthsSchema } from "@app/db/schemas"; import { IdentityKubernetesAuthsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@ -21,7 +22,8 @@ const IdentityKubernetesAuthResponseSchema = IdentityKubernetesAuthsSchema.pick(
kubernetesHost: true, kubernetesHost: true,
allowedNamespaces: true, allowedNamespaces: true,
allowedNames: true, allowedNames: true,
allowedAudience: true allowedAudience: true,
gatewayId: true
}).extend({ }).extend({
caCert: z.string(), caCert: z.string(),
tokenReviewerJwt: z.string().optional().nullable() tokenReviewerJwt: z.string().optional().nullable()
@ -100,12 +102,30 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}), }),
body: z body: z
.object({ .object({
kubernetesHost: z.string().trim().min(1).describe(KUBERNETES_AUTH.ATTACH.kubernetesHost), kubernetesHost: z
.string()
.trim()
.min(1)
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
.refine(
(val) =>
characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash
])(val),
{
message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes."
}
),
caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert), caCert: z.string().trim().default("").describe(KUBERNETES_AUTH.ATTACH.caCert),
tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().optional().describe(KUBERNETES_AUTH.ATTACH.tokenReviewerJwt),
allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNamespaces), // TODO: validation
allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames), allowedNames: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedNames),
allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience), allowedAudience: z.string().describe(KUBERNETES_AUTH.ATTACH.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.ATTACH.gatewayId),
accessTokenTrustedIps: z accessTokenTrustedIps: z
.object({ .object({
ipAddress: z.string().trim() ipAddress: z.string().trim()
@ -199,12 +219,34 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
}), }),
body: z body: z
.object({ .object({
kubernetesHost: z.string().trim().min(1).optional().describe(KUBERNETES_AUTH.UPDATE.kubernetesHost), kubernetesHost: z
.string()
.trim()
.min(1)
.optional()
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
.refine(
(val) => {
if (!val) return true;
return characterValidator([
CharacterType.Alphabets,
CharacterType.Numbers,
CharacterType.Colon,
CharacterType.Period,
CharacterType.ForwardSlash
])(val);
},
{
message: "Kubernetes host must only contain alphabets, numbers, colons, periods, and forward slashes."
}
),
caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert), caCert: z.string().trim().optional().describe(KUBERNETES_AUTH.UPDATE.caCert),
tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt), tokenReviewerJwt: z.string().trim().nullable().optional().describe(KUBERNETES_AUTH.UPDATE.tokenReviewerJwt),
allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation allowedNamespaces: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNamespaces), // TODO: validation
allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames), allowedNames: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedNames),
allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience), allowedAudience: z.string().optional().describe(KUBERNETES_AUTH.UPDATE.allowedAudience),
gatewayId: z.string().uuid().optional().nullable().describe(KUBERNETES_AUTH.UPDATE.gatewayId),
accessTokenTrustedIps: z accessTokenTrustedIps: z
.object({ .object({
ipAddress: z.string().trim() ipAddress: z.string().trim()

View File

@ -4,8 +4,14 @@ import https from "https";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas"; import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
import { TGatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
OrgPermissionGatewayActions,
OrgPermissionIdentityActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@ -13,6 +19,7 @@ import {
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
import { withGatewayProxy } from "@app/lib/gateway";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { ActorType, AuthTokenType } from "../auth/auth-type"; import { ActorType, AuthTokenType } from "../auth/auth-type";
@ -43,6 +50,8 @@ type TIdentityKubernetesAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
gatewayService: TGatewayServiceFactory;
gatewayDAL: Pick<TGatewayDALFactory, "find">;
}; };
export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>; export type TIdentityKubernetesAuthServiceFactory = ReturnType<typeof identityKubernetesAuthServiceFactory>;
@ -53,8 +62,44 @@ export const identityKubernetesAuthServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
permissionService, permissionService,
licenseService, licenseService,
gatewayService,
gatewayDAL,
kmsService kmsService
}: TIdentityKubernetesAuthServiceFactoryDep) => { }: TIdentityKubernetesAuthServiceFactoryDep) => {
const $gatewayProxyWrapper = async <T>(
inputs: {
gatewayId: string;
targetHost: string;
targetPort: number;
},
gatewayCallback: (host: string, port: number) => Promise<T>
): Promise<T> => {
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
const callbackResult = await withGatewayProxy(
async (port) => {
const res = await gatewayCallback("localhost", port);
return res;
},
{
targetHost: inputs.targetHost,
targetPort: inputs.targetPort,
relayHost,
relayPort: Number(relayPort),
identityId: relayDetails.identityId,
orgId: relayDetails.orgId,
tlsOptions: {
ca: relayDetails.certChain,
cert: relayDetails.certificate,
key: relayDetails.privateKey.toString()
}
}
);
return callbackResult;
};
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) { if (!identityKubernetesAuth) {
@ -92,46 +137,69 @@ export const identityKubernetesAuthServiceFactory = ({
tokenReviewerJwt = serviceAccountJwt; tokenReviewerJwt = serviceAccountJwt;
} }
const { data } = await axios const tokenReviewCallback = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
.post<TCreateTokenReviewResponse>( let baseUrl = `https://${host}`;
`${identityKubernetesAuth.kubernetesHost}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) { if (port) {
throw new UnauthorizedError({ baseUrl += `:${port}`;
message, }
name: "KubernetesTokenReviewRequestError"
}); const res = await axios
.post<TCreateTokenReviewResponse>(
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
// if ca cert, rejectUnauthorized: true
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: !!caCert
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
} }
} }
} throw err;
throw err; });
});
return res.data;
};
const [k8sHost, k8sPort] = identityKubernetesAuth.kubernetesHost.split(":");
const data = identityKubernetesAuth.gatewayId
? await $gatewayProxyWrapper(
{
gatewayId: identityKubernetesAuth.gatewayId,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443
},
tokenReviewCallback
)
: await tokenReviewCallback();
if ("error" in data.status) if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
@ -222,6 +290,7 @@ export const identityKubernetesAuthServiceFactory = ({
const attachKubernetesAuth = async ({ const attachKubernetesAuth = async ({
identityId, identityId,
gatewayId,
kubernetesHost, kubernetesHost,
caCert, caCert,
tokenReviewerJwt, tokenReviewerJwt,
@ -280,6 +349,27 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
@ -296,6 +386,7 @@ export const identityKubernetesAuthServiceFactory = ({
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
gatewayId,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt encryptedKubernetesTokenReviewerJwt: tokenReviewerJwt
? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob ? encryptor({ plainText: Buffer.from(tokenReviewerJwt) }).cipherTextBlob
@ -318,6 +409,7 @@ export const identityKubernetesAuthServiceFactory = ({
allowedNamespaces, allowedNamespaces,
allowedNames, allowedNames,
allowedAudience, allowedAudience,
gatewayId,
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
@ -373,11 +465,33 @@ export const identityKubernetesAuthServiceFactory = ({
return extractIPDetails(accessTokenTrustedIp.ipAddress); return extractIPDetails(accessTokenTrustedIp.ipAddress);
}); });
if (gatewayId) {
const [gateway] = await gatewayDAL.find({ id: gatewayId, orgId: identityMembershipOrg.orgId });
if (!gateway) {
throw new NotFoundError({
message: `Gateway with ID ${gatewayId} not found`
});
}
const { permission: orgPermission } = await permissionService.getOrgPermission(
actor,
actorId,
identityMembershipOrg.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(orgPermission).throwUnlessCan(
OrgPermissionGatewayActions.AttachGateways,
OrgPermissionSubjects.Gateway
);
}
const updateQuery: TIdentityKubernetesAuthsUpdate = { const updateQuery: TIdentityKubernetesAuthsUpdate = {
kubernetesHost, kubernetesHost,
allowedNamespaces, allowedNamespaces,
allowedNames, allowedNames,
allowedAudience, allowedAudience,
gatewayId,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,

View File

@ -13,6 +13,7 @@ export type TAttachKubernetesAuthDTO = {
allowedNamespaces: string; allowedNamespaces: string;
allowedNames: string; allowedNames: string;
allowedAudience: string; allowedAudience: string;
gatewayId?: string | null;
accessTokenTTL: number; accessTokenTTL: number;
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number; accessTokenNumUsesLimit: number;
@ -28,6 +29,7 @@ export type TUpdateKubernetesAuthDTO = {
allowedNamespaces?: string; allowedNamespaces?: string;
allowedNames?: string; allowedNames?: string;
allowedAudience?: string; allowedAudience?: string;
gatewayId?: string | null;
accessTokenTTL?: number; accessTokenTTL?: number;
accessTokenMaxTTL?: number; accessTokenMaxTTL?: number;
accessTokenNumUsesLimit?: number; accessTokenNumUsesLimit?: number;

View File

@ -158,14 +158,4 @@ Once authenticated, the Gateway establishes a secure connection with Infisical t
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. 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) ![Gateway List](../../../images/platform/gateways/gateway-list.png)
</Step> </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> </Steps>

View File

@ -0,0 +1,14 @@
---
title: "Pulumi"
description: "Using Infisical with Pulumi via the Terraform Bridge"
---
Infisical can be integrated with Pulumi by leveraging Pulumis [Terraform Bridge](https://www.pulumi.com/blog/any-terraform-provider/),
which allows Terraform providers to be used seamlessly within Pulumi projects. This enables infrastructure and platform teams to manage Infisical secrets and resources
using Pulumis familiar programming languages (including TypeScript, Python, Go, and C#), without any change to existing workflows.
The Terraform Bridge wraps the [Infisical Terraform provider](/integrations/frameworks/terraform) and exposes its resources (such as `infisical_secret`, `infisical_project`, and `infisical_service_token`)
in a Pulumi-compatible interface. This makes it easy to integrate secret management directly into Pulumi-based IaC pipelines, ensuring secrets stay in sync with
the rest of your cloud infrastructure. Authentication is handled through the same methods as Terraform: using environment variables such as `INFISICAL_TOKEN` and `INFISICAL_SITE_URL`.
By bridging the Infisical provider, teams using Pulumi can adopt secure, centralized secrets management without compromising on their toolchain or language preferences.

View File

@ -218,3 +218,4 @@ Supports conditions and permission inversion
| `create-gateways` | Add new gateways to organization | | `create-gateways` | Add new gateways to organization |
| `edit-gateways` | Modify existing gateway settings | | `edit-gateways` | Modify existing gateway settings |
| `delete-gateways` | Remove gateways from organization | | `delete-gateways` | Remove gateways from organization |
| `attach-gateways` | Attach gateways to resources |

View File

@ -445,6 +445,7 @@
] ]
}, },
"integrations/frameworks/terraform", "integrations/frameworks/terraform",
"integrations/frameworks/pulumi",
"integrations/platforms/ansible", "integrations/platforms/ansible",
"integrations/platforms/apache-airflow" "integrations/platforms/apache-airflow"
] ]

View File

@ -4,19 +4,19 @@ description: "Learn how to configure Infisical with custom certificates"
--- ---
By default, the Infisical Docker image includes certificates from well-known public certificate authorities. By default, the Infisical Docker image includes certificates from well-known public certificate authorities.
However, some integrations with Infisical may need to communicate with your internal services that use private certificate authorities. However, some integrations with Infisical may need to communicate with your internal services that use private certificate authorities.
To configure trust for custom certificates, follow these steps. This is particularly useful for connecting Infisical with self-hosted services like GitLab. To configure trust for custom certificates, follow these steps. This is particularly useful for connecting Infisical with self-hosted services like GitLab.
## Prerequisites ## Prerequisites
- Docker - Docker
- Standalone [Infisical image](https://hub.docker.com/r/infisical/infisical) - Standalone [Infisical image](https://hub.docker.com/r/infisical/infisical)
- Certificate public key `.pem` files - Certificate public key `.crt` files
## Setup ## Setup
1. Place all your public key `.pem` files into a single directory. 1. Place all your public key `.crt` files into a single directory.
2. Mount the directory containing the `.pem` files to the `usr/local/share/ca-certificates/` path in the Infisical container. 2. Mount the directory containing the `.crt` files to the `/usr/local/share/ca-certificates/` path in the Infisical container.
3. Set the following environment variable on your Infisical container: 3. Set the following environment variable on your Infisical container:
``` ```
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt

View File

@ -12,7 +12,8 @@ export enum OrgGatewayPermissionActions {
CreateGateways = "create-gateways", CreateGateways = "create-gateways",
ListGateways = "list-gateways", ListGateways = "list-gateways",
EditGateways = "edit-gateways", EditGateways = "edit-gateways",
DeleteGateways = "delete-gateways" DeleteGateways = "delete-gateways",
AttachGateways = "attach-gateways"
} }
export enum OrgPermissionSubjects { export enum OrgPermissionSubjects {

View File

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

View File

@ -2,7 +2,7 @@ import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types"; import { TGateway } from "./types";
export const gatewaysQueryKeys = { export const gatewaysQueryKeys = {
allKey: () => ["gateways"], allKey: () => ["gateways"],
@ -14,20 +14,5 @@ export const gatewaysQueryKeys = {
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"); const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
return data.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;
}
}) })
}; };

View File

@ -11,39 +11,13 @@ 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 TListProjectGatewayDTO = {
projectId: string;
};

View File

@ -741,7 +741,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps accessTokenTrustedIps,
gatewayId
}) => { }) => {
const { const {
data: { identityKubernetesAuth } data: { identityKubernetesAuth }
@ -757,7 +758,8 @@ export const useAddIdentityKubernetesAuth = () => {
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps accessTokenTrustedIps,
gatewayId
} }
); );
@ -846,7 +848,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps accessTokenTrustedIps,
gatewayId
}) => { }) => {
const { const {
data: { identityKubernetesAuth } data: { identityKubernetesAuth }
@ -862,7 +865,8 @@ export const useUpdateIdentityKubernetesAuth = () => {
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps accessTokenTrustedIps,
gatewayId
} }
); );

View File

@ -346,6 +346,7 @@ export type IdentityKubernetesAuth = {
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
accessTokenNumUsesLimit: number; accessTokenNumUsesLimit: number;
accessTokenTrustedIps: IdentityTrustedIp[]; accessTokenTrustedIps: IdentityTrustedIp[];
gatewayId?: string | null;
}; };
export type AddIdentityKubernetesAuthDTO = { export type AddIdentityKubernetesAuthDTO = {
@ -356,6 +357,7 @@ export type AddIdentityKubernetesAuthDTO = {
allowedNamespaces: string; allowedNamespaces: string;
allowedNames: string; allowedNames: string;
allowedAudience: string; allowedAudience: string;
gatewayId?: string | null;
caCert: string; caCert: string;
accessTokenTTL: number; accessTokenTTL: number;
accessTokenMaxTTL: number; accessTokenMaxTTL: number;
@ -373,6 +375,7 @@ export type UpdateIdentityKubernetesAuthDTO = {
allowedNamespaces?: string; allowedNamespaces?: string;
allowedNames?: string; allowedNames?: string;
allowedAudience?: string; allowedAudience?: string;
gatewayId?: string | null;
caCert?: string; caCert?: string;
accessTokenTTL?: number; accessTokenTTL?: number;
accessTokenMaxTTL?: number; accessTokenMaxTTL?: number;

View File

@ -3,22 +3,32 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
FormControl, FormControl,
IconButton, IconButton,
Input, Input,
Select,
SelectItem,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
Tabs, Tabs,
TextArea TextArea,
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context"; import { useOrganization, useSubscription } from "@app/context";
import { import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import {
gatewaysQueryKeys,
useAddIdentityKubernetesAuth, useAddIdentityKubernetesAuth,
useGetIdentityKubernetesAuth, useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth useUpdateIdentityKubernetesAuth
@ -32,6 +42,7 @@ const schema = z
.object({ .object({
kubernetesHost: z.string().min(1), kubernetesHost: z.string().min(1),
tokenReviewerJwt: z.string().optional(), tokenReviewerJwt: z.string().optional(),
gatewayId: z.string().optional().nullable(),
allowedNames: z.string(), allowedNames: z.string(),
allowedNamespaces: z.string(), allowedNamespaces: z.string(),
allowedAudience: z.string(), allowedAudience: z.string(),
@ -79,6 +90,8 @@ export const IdentityKubernetesAuthForm = ({
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth(); const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration); const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", { const { data } = useGetIdentityKubernetesAuth(identityId ?? "", {
enabled: isUpdate enabled: isUpdate
}); });
@ -96,6 +109,7 @@ export const IdentityKubernetesAuthForm = ({
tokenReviewerJwt: "", tokenReviewerJwt: "",
allowedNames: "", allowedNames: "",
allowedNamespaces: "", allowedNamespaces: "",
gatewayId: "",
allowedAudience: "", allowedAudience: "",
caCert: "", caCert: "",
accessTokenTTL: "2592000", accessTokenTTL: "2592000",
@ -120,6 +134,7 @@ export const IdentityKubernetesAuthForm = ({
allowedNamespaces: data.allowedNamespaces, allowedNamespaces: data.allowedNamespaces,
allowedAudience: data.allowedAudience, allowedAudience: data.allowedAudience,
caCert: data.caCert, caCert: data.caCert,
gatewayId: data.gatewayId || null,
accessTokenTTL: String(data.accessTokenTTL), accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL), accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit), accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
@ -157,6 +172,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenTTL, accessTokenTTL,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
gatewayId,
accessTokenTrustedIps accessTokenTrustedIps
}: FormData) => { }: FormData) => {
try { try {
@ -172,6 +188,7 @@ export const IdentityKubernetesAuthForm = ({
allowedAudience, allowedAudience,
caCert, caCert,
identityId, identityId,
gatewayId: gatewayId || null,
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit), accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -186,6 +203,7 @@ export const IdentityKubernetesAuthForm = ({
allowedNames: allowedNames || "", allowedNames: allowedNames || "",
allowedNamespaces: allowedNamespaces || "", allowedNamespaces: allowedNamespaces || "",
allowedAudience: allowedAudience || "", allowedAudience: allowedAudience || "",
gatewayId: gatewayId || null,
caCert: caCert || "", caCert: caCert || "",
accessTokenTTL: Number(accessTokenTTL), accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL), accessTokenMaxTTL: Number(accessTokenMaxTTL),
@ -217,6 +235,7 @@ export const IdentityKubernetesAuthForm = ({
[ [
"kubernetesHost", "kubernetesHost",
"tokenReviewerJwt", "tokenReviewerJwt",
"gatewayId",
"accessTokenTTL", "accessTokenTTL",
"accessTokenMaxTTL", "accessTokenMaxTTL",
"accessTokenNumUsesLimit", "accessTokenNumUsesLimit",
@ -280,6 +299,62 @@ export const IdentityKubernetesAuthForm = ({
</FormControl> </FormControl>
)} )}
/> />
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
isOptional
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={(v) => {
if (v !== "") {
onChange(v);
}
}}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewayLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(null)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
<Controller <Controller
control={control} control={control}
name="allowedNames" name="allowedNames"

View File

@ -32,7 +32,6 @@ import {
Table, Table,
TableContainer, TableContainer,
TableSkeleton, TableSkeleton,
Tag,
TBody, TBody,
Td, Td,
Th, Th,
@ -128,7 +127,6 @@ 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
@ -151,13 +149,6 @@ 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

View File

@ -3,10 +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, FilterableSelect, FormControl, Input } from "@app/components/v2"; import { Button, FormControl, Input } from "@app/components/v2";
import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api"; import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { 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;
@ -14,13 +14,7 @@ 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>;
@ -38,20 +32,13 @@ 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, projects }: FormData) => { const onFormSubmit = ({ name }: 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: () => {
@ -67,6 +54,16 @@ export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
return ( return (
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
<NoticeBannerV2 className="mx-auto mb-4" title="Project Linking">
<p className="mt-1 text-xs text-mineshaft-300">
Since the 15th May 2025, all gateways are automatically available for use in all projects
and you no longer need to link them.
<br />
Organization members with the &quot;Attach Gateways&quot; permission can use gateways
anywhere within the organization.
</p>
</NoticeBannerV2>
<Controller <Controller
control={control} control={control}
name="name" name="name"
@ -76,30 +73,6 @@ 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(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"> <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

View File

@ -1,8 +1,10 @@
import { useMemo } from "react";
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons"; import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2"; import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityKubernetesAuth } from "@app/hooks/api"; import { gatewaysQueryKeys, useGetIdentityKubernetesAuth } from "@app/hooks/api";
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm"; import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay"; import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
@ -16,8 +18,14 @@ export const ViewIdentityKubernetesAuthContent = ({
onDelete, onDelete,
popUp popUp
}: ViewAuthMethodProps) => { }: ViewAuthMethodProps) => {
const { data: gateways } = useQuery(gatewaysQueryKeys.list());
const { data, isPending } = useGetIdentityKubernetesAuth(identityId); const { data, isPending } = useGetIdentityKubernetesAuth(identityId);
const selectedGateway = useMemo(() => {
return gateways?.find((gateway) => gateway.id === data?.gatewayId) || null;
}, [gateways, data?.gatewayId]);
if (isPending) { if (isPending) {
return ( return (
<div className="flex w-full items-center justify-center"> <div className="flex w-full items-center justify-center">
@ -69,6 +77,7 @@ export const ViewIdentityKubernetesAuthContent = ({
> >
{data.kubernetesHost} {data.kubernetesHost}
</IdentityAuthFieldDisplay> </IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Gateway">{selectedGateway?.name}</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT"> <IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
{data.tokenReviewerJwt ? ( {data.tokenReviewerJwt ? (
<Tooltip <Tooltip

View File

@ -69,7 +69,8 @@ const orgGatewayPermissionSchema = z
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(), [OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional() [OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional(),
[OrgGatewayPermissionActions.AttachGateways]: z.boolean().optional()
}) })
.optional(); .optional();

View File

@ -27,7 +27,8 @@ const PERMISSION_ACTIONS = [
{ action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" }, { action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" },
{ action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" }, { action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" },
{ action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" }, { action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" },
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" } { action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" },
{ action: OrgGatewayPermissionActions.AttachGateways, label: "Attach Gateways" }
] as const; ] as const;
export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => { export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => {

View File

@ -1,11 +1,9 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs, Tooltip } from "@app/components/v2"; import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
@ -54,16 +52,7 @@ export const IntegrationsListPage = () => {
<Tabs value={selectedTab} onValueChange={updateSelectedTab}> <Tabs value={selectedTab} onValueChange={updateSelectedTab}>
<TabList> <TabList>
<Tab value={IntegrationsListPageTabs.SecretSyncs}>Secret Syncs</Tab> <Tab value={IntegrationsListPageTabs.SecretSyncs}>Secret Syncs</Tab>
<Tab value={IntegrationsListPageTabs.NativeIntegrations}> <Tab value={IntegrationsListPageTabs.NativeIntegrations}>Native Integrations</Tab>
Native Integrations
<Tooltip content="Native Integrations will be deprecated in 2026. Please migrate to Secret Syncs as they become available.">
<div>
<Badge variant="primary" className="ml-1 cursor-pointer text-xs">
Legacy
</Badge>
</div>
</Tooltip>
</Tab>
<Tab value={IntegrationsListPageTabs.FrameworkIntegrations}> <Tab value={IntegrationsListPageTabs.FrameworkIntegrations}>
Framework Integrations Framework Integrations
</Tab> </Tab>
@ -81,26 +70,6 @@ export const IntegrationsListPage = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</TabPanel> </TabPanel>
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}> <TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
<div className="mb-5 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1.5 text-primary" />
Native Integrations Transitioning to Legacy Status
</div>
<p className="mb-2 mt-1 text-sm text-bunker-300">
Native integrations are now a legacy feature and we will begin a phased
deprecation in 2026. We recommend migrating to our new{" "}
<a
className="text-bunker-200 underline decoration-primary-700 underline-offset-4 duration-200 hover:text-mineshaft-100 hover:decoration-primary-600"
href="https://infisical.com/docs/integrations/secret-syncs/overview"
target="_blank"
rel="noopener noreferrer"
>
Secret Syncs
</a>{" "}
feature which offers the same functionality as Native Integrations with improved
stability, insights, re-configurability, and customization.
</p>
</div>
<ProjectPermissionCan <ProjectPermissionCan
renderGuardBanner renderGuardBanner
I={ProjectPermissionActions.Read} I={ProjectPermissionActions.Read}

View File

@ -6,6 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -18,9 +19,13 @@ import {
SecretInput, SecretInput,
Select, Select,
SelectItem, SelectItem,
TextArea TextArea,
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import {
OrgGatewayPermissionActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api"; import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
@ -61,7 +66,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(),
projectGatewayId: z.string().optional() gatewayId: z.string().optional()
}), }),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
const valMs = ms(val); const valMs = ms(val);
@ -164,8 +169,6 @@ export const SqlDatabaseInputForm = ({
projectSlug, projectSlug,
isSingleEnvironmentMode isSingleEnvironmentMode
}: Props) => { }: Props) => {
const { currentWorkspace } = useWorkspace();
const { const {
control, control,
setValue, setValue,
@ -193,9 +196,7 @@ export const SqlDatabaseInputForm = ({
}); });
const createDynamicSecret = useCreateDynamicSecret(); const createDynamicSecret = useCreateDynamicSecret();
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const handleCreateDynamicSecret = async ({ const handleCreateDynamicSecret = async ({
name, name,
@ -301,40 +302,55 @@ export const SqlDatabaseInputForm = ({
Configuration Configuration
</div> </div>
<div> <div>
<Controller <OrgPermissionCan
control={control} I={OrgGatewayPermissionActions.AttachGateways}
name="provider.projectGatewayId" a={OrgPermissionSubjects.Gateway}
defaultValue="" >
render={({ field: { value, onChange }, fieldState: { error } }) => ( {(isAllowed) => (
<FormControl <Controller
isError={Boolean(error?.message)} control={control}
errorText={error?.message} name="provider.gatewayId"
label="Gateway" defaultValue=""
> render={({ field: { value, onChange }, fieldState: { error } }) => (
<Select <FormControl
value={value} isError={Boolean(error?.message)}
onValueChange={onChange} errorText={error?.message}
className="w-full border border-mineshaft-500" label="Gateway"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
> >
Internet Gateway <Tooltip
</SelectItem> isDisabled={isAllowed}
{projectGateways?.map((el) => ( content="Restricted access. You don't have permission to attach gateways to resources."
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}> >
{el.name} <div>
</SelectItem> <Select
))} isDisabled={!isAllowed}
</Select> value={value}
</FormControl> onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)} )}
/> </OrgPermissionCan>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div> <div className="pb-0.5 pl-1 text-sm text-mineshaft-400">Service</div>

View File

@ -6,6 +6,7 @@ import { z } from "zod";
import { TtlFormLabel } from "@app/components/features"; import { TtlFormLabel } from "@app/components/features";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import { import {
Accordion, Accordion,
AccordionContent, AccordionContent,
@ -17,9 +18,11 @@ import {
SecretInput, SecretInput,
Select, Select,
SelectItem, SelectItem,
TextArea TextArea,
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { OrgPermissionSubjects } from "@app/context";
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
@ -60,7 +63,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(),
projectGatewayId: z.string().optional().nullable() gatewayId: z.string().optional().nullable()
}) })
.partial(), .partial(),
defaultTTL: z.string().superRefine((val, ctx) => { defaultTTL: z.string().superRefine((val, ctx) => {
@ -147,15 +150,11 @@ export const EditDynamicSecretSqlProviderForm = ({
} }
}); });
const { currentWorkspace } = useWorkspace(); const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
);
const updateDynamicSecret = useUpdateDynamicSecret(); const updateDynamicSecret = useUpdateDynamicSecret();
const selectedProjectGatewayId = watch("inputs.projectGatewayId"); const selectedGatewayId = watch("inputs.gatewayId");
const isGatewayInActive = const isGatewayInActive = gateways?.findIndex((el) => el.id === selectedGatewayId) === -1;
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
const handleUpdateDynamicSecret = async ({ const handleUpdateDynamicSecret = async ({
inputs, inputs,
@ -177,7 +176,7 @@ export const EditDynamicSecretSqlProviderForm = ({
defaultTTL, defaultTTL,
inputs: { inputs: {
...inputs, ...inputs,
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId gatewayId: isGatewayInActive ? null : inputs.gatewayId
}, },
newName: newName === dynamicSecret.name ? undefined : newName, newName: newName === dynamicSecret.name ? undefined : newName,
metadata metadata
@ -250,45 +249,60 @@ export const EditDynamicSecretSqlProviderForm = ({
<div> <div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div> <div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div> <div>
<Controller <OrgPermissionCan
control={control} I={OrgGatewayPermissionActions.AttachGateways}
name="inputs.projectGatewayId" a={OrgPermissionSubjects.Gateway}
defaultValue="" >
render={({ field: { value, onChange }, fieldState: { error } }) => ( {(isAllowed) => (
<FormControl <Controller
isError={Boolean(error?.message) || isGatewayInActive} control={control}
errorText={ name="inputs.gatewayId"
isGatewayInActive && selectedProjectGatewayId defaultValue=""
? `Project Gateway ${selectedProjectGatewayId} is removed` render={({ field: { value, onChange }, fieldState: { error } }) => (
: error?.message <FormControl
} isError={Boolean(error?.message) || isGatewayInActive}
label="Gateway" errorText={
helperText="" isGatewayInActive && selectedGatewayId
> ? `Project Gateway ${selectedGatewayId} is removed`
<Select : error?.message
value={value || undefined} }
onValueChange={onChange} label="Gateway"
className="w-full border border-mineshaft-500" helperText=""
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
> >
Internet Gateway <Tooltip
</SelectItem> isDisabled={isAllowed}
{projectGateways?.map((el) => ( content="Restricted access. You don't have permission to attach gateways to resources."
<SelectItem value={el.projectGatewayId} key={el.id}> >
{el.name} <div>
</SelectItem> <Select
))} isDisabled={!isAllowed}
</Select> value={value || undefined}
</FormControl> onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)} )}
/> </OrgPermissionCan>
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
<Controller <Controller