Compare commits
62 Commits
doc/add-ab
...
misc/add-d
Author | SHA1 | Date | |
---|---|---|---|
|
7abf3e3642 | ||
|
82ef35bd08 | ||
|
4eb668b5a5 | ||
|
18edea9f26 | ||
|
68646bcdf8 | ||
|
9989ceb6d1 | ||
|
95d7ba5f22 | ||
|
2aa6fdf983 | ||
|
be5a32a5d6 | ||
|
f009cd329b | ||
|
e2778864e2 | ||
|
ea7375b2c6 | ||
|
d42566c335 | ||
|
45cbd9f006 | ||
|
8580602ea7 | ||
|
7ff75cdfab | ||
|
bd8c8871c0 | ||
|
d5aa13b277 | ||
|
428dc5d371 | ||
|
f1facf1f2c | ||
|
31dc36d4e2 | ||
|
51f29e5357 | ||
|
30f0f174d1 | ||
|
3e7110f334 | ||
|
e6af7a6fb9 | ||
|
de420fd02c | ||
|
41a3ca149d | ||
|
da38d1a261 | ||
|
b0d8c8fb23 | ||
|
d84bac5fba | ||
|
44f74e4d12 | ||
|
c16a4e00d8 | ||
|
11f2719842 | ||
|
f8153dd896 | ||
|
b104f8c07d | ||
|
746687e5b5 | ||
|
080b1e1550 | ||
|
38a6fd140c | ||
|
19d66abc38 | ||
|
e61c0be6db | ||
|
917573931f | ||
|
929a41065c | ||
|
9b44972e77 | ||
|
17e576511b | ||
|
afd444cad6 | ||
|
55b1fbdf52 | ||
|
46ca5c8efa | ||
|
f7406ea8f8 | ||
|
f34370cb9d | ||
|
78718cd299 | ||
|
1307fa49d4 | ||
|
a7ca242f5d | ||
|
c6b3b24312 | ||
|
8520029958 | ||
|
7905017121 | ||
|
4bbe80c083 | ||
|
d65ae2c61b | ||
|
84c534ef70 | ||
|
ce4c5d8ea1 | ||
|
617aa2f533 | ||
|
e9dd3340bf | ||
|
1c2b4e91ba |
@@ -112,4 +112,11 @@ INF_APP_CONNECTION_GCP_SERVICE_ACCOUNT_CREDENTIAL=
|
||||
|
||||
# azure app connection
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_ID=
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET=
|
||||
|
||||
# datadog
|
||||
SHOULD_USE_DATADOG_TRACER=
|
||||
DATADOG_PROFILING_ENABLED=
|
||||
DATADOG_ENV=
|
||||
DATADOG_SERVICE=
|
||||
DATADOG_HOSTNAME=
|
||||
|
@@ -161,6 +161,9 @@ COPY --from=backend-runner /app /backend
|
||||
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
|
||||
ENV PORT 8080
|
||||
ENV HOST=0.0.0.0
|
||||
ENV HTTPS_ENABLED false
|
||||
|
@@ -159,6 +159,8 @@ COPY --from=backend-runner /app /backend
|
||||
|
||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||
|
||||
ARG INFISICAL_PLATFORM_VERSION
|
||||
ENV INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
|
||||
|
||||
ENV PORT 8080
|
||||
ENV HOST=0.0.0.0
|
||||
@@ -166,6 +168,7 @@ ENV HTTPS_ENABLED false
|
||||
ENV NODE_ENV production
|
||||
ENV STANDALONE_BUILD true
|
||||
ENV STANDALONE_MODE true
|
||||
|
||||
WORKDIR /backend
|
||||
|
||||
ENV TELEMETRY_ENABLED true
|
||||
|
914
backend/package-lock.json
generated
@@ -60,6 +60,13 @@
|
||||
"migration:status": "npm run auditlog-migration:status && knex --knexfile ./dist/db/knexfile.mjs --client pg migrate:status",
|
||||
"migration:rollback": "npm run auditlog-migration:rollback && knex --knexfile ./dist/db/knexfile.mjs migrate:rollback",
|
||||
"migration:unlock": "npm run auditlog-migration:unlock && knex --knexfile ./dist/db/knexfile.mjs migrate:unlock",
|
||||
"migration:up-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||
"migration:down-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||
"migration:list-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||
"migration:latest-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||
"migration:status-dev": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:status",
|
||||
"migration:rollback-dev": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||
"migration:unlock-dev": "knex --knexfile ./src/db/knexfile.ts migrate:unlock",
|
||||
"migrate:org": "tsx ./scripts/migrate-organization.ts",
|
||||
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
|
||||
@@ -169,6 +176,7 @@
|
||||
"cassandra-driver": "^4.7.2",
|
||||
"connect-redis": "^7.1.1",
|
||||
"cron": "^3.1.7",
|
||||
"dd-trace": "^5.40.0",
|
||||
"dotenv": "^16.4.1",
|
||||
"fastify": "^4.28.1",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
@@ -177,6 +185,7 @@
|
||||
"handlebars": "^4.7.8",
|
||||
"hdb": "^0.19.10",
|
||||
"ioredis": "^5.3.2",
|
||||
"isomorphic-dompurify": "^2.22.0",
|
||||
"jmespath": "^0.16.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsrp": "^0.2.4",
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
@@ -13,6 +13,7 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||
@@ -228,6 +229,7 @@ declare module "fastify" {
|
||||
secretSync: TSecretSyncServiceFactory;
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
20
backend/src/@types/knex.d.ts
vendored
@@ -68,6 +68,9 @@ import {
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TGateways,
|
||||
TGatewaysInsert,
|
||||
TGatewaysUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@@ -179,6 +182,9 @@ import {
|
||||
TOrgBots,
|
||||
TOrgBotsInsert,
|
||||
TOrgBotsUpdate,
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate,
|
||||
@@ -200,6 +206,9 @@ import {
|
||||
TProjectEnvironments,
|
||||
TProjectEnvironmentsInsert,
|
||||
TProjectEnvironmentsUpdate,
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate,
|
||||
TProjectKeys,
|
||||
TProjectKeysInsert,
|
||||
TProjectKeysUpdate,
|
||||
@@ -930,5 +939,16 @@ declare module "knex/types/tables" {
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate
|
||||
>;
|
||||
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
|
||||
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate
|
||||
>;
|
||||
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ export default {
|
||||
},
|
||||
migrations: {
|
||||
tableName: "infisical_migrations",
|
||||
loadExtensions: [".mjs"]
|
||||
loadExtensions: [".mjs", ".ts"]
|
||||
}
|
||||
},
|
||||
production: {
|
||||
@@ -64,7 +64,7 @@ export default {
|
||||
},
|
||||
migrations: {
|
||||
tableName: "infisical_migrations",
|
||||
loadExtensions: [".mjs"]
|
||||
loadExtensions: [".mjs", ".ts"]
|
||||
}
|
||||
}
|
||||
} as Knex.Config;
|
||||
|
115
backend/src/db/migrations/20250212191958_create-gateway.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.OrgGatewayConfig))) {
|
||||
await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("rootCaKeyAlgorithm").notNullable();
|
||||
|
||||
t.datetime("rootCaIssuedAt").notNullable();
|
||||
t.datetime("rootCaExpiration").notNullable();
|
||||
t.string("rootCaSerialNumber").notNullable();
|
||||
t.binary("encryptedRootCaCertificate").notNullable();
|
||||
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||
|
||||
t.datetime("clientCaIssuedAt").notNullable();
|
||||
t.datetime("clientCaExpiration").notNullable();
|
||||
t.string("clientCaSerialNumber");
|
||||
t.binary("encryptedClientCaCertificate").notNullable();
|
||||
t.binary("encryptedClientCaPrivateKey").notNullable();
|
||||
|
||||
t.string("clientCertSerialNumber").notNullable();
|
||||
t.string("clientCertKeyAlgorithm").notNullable();
|
||||
t.datetime("clientCertIssuedAt").notNullable();
|
||||
t.datetime("clientCertExpiration").notNullable();
|
||||
t.binary("encryptedClientCertificate").notNullable();
|
||||
t.binary("encryptedClientPrivateKey").notNullable();
|
||||
|
||||
t.datetime("gatewayCaIssuedAt").notNullable();
|
||||
t.datetime("gatewayCaExpiration").notNullable();
|
||||
t.string("gatewayCaSerialNumber").notNullable();
|
||||
t.binary("encryptedGatewayCaCertificate").notNullable();
|
||||
t.binary("encryptedGatewayCaPrivateKey").notNullable();
|
||||
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.unique("orgId");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.Gateway))) {
|
||||
await knex.schema.createTable(TableName.Gateway, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.string("serialNumber").notNullable();
|
||||
t.string("keyAlgorithm").notNullable();
|
||||
t.datetime("issuedAt").notNullable();
|
||||
t.datetime("expiration").notNullable();
|
||||
t.datetime("heartbeat");
|
||||
|
||||
t.binary("relayAddress").notNullable();
|
||||
|
||||
t.uuid("orgGatewayRootCaId").notNullable();
|
||||
t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE");
|
||||
|
||||
t.uuid("identityId").notNullable();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.Gateway);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) {
|
||||
await knex.schema.createTable(TableName.ProjectGateway, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
t.uuid("gatewayId").notNullable();
|
||||
t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
// not setting a foreign constraint so that cascade effects are not triggered
|
||||
if (!doesGatewayColExist) {
|
||||
t.uuid("projectGatewayId");
|
||||
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
if (doesGatewayColExist) t.dropColumn("projectGatewayId");
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectGateway);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.Gateway);
|
||||
await dropOnUpdateTrigger(knex, TableName.Gateway);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||
if (await knex.schema.hasTable(TableName.SuperAdmin)) {
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (!hasAuthConsentContentCol) {
|
||||
t.text("authConsentContent");
|
||||
}
|
||||
if (!hasPageFrameContentCol) {
|
||||
t.text("pageFrameContent");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAuthConsentContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "authConsentContent");
|
||||
const hasPageFrameContentCol = await knex.schema.hasColumn(TableName.SuperAdmin, "pageFrameContent");
|
||||
await knex.schema.alterTable(TableName.SuperAdmin, (t) => {
|
||||
if (hasAuthConsentContentCol) {
|
||||
t.dropColumn("authConsentContent");
|
||||
}
|
||||
if (hasPageFrameContentCol) {
|
||||
t.dropColumn("pageFrameContent");
|
||||
}
|
||||
});
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
for await (const tableName of [
|
||||
TableName.SecretV2,
|
||||
TableName.SecretVersionV2,
|
||||
TableName.SecretApprovalRequestSecretV2
|
||||
]) {
|
||||
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||
|
||||
if (hasReminderNoteCol) {
|
||||
await knex.schema.alterTable(tableName, (t) => {
|
||||
t.string("reminderNote", 1024).alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
for await (const tableName of [
|
||||
TableName.SecretV2,
|
||||
TableName.SecretVersionV2,
|
||||
TableName.SecretApprovalRequestSecretV2
|
||||
]) {
|
||||
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||
|
||||
if (hasReminderNoteCol) {
|
||||
await knex.schema.alterTable(tableName, (t) => {
|
||||
t.string("reminderNote").alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@@ -26,7 +26,8 @@ export const DynamicSecretsSchema = z.object({
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedInput: zodBuffer
|
||||
encryptedInput: zodBuffer,
|
||||
projectGatewayId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
29
backend/src/db/schemas/gateways.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GatewaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
expiration: z.date(),
|
||||
heartbeat: z.date().nullable().optional(),
|
||||
relayAddress: zodBuffer,
|
||||
orgGatewayRootCaId: z.string().uuid(),
|
||||
identityId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGateways = z.infer<typeof GatewaysSchema>;
|
||||
export type TGatewaysInsert = Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>;
|
||||
export type TGatewaysUpdate = Partial<Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>>;
|
@@ -20,6 +20,7 @@ export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
@@ -57,6 +58,7 @@ export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
export * from "./oidc-configs";
|
||||
export * from "./org-bots";
|
||||
export * from "./org-gateway-config";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
@@ -65,6 +67,7 @@ export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-gateways";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
|
@@ -113,6 +113,10 @@ export enum TableName {
|
||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||
// Gateway
|
||||
OrgGatewayConfig = "org_gateway_config",
|
||||
Gateway = "gateways",
|
||||
ProjectGateway = "project_gateways",
|
||||
// junction tables with tags
|
||||
SecretV2JnTag = "secret_v2_tag_junction",
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
|
43
backend/src/db/schemas/org-gateway-config.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgGatewayConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
rootCaKeyAlgorithm: z.string(),
|
||||
rootCaIssuedAt: z.date(),
|
||||
rootCaExpiration: z.date(),
|
||||
rootCaSerialNumber: z.string(),
|
||||
encryptedRootCaCertificate: zodBuffer,
|
||||
encryptedRootCaPrivateKey: zodBuffer,
|
||||
clientCaIssuedAt: z.date(),
|
||||
clientCaExpiration: z.date(),
|
||||
clientCaSerialNumber: z.string().nullable().optional(),
|
||||
encryptedClientCaCertificate: zodBuffer,
|
||||
encryptedClientCaPrivateKey: zodBuffer,
|
||||
clientCertSerialNumber: z.string(),
|
||||
clientCertKeyAlgorithm: z.string(),
|
||||
clientCertIssuedAt: z.date(),
|
||||
clientCertExpiration: z.date(),
|
||||
encryptedClientCertificate: zodBuffer,
|
||||
encryptedClientPrivateKey: zodBuffer,
|
||||
gatewayCaIssuedAt: z.date(),
|
||||
gatewayCaExpiration: z.date(),
|
||||
gatewayCaSerialNumber: z.string(),
|
||||
encryptedGatewayCaCertificate: zodBuffer,
|
||||
encryptedGatewayCaPrivateKey: zodBuffer,
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TOrgGatewayConfig = z.infer<typeof OrgGatewayConfigSchema>;
|
||||
export type TOrgGatewayConfigInsert = Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>;
|
||||
export type TOrgGatewayConfigUpdate = Partial<Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>>;
|
20
backend/src/db/schemas/project-gateways.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectGatewaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
gatewayId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectGateways = z.infer<typeof ProjectGatewaysSchema>;
|
||||
export type TProjectGatewaysInsert = Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>;
|
||||
export type TProjectGatewaysUpdate = Partial<Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>>;
|
@@ -23,7 +23,9 @@ export const SuperAdminSchema = z.object({
|
||||
defaultAuthOrgId: z.string().uuid().nullable().optional(),
|
||||
enabledLoginMethods: z.string().array().nullable().optional(),
|
||||
encryptedSlackClientId: zodBuffer.nullable().optional(),
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional()
|
||||
encryptedSlackClientSecret: zodBuffer.nullable().optional(),
|
||||
authConsentContent: z.string().nullable().optional(),
|
||||
pageFrameContent: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSuperAdmin = z.infer<typeof SuperAdminSchema>;
|
||||
|
265
backend/src/ee/routes/v1/gateway-router.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GatewaysSchema } from "@app/db/schemas";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SanitizedGatewaySchema = GatewaysSchema.pick({
|
||||
id: true,
|
||||
identityId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
issuedAt: true,
|
||||
serialNumber: true,
|
||||
heartbeat: true
|
||||
});
|
||||
|
||||
const isValidRelayAddress = (relayAddress: string) => {
|
||||
const [ip, port] = relayAddress.split(":");
|
||||
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
|
||||
};
|
||||
|
||||
export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/register-identity",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
turnServerUsername: z.string(),
|
||||
turnServerPassword: z.string(),
|
||||
turnServerRealm: z.string(),
|
||||
turnServerAddress: z.string(),
|
||||
infisicalStaticIp: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const relayDetails = await server.services.gateway.getGatewayRelayDetails(
|
||||
req.permission.id,
|
||||
req.permission.orgId,
|
||||
req.permission.authMethod
|
||||
);
|
||||
return relayDetails;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/exchange-cert",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serialNumber: z.string(),
|
||||
privateKey: z.string(),
|
||||
certificate: z.string(),
|
||||
certificateChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
|
||||
identityOrg: req.permission.orgId,
|
||||
identityId: req.permission.id,
|
||||
relayAddress: req.body.relayAddress,
|
||||
identityOrgAuthMethod: req.permission.authMethod
|
||||
});
|
||||
return gatewayCertificates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/heartbeat",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.gateway.heartbeat({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { message: "Successfully registered heartbeat" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateways: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projects: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gateway.listGateways({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/projects/:projectId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateways: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projectGatewayId: z.string()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gateway.getProjectGateways({
|
||||
projectId: req.params.projectId,
|
||||
projectPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.getGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
projectIds: z.string().array().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.updateGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id,
|
||||
name: req.body.name,
|
||||
projectIds: req.body.projectIds
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.deleteGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
};
|
@@ -7,6 +7,7 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerKmipRouter } from "./kmip-router";
|
||||
@@ -67,6 +68,8 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/dynamic-secrets" }
|
||||
);
|
||||
|
||||
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||
|
@@ -1,20 +1,31 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
|
||||
export const verifyHostInputValidity = (host: string) => {
|
||||
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||
const appCfg = getConfig();
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
// no need for validation when it's dev
|
||||
if (appCfg.NODE_ENV === "development") return;
|
||||
|
||||
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
!isGateway &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
|
||||
) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
};
|
||||
|
@@ -16,6 +16,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
@@ -44,6 +45,7 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@@ -57,7 +59,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@@ -108,6 +111,18 @@ export const dynamicSecretServiceFactory = ({
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
|
||||
const projectGatewayId = inputs.projectGatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@@ -123,7 +138,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
});
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
@@ -195,6 +211,23 @@ export const dynamicSecretServiceFactory = ({
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (
|
||||
updatedInput &&
|
||||
typeof updatedInput === "object" &&
|
||||
"projectGatewayId" in updatedInput &&
|
||||
updatedInput?.projectGatewayId
|
||||
) {
|
||||
const projectGatewayId = updatedInput.projectGatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@@ -204,7 +237,8 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null
|
||||
statusDetails: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
@@ -16,8 +17,14 @@ import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
|
||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const buildDynamicSecretProviders = ({
|
||||
gatewayService
|
||||
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
|
@@ -103,7 +103,8 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional()
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
|
@@ -3,8 +3,10 @@ import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
@@ -25,10 +27,14 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
type TSqlDatabaseProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
@@ -45,7 +51,6 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 },
|
||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||
@@ -61,61 +66,112 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
return db;
|
||||
};
|
||||
|
||||
const gatewayProxyWrapper = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
const isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
await db.destroy();
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { database } = providerInputs;
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
await db.destroy();
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
@@ -123,28 +179,35 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const db = await $getClient(providerInputs);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
try {
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
|
86
backend/src/ee/services/gateway/gateway-dal.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
buildFindFilter,
|
||||
ormify,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
TFindFilter,
|
||||
TFindOpt
|
||||
} from "@app/lib/knex";
|
||||
|
||||
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
|
||||
|
||||
export const gatewayDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.Gateway);
|
||||
|
||||
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Gateway)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
|
||||
.select(selectAllTableCols(TableName.Gateway))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
);
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
return sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (data) => ({
|
||||
...GatewaysSchema.parse(data),
|
||||
identity: { id: data.identityId, name: data.identityName }
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "projectId",
|
||||
label: "projects" as const,
|
||||
mapper: ({ projectId, projectName, projectSlug }) => ({
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
slug: projectSlug
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Gateway)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||
.select(selectAllTableCols(TableName.Gateway))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
|
||||
)
|
||||
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
|
||||
|
||||
const docs = await query;
|
||||
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, find, findByProjectId };
|
||||
};
|
652
backend/src/ee/services/gateway/gateway-service.ts
Normal file
@@ -0,0 +1,652 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { pingGatewayAndVerify } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { getTurnCredentials } from "@app/lib/turn/credentials";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
createSerialNumber,
|
||||
keyAlgorithmToAlgCfg
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGatewayDALFactory } from "./gateway-dal";
|
||||
import {
|
||||
TExchangeAllocatedRelayAddressDTO,
|
||||
TGetGatewayByIdDTO,
|
||||
TGetProjectGatewayByIdDTO,
|
||||
THeartBeatDTO,
|
||||
TListGatewaysDTO,
|
||||
TUpdateGatewayByIdDTO
|
||||
} from "./gateway-types";
|
||||
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
|
||||
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
|
||||
|
||||
type TGatewayServiceFactoryDep = {
|
||||
gatewayDAL: TGatewayDALFactory;
|
||||
projectGatewayDAL: TProjectGatewayDALFactory;
|
||||
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
|
||||
};
|
||||
|
||||
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
|
||||
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
export const gatewayServiceFactory = ({
|
||||
gatewayDAL,
|
||||
licenseService,
|
||||
kmsService,
|
||||
permissionService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
}: TGatewayServiceFactoryDep) => {
|
||||
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
// if (!licenseService.onPremFeatures.gateway) {
|
||||
// throw new BadRequestError({
|
||||
// message:
|
||||
// "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
// });
|
||||
// }
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
};
|
||||
|
||||
const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes
|
||||
|
||||
const envCfg = getConfig();
|
||||
await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway handshake failed due to missing instance configuration."
|
||||
});
|
||||
}
|
||||
|
||||
let turnServerUsername = "";
|
||||
let turnServerPassword = "";
|
||||
// keep it in redis for 5mins to avoid generating so many credentials
|
||||
const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
|
||||
if (previousCredential) {
|
||||
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(
|
||||
JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString())
|
||||
);
|
||||
turnServerUsername = el.username;
|
||||
turnServerPassword = el.password;
|
||||
} else {
|
||||
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.GatewayIdentityCredential(actorId),
|
||||
TURN_CRED_EXPIRY,
|
||||
encryptor({
|
||||
plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password }))
|
||||
}).cipherTextBlob.toString("hex")
|
||||
);
|
||||
turnServerUsername = el.username;
|
||||
turnServerPassword = el.password;
|
||||
}
|
||||
|
||||
return {
|
||||
turnServerUsername,
|
||||
turnServerPassword,
|
||||
turnServerRealm: envCfg.GATEWAY_RELAY_REALM,
|
||||
turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS,
|
||||
infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS
|
||||
};
|
||||
};
|
||||
|
||||
const exchangeAllocatedRelayAddress = async ({
|
||||
identityId,
|
||||
identityOrg,
|
||||
relayAddress,
|
||||
identityOrgAuthMethod
|
||||
}: TExchangeAllocatedRelayAddressDTO) => {
|
||||
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
|
||||
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityOrg
|
||||
});
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]);
|
||||
const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg });
|
||||
if (existingGatewayConfig) return existingGatewayConfig;
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
// generate root CA
|
||||
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const rootCaSerialNumber = createSerialNumber();
|
||||
const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey);
|
||||
const rootCaIssuedAt = new Date();
|
||||
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||
const rootCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
name: `O=${identityOrg},CN=Infisical Gateway Root CA`,
|
||||
serialNumber: rootCaSerialNumber,
|
||||
notBefore: rootCaIssuedAt,
|
||||
notAfter: rootCaExpiration,
|
||||
signingAlgorithm: alg,
|
||||
keys: rootCaKeys,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
// generate client ca
|
||||
const clientCaSerialNumber = createSerialNumber();
|
||||
const clientCaIssuedAt = new Date();
|
||||
const clientCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey);
|
||||
|
||||
const clientCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCaSerialNumber,
|
||||
subject: `O=${identityOrg},CN=Client Intermediate CA`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: clientCaIssuedAt,
|
||||
notAfter: clientCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: clientCaKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, 0, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCertSerialNumber,
|
||||
subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
|
||||
issuer: clientCaCert.subject,
|
||||
notAfter: clientCaExpiration,
|
||||
notBefore: clientCaIssuedAt,
|
||||
signingKey: clientCaKeys.privateKey,
|
||||
publicKey: clientKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||
]
|
||||
});
|
||||
const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey);
|
||||
|
||||
// generate gateway ca
|
||||
const gatewayCaSerialNumber = createSerialNumber();
|
||||
const gatewayCaIssuedAt = new Date();
|
||||
const gatewayCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey);
|
||||
const gatewayCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: gatewayCaSerialNumber,
|
||||
subject: `O=${identityOrg},CN=Gateway CA`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: gatewayCaIssuedAt,
|
||||
notAfter: gatewayCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: gatewayCaKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, 0, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(gatewayCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
return orgGatewayConfigDAL.create({
|
||||
orgId: identityOrg,
|
||||
rootCaIssuedAt,
|
||||
rootCaExpiration,
|
||||
rootCaSerialNumber,
|
||||
rootCaKeyAlgorithm,
|
||||
encryptedRootCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: rootCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||
|
||||
clientCaIssuedAt,
|
||||
clientCaExpiration,
|
||||
clientCaSerialNumber,
|
||||
encryptedClientCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: clientCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedClientCaCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(clientCaCert.rawData)
|
||||
}).cipherTextBlob,
|
||||
|
||||
clientCertIssuedAt: clientCaIssuedAt,
|
||||
clientCertExpiration: clientCaExpiration,
|
||||
clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
clientCertSerialNumber,
|
||||
encryptedClientPrivateKey: orgKmsEncryptor({
|
||||
plainText: clientSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedClientCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(clientCert.rawData)
|
||||
}).cipherTextBlob,
|
||||
|
||||
gatewayCaIssuedAt,
|
||||
gatewayCaExpiration,
|
||||
gatewayCaSerialNumber,
|
||||
encryptedGatewayCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: gatewayCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedGatewayCaCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(gatewayCaCert.rawData)
|
||||
}).cipherTextBlob
|
||||
});
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm);
|
||||
const gatewayCaSkObj = crypto.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayCaPrivateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
gatewayCaSkObj.export({ format: "der", type: "pkcs8" }),
|
||||
gatewayCaAlg,
|
||||
true,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const certIssuedAt = new Date();
|
||||
// then need to periodically init
|
||||
const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1));
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||
// san
|
||||
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false)
|
||||
];
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey);
|
||||
const gatewayCertificate = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: `CN=${identityId},O=${identityOrg},OU=Gateway`,
|
||||
issuer: gatewayCaCert.subject,
|
||||
notBefore: certIssuedAt,
|
||||
notAfter: certExpireAt,
|
||||
signingKey: gatewayCaPrivateKey,
|
||||
publicKey: gatewayKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
// just for local development
|
||||
const formatedRelayAddress =
|
||||
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
|
||||
|
||||
await gatewayDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
|
||||
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
|
||||
if (existingGateway) {
|
||||
return gatewayDAL.updateById(existingGateway.id, {
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
issuedAt: certIssuedAt,
|
||||
expiration: certExpireAt,
|
||||
serialNumber,
|
||||
relayAddress: orgKmsEncryptor({
|
||||
plainText: Buffer.from(formatedRelayAddress)
|
||||
}).cipherTextBlob
|
||||
});
|
||||
}
|
||||
|
||||
return gatewayDAL.create({
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
issuedAt: certIssuedAt,
|
||||
expiration: certExpireAt,
|
||||
serialNumber,
|
||||
relayAddress: orgKmsEncryptor({
|
||||
plainText: Buffer.from(formatedRelayAddress)
|
||||
}).cipherTextBlob,
|
||||
identityId,
|
||||
orgGatewayRootCaId: orgGatewayConfig.id,
|
||||
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
|
||||
});
|
||||
});
|
||||
|
||||
const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim();
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
|
||||
certificate: gatewayCertificate.toString("pem"),
|
||||
certificateChain: gatewayCertificateChain
|
||||
};
|
||||
};
|
||||
|
||||
const heartbeat = async ({ orgPermission }: THeartBeatDTO) => {
|
||||
await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` });
|
||||
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const privateKey = crypto
|
||||
.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
})
|
||||
.export({ type: "pkcs8", format: "pem" });
|
||||
|
||||
const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString();
|
||||
const [relayHost, relayPort] = relayAddress.split(":");
|
||||
|
||||
await pingGatewayAndVerify({
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
tlsOptions: {
|
||||
key: privateKey,
|
||||
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||
cert: clientCert.toString("pem")
|
||||
},
|
||||
identityId: orgPermission.id,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() });
|
||||
};
|
||||
|
||||
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) return [];
|
||||
|
||||
const gateways = await gatewayDAL.find({
|
||||
orgGatewayRootCaId: orgGatewayConfig.id
|
||||
});
|
||||
return gateways;
|
||||
};
|
||||
|
||||
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.EditGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
if (projectIds) {
|
||||
await projectGatewayDAL.transaction(async (tx) => {
|
||||
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
|
||||
await projectGatewayDAL.insertMany(
|
||||
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
|
||||
tx
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.DeleteGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
|
||||
await permissionService.getProjectPermission({
|
||||
projectId,
|
||||
actor: projectPermission.type,
|
||||
actorId: projectPermission.id,
|
||||
actorOrgId: projectPermission.orgId,
|
||||
actorAuthMethod: projectPermission.authMethod,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
const gateways = await gatewayDAL.findByProjectId(projectId);
|
||||
return gateways;
|
||||
};
|
||||
|
||||
// this has no permission check and used for dynamic secrets directly
|
||||
// assumes permission check is already done
|
||||
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
|
||||
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
|
||||
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
|
||||
|
||||
const { gatewayId } = projectGateway;
|
||||
const gateway = await gatewayDAL.findById(gatewayId);
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const clientSkObj = crypto.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
return {
|
||||
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
|
||||
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
|
||||
certificate: clientCert.toString("pem"),
|
||||
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||
identityId: gateway.identityId,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getGatewayRelayDetails,
|
||||
exchangeAllocatedRelayAddress,
|
||||
listGateways,
|
||||
getGatewayById,
|
||||
updateGatewayById,
|
||||
deleteGatewayById,
|
||||
getProjectGateways,
|
||||
fnGetGatewayClientTls,
|
||||
heartbeat
|
||||
};
|
||||
};
|
39
backend/src/ee/services/gateway/gateway-types.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
export type TExchangeAllocatedRelayAddressDTO = {
|
||||
identityId: string;
|
||||
identityOrg: string;
|
||||
identityOrgAuthMethod: ActorAuthMethod;
|
||||
relayAddress: string;
|
||||
};
|
||||
|
||||
export type TListGatewaysDTO = {
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TGetGatewayByIdDTO = {
|
||||
id: string;
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TUpdateGatewayByIdDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
projectIds?: string[];
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TDeleteGatewayByIdDTO = {
|
||||
id: string;
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TGetProjectGatewayByIdDTO = {
|
||||
projectId: string;
|
||||
projectPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type THeartBeatDTO = {
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
10
backend/src/ee/services/gateway/org-gateway-config-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TOrgGatewayConfigDALFactory = ReturnType<typeof orgGatewayConfigDALFactory>;
|
||||
|
||||
export const orgGatewayConfigDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.OrgGatewayConfig);
|
||||
return orm;
|
||||
};
|
10
backend/src/ee/services/gateway/project-gateway-dal.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
|
||||
|
||||
export const projectGatewayDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ProjectGateway);
|
||||
return orm;
|
||||
};
|
@@ -51,7 +51,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
pkiEst: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false
|
||||
kmip: false,
|
||||
gateway: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@@ -69,6 +69,7 @@ export type TFeatureSet = {
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: false;
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@@ -32,6 +32,14 @@ export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionGatewayActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
ListGateways = "list-gateways",
|
||||
EditGateways = "edit-gateways",
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@@ -50,7 +58,8 @@ export enum OrgPermissionSubjects {
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@@ -73,6 +82,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
(
|
||||
@@ -180,6 +190,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -264,6 +280,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
@@ -300,6 +321,8 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
@@ -1294,7 +1294,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretMetadata
|
||||
}) => {
|
||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||
if (tagIds?.length) commitTagIds[secretKey] = tagIds;
|
||||
if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds;
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
secretMetadata,
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||
|
||||
export enum PgSqlLock {
|
||||
BootUpMigration = 2023,
|
||||
SuperAdminInit = 2024,
|
||||
KmsRootKeyInit = 2025
|
||||
}
|
||||
export const PgSqlLock = {
|
||||
BootUpMigration: 2023,
|
||||
SuperAdminInit: 2024,
|
||||
KmsRootKeyInit: 2025,
|
||||
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
|
||||
} as const;
|
||||
|
||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
|
||||
@@ -33,7 +36,8 @@ export const KeyStorePrefixes = {
|
||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`identity-access-token-status:${identityAccessTokenId}`,
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
|
||||
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
|
@@ -24,6 +24,7 @@ const databaseReadReplicaSchema = z
|
||||
|
||||
const envSchema = z
|
||||
.object({
|
||||
INFISICAL_PLATFORM_VERSION: zpStr(z.string().optional()),
|
||||
PORT: z.coerce.number().default(IS_PACKAGED ? 8080 : 4000),
|
||||
DISABLE_SECRET_SCANNING: z
|
||||
.enum(["true", "false"])
|
||||
@@ -184,6 +185,14 @@ const envSchema = z
|
||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||
|
||||
/* Gateway----------------------------------------------------------------------------- */
|
||||
GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
|
||||
// aws
|
||||
@@ -208,6 +217,13 @@ const envSchema = z
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_ID: zpStr(z.string().optional()),
|
||||
INF_APP_CONNECTION_AZURE_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
// datadog
|
||||
SHOULD_USE_DATADOG_TRACER: zodStrBool.default("false"),
|
||||
DATADOG_PROFILING_ENABLED: zodStrBool.default("false"),
|
||||
DATADOG_ENV: zpStr(z.string().optional().default("prod")),
|
||||
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
||||
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||
|
||||
/* CORS ----------------------------------------------------------------------------- */
|
||||
|
||||
CORS_ALLOWED_ORIGINS: zpStr(
|
||||
|
29
backend/src/lib/crypto/hashtext.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// used for postgres lock
|
||||
// this is something postgres does under the hood
|
||||
// convert any string to a unique number
|
||||
export const hashtext = (text: string) => {
|
||||
// Convert text to UTF8 bytes array for consistent behavior with PostgreSQL
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(text);
|
||||
|
||||
// Implementation of hash_any
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
result = ((result << 5) + result) ^ bytes[i];
|
||||
// Keep within 32-bit integer range
|
||||
// eslint-disable-next-line no-bitwise
|
||||
result >>>= 0;
|
||||
}
|
||||
|
||||
// Convert to signed 32-bit integer like PostgreSQL
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return result | 0;
|
||||
};
|
||||
|
||||
export const pgAdvisoryLockHashText = (text: string) => {
|
||||
const hash = hashtext(text);
|
||||
// Ensure positive value within PostgreSQL integer range
|
||||
return Math.abs(hash) % 2 ** 31;
|
||||
};
|
264
backend/src/lib/gateway/index.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
const createTLSConnection = (relayHost: string, relayPort: number, tlsOptions: tls.TlsOptions = {}) => {
|
||||
return new Promise<tls.TLSSocket>((resolve, reject) => {
|
||||
// @ts-expect-error this is resolved in next connect
|
||||
const socket = new tls.TLSSocket(null, {
|
||||
rejectUnauthorized: true,
|
||||
...tlsOptions
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
socket.removeAllListeners();
|
||||
socket.end();
|
||||
};
|
||||
|
||||
socket.once("error", (err) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
socket.connect(relayPort, relayHost, () => {
|
||||
resolve(socket);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
type TPingGatewayAndVerifyDTO = {
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: tls.TlsOptions;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export const pingGatewayAndVerify = async ({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions = {},
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
}: TPingGatewayAndVerifyDTO) => {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||
try {
|
||||
const socket = await createTLSConnection(relayHost, relayPort, tlsOptions);
|
||||
socket.setTimeout(2000);
|
||||
|
||||
const pingResult = await new Promise((resolve, reject) => {
|
||||
socket.once("timeout", () => {
|
||||
socket.destroy();
|
||||
reject(new Error("Timeout"));
|
||||
});
|
||||
socket.once("close", () => {
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
socket.once("end", () => {
|
||||
socket.destroy();
|
||||
});
|
||||
socket.once("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
socket.write(Buffer.from("PING\n"), () => {
|
||||
socket.once("data", (data) => {
|
||||
const response = (data as string).toString();
|
||||
const certificate = socket.getPeerCertificate();
|
||||
|
||||
if (certificate.subject.CN !== identityId || certificate.subject.O !== orgId) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid gateway. Certificate not found for ${identityId} in organization ${orgId}`
|
||||
});
|
||||
}
|
||||
|
||||
if (response === "PONG") {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(`Unexpected response: ${response}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.end();
|
||||
return pingResult;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, DEFAULT_RETRY_DELAY);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(lastError);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
|
||||
});
|
||||
};
|
||||
|
||||
interface TProxyServer {
|
||||
server: net.Server;
|
||||
port: number;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
const setupProxyServer = ({
|
||||
targetPort,
|
||||
targetHost,
|
||||
tlsOptions = {},
|
||||
relayHost,
|
||||
relayPort
|
||||
}: {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayPort: number;
|
||||
relayHost: string;
|
||||
tlsOptions: tls.TlsOptions;
|
||||
}): Promise<TProxyServer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
server.on("connection", async (clientSocket) => {
|
||||
try {
|
||||
const targetSocket = await createTLSConnection(relayHost, relayPort, tlsOptions);
|
||||
|
||||
targetSocket.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`), () => {
|
||||
clientSocket.on("data", (data) => {
|
||||
const flushed = targetSocket.write(data);
|
||||
if (!flushed) {
|
||||
clientSocket.pause();
|
||||
targetSocket.once("drain", () => {
|
||||
clientSocket.resume();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
targetSocket.on("data", (data) => {
|
||||
const flushed = clientSocket.write(data as string);
|
||||
if (!flushed) {
|
||||
targetSocket.pause();
|
||||
clientSocket.once("drain", () => {
|
||||
targetSocket.resume();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
clientSocket?.unpipe();
|
||||
clientSocket?.end();
|
||||
targetSocket?.unpipe();
|
||||
targetSocket?.end();
|
||||
};
|
||||
|
||||
clientSocket.on("error", (err) => {
|
||||
logger.error(err, "Client socket error");
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
targetSocket.on("error", (err) => {
|
||||
logger.error(err, "Target socket error");
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
clientSocket.on("end", cleanup);
|
||||
targetSocket.on("end", cleanup);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to establish target connection:");
|
||||
clientSocket.end();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("Failed to get server port"));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Gateway proxy started");
|
||||
resolve({
|
||||
server,
|
||||
port: address.port,
|
||||
cleanup: () => {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
interface ProxyOptions {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions?: tls.TlsOptions;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export const withGatewayProxy = async (
|
||||
callback: (port: number) => Promise<void>,
|
||||
options: ProxyOptions
|
||||
): Promise<void> => {
|
||||
const {
|
||||
relayHost,
|
||||
relayPort,
|
||||
targetHost,
|
||||
targetPort,
|
||||
tlsOptions = {},
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
} = options;
|
||||
|
||||
// First, try to ping the gateway
|
||||
await pingGatewayAndVerify({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions,
|
||||
maxRetries,
|
||||
identityId,
|
||||
orgId
|
||||
});
|
||||
|
||||
// Setup the proxy server
|
||||
const { port, cleanup } = await setupProxyServer({ targetHost, targetPort, relayPort, relayHost, tlsOptions });
|
||||
|
||||
try {
|
||||
// Execute the callback with the allocated port
|
||||
await callback(port);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to proxy");
|
||||
throw new BadRequestError({ message: (err as Error)?.message });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
cleanup();
|
||||
}
|
||||
};
|
@@ -6,6 +6,7 @@ import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
||||
import { Resource } from "@opentelemetry/resources";
|
||||
import { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
||||
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
||||
import tracer from "dd-trace";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
import { initEnvConfig } from "../config/env";
|
||||
@@ -86,6 +87,17 @@ const setupTelemetry = () => {
|
||||
exportType: appCfg.OTEL_EXPORT_TYPE
|
||||
});
|
||||
}
|
||||
|
||||
if (appCfg.SHOULD_USE_DATADOG_TRACER) {
|
||||
console.log("Initializing Datadog tracer");
|
||||
tracer.init({
|
||||
profiling: appCfg.DATADOG_PROFILING_ENABLED,
|
||||
version: appCfg.INFISICAL_PLATFORM_VERSION,
|
||||
env: appCfg.DATADOG_ENV,
|
||||
service: appCfg.DATADOG_SERVICE,
|
||||
hostname: appCfg.DATADOG_HOSTNAME
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
void setupTelemetry();
|
||||
|
16
backend/src/lib/turn/credentials.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
||||
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
||||
const username = `${timestamp}:${id}`;
|
||||
|
||||
const hmac = crypto.createHmac("sha1", authSecret);
|
||||
hmac.update(username);
|
||||
const password = hmac.digest("base64");
|
||||
|
||||
return {
|
||||
username,
|
||||
password
|
||||
};
|
||||
};
|
@@ -27,6 +27,10 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-
|
||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
||||
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
|
||||
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@@ -393,6 +397,10 @@ export const registerRoutes = async (
|
||||
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
|
||||
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
|
||||
|
||||
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@@ -1300,7 +1308,19 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const gatewayService = gatewayServiceFactory({
|
||||
permissionService,
|
||||
gatewayDAL,
|
||||
kmsService,
|
||||
licenseService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||
gatewayService
|
||||
});
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
queueService,
|
||||
dynamicSecretLeaseDAL,
|
||||
@@ -1318,8 +1338,10 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
@@ -1557,7 +1579,8 @@ export const registerRoutes = async (
|
||||
appConnection: appConnectionService,
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||
@@ -72,7 +73,21 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
message: "At least one login method should be enabled."
|
||||
}),
|
||||
slackClientId: z.string().optional(),
|
||||
slackClientSecret: z.string().optional()
|
||||
slackClientSecret: z.string().optional(),
|
||||
authConsentContent: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((content) => DOMPurify.sanitize(content) === content, {
|
||||
message: "Auth consent content contains unsafe HTML."
|
||||
})
|
||||
.optional(),
|
||||
pageFrameContent: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine((content) => DOMPurify.sanitize(content) === content, {
|
||||
message: "Page frame content contains unsafe HTML."
|
||||
})
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
|
@@ -537,7 +537,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||
}),
|
||||
response: {
|
||||
200: z.union([
|
||||
@@ -640,7 +645,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderRepeatDays: z
|
||||
.number()
|
||||
.optional()
|
||||
@@ -2053,7 +2063,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
||||
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
secretReminderRepeatDays: z
|
||||
.number()
|
||||
|
17
cli/go.mod
@@ -18,14 +18,16 @@ require (
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
|
||||
github.com/pion/logging v0.2.3
|
||||
github.com/pion/turn/v4 v4.0.0
|
||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/term v0.29.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@@ -81,6 +83,10 @@ require (
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
@@ -88,6 +94,7 @@ require (
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@@ -98,9 +105,9 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
|
34
cli/go.sum
@@ -347,6 +347,18 @@ github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5d
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
|
||||
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -410,6 +422,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
@@ -458,8 +472,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -560,8 +574,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -610,11 +624,11 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -624,8 +638,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@@ -544,3 +544,59 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallRegisterGatewayIdentityV1(httpClient *resty.Client) (*GetRelayCredentialsResponseV1, error) {
|
||||
var resBody GetRelayCredentialsResponseV1
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/register-identity", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return &resBody, nil
|
||||
}
|
||||
|
||||
func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCertRequestV1) (*ExchangeRelayCertResponseV1, error) {
|
||||
var resBody ExchangeRelayCertResponseV1
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
SetBody(request).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/exchange-cert", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return &resBody, nil
|
||||
}
|
||||
|
||||
func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/heartbeat", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("CallGatewayHeartBeatV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return fmt.Errorf("CallGatewayHeartBeatV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -629,3 +629,22 @@ type GetRawSecretV3ByNameResponse struct {
|
||||
} `json:"secret"`
|
||||
ETag string
|
||||
}
|
||||
|
||||
type GetRelayCredentialsResponseV1 struct {
|
||||
TurnServerUsername string `json:"turnServerUsername"`
|
||||
TurnServerPassword string `json:"turnServerPassword"`
|
||||
TurnServerRealm string `json:"turnServerRealm"`
|
||||
TurnServerAddress string `json:"turnServerAddress"`
|
||||
InfisicalStaticIp string `json:"infisicalStaticIp"`
|
||||
}
|
||||
|
||||
type ExchangeRelayCertRequestV1 struct {
|
||||
RelayAddress string `json:"relayAddress"`
|
||||
}
|
||||
|
||||
type ExchangeRelayCertResponseV1 struct {
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Certificate string `json:"certificate"`
|
||||
CertificateChain string `json:"certificateChain"`
|
||||
}
|
||||
|
120
cli/packages/cmd/gateway.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/api"
|
||||
// "github.com/Infisical/infisical-merge/packages/models"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/gateway"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
||||
// "github.com/rs/zerolog/log"
|
||||
|
||||
// "github.com/go-resty/resty/v2"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var gatewayCmd = &cobra.Command{
|
||||
Example: `infisical gateway`,
|
||||
Short: "Used to infisical gateway",
|
||||
Use: "gateway",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
util.HandleError(fmt.Errorf("Token not found"))
|
||||
}
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
sigStopCh := make(chan bool, 1)
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
close(sigStopCh)
|
||||
cancel()
|
||||
|
||||
// If we get a second signal, force exit
|
||||
<-sigCh
|
||||
log.Warn().Msgf("Force exit triggered")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
// Main gateway retry loop with proper context handling
|
||||
retryTicker := time.NewTicker(5 * time.Second)
|
||||
defer retryTicker.Stop()
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
gatewayInstance, err := gateway.NewGateway(token.Token)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if err = gatewayInstance.ConnectWithRelay(); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().Msgf("Gateway connection error with relay: %s", err)
|
||||
log.Info().Msg("Retrying connection in 5 seconds...")
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = gatewayInstance.Listen(ctx)
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Gateway shutdown complete")
|
||||
return
|
||||
}
|
||||
log.Error().Msgf("Gateway listen error: %s", err)
|
||||
log.Info().Msg("Retrying connection in 5 seconds...")
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
gatewayCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
|
||||
command.Flags().MarkHidden("domain")
|
||||
command.Parent().HelpFunc()(command, strings)
|
||||
})
|
||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||
|
||||
rootCmd.AddCommand(gatewayCmd)
|
||||
}
|
107
cli/packages/gateway/connection.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
log.Info().Msgf("New connection from: %s", conn.RemoteAddr().String())
|
||||
|
||||
// Use buffered reader for better handling of fragmented data
|
||||
reader := bufio.NewReader(conn)
|
||||
for {
|
||||
msg, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
log.Error().Msgf("Error reading command: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := bytes.ToUpper(bytes.TrimSpace(bytes.Split(msg, []byte(" "))[0]))
|
||||
args := bytes.TrimSpace(bytes.TrimPrefix(msg, cmd))
|
||||
|
||||
switch string(cmd) {
|
||||
case "FORWARD-TCP":
|
||||
proxyAddress := string(bytes.Split(args, []byte(" "))[0])
|
||||
destTarget, err := net.Dial("tcp", proxyAddress)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to connect to target: %v", err)
|
||||
return
|
||||
}
|
||||
defer destTarget.Close()
|
||||
|
||||
// Handle buffered data
|
||||
buffered := reader.Buffered()
|
||||
if buffered > 0 {
|
||||
bufferedData := make([]byte, buffered)
|
||||
_, err := reader.Read(bufferedData)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Error reading buffered data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = destTarget.Write(bufferedData); err != nil {
|
||||
log.Error().Msgf("Error writing buffered data: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
CopyData(conn, destTarget)
|
||||
return
|
||||
case "PING":
|
||||
if _, err := conn.Write([]byte("PONG")); err != nil {
|
||||
log.Error().Msgf("Error writing PONG response: %v", err)
|
||||
}
|
||||
return
|
||||
default:
|
||||
log.Error().Msgf("Unknown command: %s", string(cmd))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CloseWrite interface {
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
func CopyData(src, dst net.Conn) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
copyAndClose := func(dst, src net.Conn, done chan<- bool) {
|
||||
defer wg.Done()
|
||||
_, err := io.Copy(dst, src)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Error().Msgf("Copy error: %v", err)
|
||||
}
|
||||
|
||||
// Signal we're done writing
|
||||
done <- true
|
||||
|
||||
// Half close the connection if possible
|
||||
if c, ok := dst.(CloseWrite); ok {
|
||||
c.CloseWrite()
|
||||
}
|
||||
}
|
||||
|
||||
done1 := make(chan bool, 1)
|
||||
done2 := make(chan bool, 1)
|
||||
|
||||
go copyAndClose(dst, src, done1)
|
||||
go copyAndClose(src, dst, done2)
|
||||
|
||||
// Wait for both copies to complete
|
||||
<-done1
|
||||
<-done2
|
||||
wg.Wait()
|
||||
}
|
350
cli/packages/gateway/gateway.go
Normal file
@@ -0,0 +1,350 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/turn/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GatewayConfig struct {
|
||||
TurnServerUsername string
|
||||
TurnServerPassword string
|
||||
TurnServerAddress string
|
||||
InfisicalStaticIp string
|
||||
SerialNumber string
|
||||
PrivateKey string
|
||||
Certificate string
|
||||
CertificateChain string
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
httpClient *resty.Client
|
||||
config *GatewayConfig
|
||||
client *turn.Client
|
||||
}
|
||||
|
||||
func NewGateway(identityToken string) (Gateway, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(identityToken)
|
||||
|
||||
return Gateway{
|
||||
httpClient: httpClient,
|
||||
config: &GatewayConfig{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) ConnectWithRelay() error {
|
||||
relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1]
|
||||
var conn net.Conn
|
||||
|
||||
// Dial TURN Server
|
||||
if relayPort == "5349" {
|
||||
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
||||
conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: relayAddress,
|
||||
})
|
||||
} else {
|
||||
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
|
||||
peerAddr, err := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse turn server address: %w", err)
|
||||
}
|
||||
conn, err = net.DialTCP("tcp", nil, peerAddr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
||||
}
|
||||
|
||||
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
||||
// This allows us to simulate datagram based communication over a net.Conn
|
||||
cfg := &turn.ClientConfig{
|
||||
STUNServerAddr: relayDetails.TurnServerAddress,
|
||||
TURNServerAddr: relayDetails.TurnServerAddress,
|
||||
Conn: turn.NewSTUNConn(conn),
|
||||
Username: relayDetails.TurnServerUsername,
|
||||
Password: relayDetails.TurnServerPassword,
|
||||
Realm: relayDetails.TurnServerRealm,
|
||||
LoggerFactory: logging.NewDefaultLoggerFactory(),
|
||||
}
|
||||
|
||||
client, err := turn.NewClient(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create relay client: %w", err)
|
||||
}
|
||||
|
||||
g.config = &GatewayConfig{
|
||||
TurnServerUsername: relayDetails.TurnServerUsername,
|
||||
TurnServerPassword: relayDetails.TurnServerPassword,
|
||||
TurnServerAddress: relayDetails.TurnServerAddress,
|
||||
InfisicalStaticIp: relayDetails.InfisicalStaticIp,
|
||||
}
|
||||
// if port not specific allow all port
|
||||
if relayDetails.InfisicalStaticIp != "" && !strings.Contains(relayDetails.InfisicalStaticIp, ":") {
|
||||
g.config.InfisicalStaticIp = g.config.InfisicalStaticIp + ":0"
|
||||
}
|
||||
|
||||
g.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) Listen(ctx context.Context) error {
|
||||
defer g.client.Close()
|
||||
err := g.client.Listen()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to listen to relay server: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg("Connected with relay")
|
||||
|
||||
// Allocate a relay socket on the TURN server. On success, it
|
||||
// will return a net.PacketConn which represents the remote
|
||||
// socket.
|
||||
relayNonTlsConn, err := g.client.AllocateTCP()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to allocate relay connection: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg(relayNonTlsConn.Addr().String())
|
||||
defer func() {
|
||||
if closeErr := relayNonTlsConn.Close(); closeErr != nil {
|
||||
log.Error().Msgf("Failed to close connection: %s", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{
|
||||
RelayAddress: relayNonTlsConn.Addr().String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.config.SerialNumber = gatewayCert.SerialNumber
|
||||
g.config.PrivateKey = gatewayCert.PrivateKey
|
||||
g.config.Certificate = gatewayCert.Certificate
|
||||
g.config.CertificateChain = gatewayCert.CertificateChain
|
||||
|
||||
shutdownCh := make(chan bool, 1)
|
||||
|
||||
if g.config.InfisicalStaticIp != "" {
|
||||
log.Info().Msgf("Found static ip from Infisical: %s. Creating permission IP lifecycle", g.config.InfisicalStaticIp)
|
||||
peerAddr, err := net.ResolveTCPAddr("tcp", g.config.InfisicalStaticIp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse infisical static ip: %w", err)
|
||||
}
|
||||
g.registerPermissionLifecycle(func() error {
|
||||
err := relayNonTlsConn.CreatePermissions(peerAddr)
|
||||
return err
|
||||
}, shutdownCh)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse cert: %s", err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain))
|
||||
|
||||
relayConn := tls.NewListener(relayNonTlsConn, &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
})
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
log.Info().Msg("Gateway started successfully")
|
||||
g.registerHeartBeat(errCh, shutdownCh)
|
||||
g.registerRelayIsActive(relayNonTlsConn.Addr().String(), errCh, shutdownCh)
|
||||
|
||||
// Create a WaitGroup to track active connections
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if relayDeadlineConn, ok := relayConn.(*net.TCPListener); ok {
|
||||
relayDeadlineConn.SetDeadline(time.Now().Add(1 * time.Second))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-shutdownCh:
|
||||
return
|
||||
default:
|
||||
// Accept new relay connection
|
||||
conn, err := relayConn.Accept()
|
||||
if err != nil {
|
||||
// Check if it's a timeout error (which we expect due to our deadline)
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "data contains incomplete STUN or TURN frame") {
|
||||
log.Error().Msgf("Failed to accept connection: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
tlsConn, ok := conn.(*tls.Conn)
|
||||
if !ok {
|
||||
log.Error().Msg("Failed to convert to TLS connection")
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Set a deadline for the handshake to prevent hanging
|
||||
tlsConn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
err = tlsConn.Handshake()
|
||||
// Clear the deadline after handshake
|
||||
tlsConn.SetDeadline(time.Time{})
|
||||
if err != nil {
|
||||
log.Error().Msgf("TLS handshake failed: %v", err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Get connection state which contains certificate information
|
||||
state := tlsConn.ConnectionState()
|
||||
if len(state.PeerCertificates) > 0 {
|
||||
organizationUnit := state.PeerCertificates[0].Subject.OrganizationalUnit
|
||||
commonName := state.PeerCertificates[0].Subject.CommonName
|
||||
if organizationUnit[0] != "gateway-client" || commonName != "cloud" {
|
||||
log.Error().Msgf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the connection in a goroutine
|
||||
wg.Add(1)
|
||||
go func(c net.Conn) {
|
||||
defer wg.Done()
|
||||
defer c.Close()
|
||||
|
||||
// Monitor parent context to close this connection when needed
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.Close() // Force close connection when context is canceled
|
||||
case <-shutdownCh:
|
||||
c.Close() // Force close connection when accepting loop is done
|
||||
}
|
||||
}()
|
||||
|
||||
handleConnection(c)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway...")
|
||||
case err = <-errCh:
|
||||
}
|
||||
|
||||
// Signal the accept loop to stop
|
||||
close(shutdownCh)
|
||||
|
||||
// Set a timeout for waiting on connections to close
|
||||
waitCh := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
// All connections closed normally
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Warn().Msg("Timeout waiting for connections to close gracefully")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *Gateway) registerHeartBeat(errCh chan error, done chan bool) {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
log.Info().Msg("Registering first heart beat")
|
||||
err := api.CallGatewayHeartBeatV1(g.httpClient)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to register heartbeat: %s", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Info().Msg("Registering heart beat")
|
||||
err := api.CallGatewayHeartBeatV1(g.httpClient)
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *Gateway) registerPermissionLifecycle(permissionFn func() error, done chan bool) {
|
||||
ticker := time.NewTicker(3 * time.Minute)
|
||||
|
||||
go func() {
|
||||
// wait for 5 mins
|
||||
permissionFn()
|
||||
log.Printf("Created permission for incoming connections")
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
permissionFn()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *Gateway) registerRelayIsActive(serverAddr string, errCh chan error, done chan bool) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
conn, err := net.Dial("tcp", serverAddr)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
@@ -12,18 +12,15 @@ Plus, our team is remote-first and spread across the globe (from San Francisco t
|
||||
|
||||
## Onboarding buddy
|
||||
|
||||
Every new joiner has an onboarding buddy who should ideally be in the the same timezone. The onboarding buddy should be able to help with any questions that pop up during the first few weeks. Of course, everyone is available to help, but it's good to have a dedicated person that you can go to with any questions.
|
||||
Every new joiner at Infisical will have an onboarding buddy—a teammate in a similar time zone who’s there to help you settle in. They are your go-to person for any questions that come up. Of course, everyone on the team is happy to help, but it’s always nice to have a dedicated person who’s there for you. Don’t hesitate to reach out to your buddy if you’re unsure about something or need a hand! Your onboarding buddy will set up regular syncs for your first two months—ideally at least 2-3 times a week.
|
||||
|
||||
If you’re joining the engineering team, your onboarding buddy will:
|
||||
1. Walk you through Infisical’s development process and share any best practices to keep in mind when tackling tickets.
|
||||
2. Be your go-to person if you are blocked or need to think through your sprint task.
|
||||
3. Help you ship something small on day one!
|
||||
|
||||
## Onboarding Checklist
|
||||
|
||||
1. Join the weekly all-hands meeting. It typically happens on Monday's at 8:30am PT.
|
||||
2. Ship something together on day one – even if tiny! It feels great to hit the ground running, with a development environment all ready to go.
|
||||
3. Check out the [Areas of Responsibility (AoR) Table](https://docs.google.com/spreadsheets/d/1RnXlGFg83Sgu0dh7ycuydsSobmFfI3A0XkGw7vrVxEI/edit?usp=sharing). This is helpful to know who you can ask about particular areas of Infisical. Feel free to add yourself to the areas you'd be most interesting to dive into.
|
||||
4. Read the [Infisical Strategy Doc](https://docs.google.com/document/d/1uV9IaahYwbZ5OuzDTFdQMSa1P0mpMOnetGB-xqf4G40).
|
||||
5. Update your LinkedIn profile with one of [Infisical's official banners](https://drive.google.com/drive/u/0/folders/1oSNWjbpRl9oNYwxM_98IqzKs9fAskrb2) (if you want to). You can also coordinate your social posts in the #marketing Slack channel, so that we can boost it from Infisical's official social media accounts.
|
||||
6. Over the first few weeks, feel free to schedule 1:1s with folks on the team to get to know them a bit better.
|
||||
7. Change your Slack username in the users channel to `[NAME] (Infisical)`.
|
||||
8. Go through the [technical overview](https://infisical.com/docs/internals/overview) of Infisical.
|
||||
9. Request a company credit card (Maidul will be able to help with that).
|
||||
Your hiring manager will send you an onboarding checklist doc for your first day.
|
||||
|
||||
|
||||
|
@@ -24,6 +24,9 @@ Make sure you keep copies for all receipts. If you expense something on a compan
|
||||
|
||||
You should default to using your company card in all cases - it has no transaction fees. If using your personal card is unavoidable, please reach out to Maidul to get it reimbursed manually.
|
||||
|
||||
## Training
|
||||
|
||||
For engineers, you’re welcome to take an approved Udemy course. Please reach out to Maidul. For the GTM team, you may buy a book a month if it’s relevant to your work.
|
||||
|
||||
# Equipment
|
||||
|
||||
@@ -55,4 +58,4 @@ For any equipment related questions, please reach out to Maidul.
|
||||
|
||||
## Brex
|
||||
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
||||
We use Brex as our primary credit card provider. Don't have a company card yet? Reach out to Maidul.
|
||||
|
@@ -56,4 +56,3 @@ volumes:
|
||||
|
||||
networks:
|
||||
infisical:
|
||||
|
||||
|
@@ -11,13 +11,6 @@ To interact with the Infisical API, you will need to obtain an access token. Fol
|
||||
**FAQ**
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What happened to the Service Token and API Key authentication modes?">
|
||||
The Service Token and API Key authentication modes are being deprecated out in favor of [Identities](/documentation/platform/identity).
|
||||
We expect to make a deprecation notice in the coming months alongside a larger deprecation initiative planned for Q1/Q2 2024.
|
||||
|
||||
With identities, we're improving significantly over the shortcomings of Service Tokens and API Keys. Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based
|
||||
permission system used by users, and comes with ample more configurable security measures.
|
||||
</Accordion>
|
||||
<Accordion title="Why can I not create, read, update, or delete an identity?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
|
@@ -7,21 +7,22 @@ The Server Admin Console provides **server administrators** with the ability to
|
||||
customize settings and manage users for their entire Infisical instance.
|
||||
|
||||
<Note>
|
||||
The first user to setup an account on your Infisical instance is designated as the server administrator by default.
|
||||
The first user to setup an account on your Infisical instance is designated as
|
||||
the server administrator by default.
|
||||
</Note>
|
||||
|
||||
## Accessing the Server Admin Console
|
||||
|
||||
|
||||
On the sidebar, tap on your initials to access the settings dropdown and press the **Server Admin Console** option.
|
||||
|
||||

|
||||
|
||||
## General Tab
|
||||
|
||||
Configure general settings for your instance.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Allow User Signups
|
||||
|
||||
@@ -39,6 +40,13 @@ If you're using SAML/LDAP/OIDC for only one organization on your instance, you c
|
||||
|
||||
By default, users signing up through SAML/LDAP/OIDC will still need to verify their email address to prevent email spoofing. This requirement can be skipped by enabling the switch to trust logins through the respective method.
|
||||
|
||||
### Notices
|
||||
|
||||
Auth consent content is displayed to users on the login page. They can be used to display important information to users, such as a maintenance message or a new feature announcement.
|
||||

|
||||
|
||||
Page frame content is displayed as a header and footer in ALL protected pages.
|
||||

|
||||
|
||||
## Authentication Tab
|
||||
|
||||
@@ -46,16 +54,15 @@ From this tab, you can configure which login methods are enabled for your instan
|
||||
|
||||

|
||||
|
||||
|
||||
## Rate Limit Tab
|
||||
|
||||
This tab allows you to set various rate limits for your Infisical instance. You do not need to redeploy when making changes to rate limits as these will be propagated automatically.
|
||||
|
||||

|
||||
|
||||
|
||||
<Note>
|
||||
Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use.
|
||||
Note that rate limit configuration is a paid feature. Please contact
|
||||
sales@infisical.com to purchase a license for its use.
|
||||
</Note>
|
||||
|
||||
## User Management Tab
|
||||
@@ -65,5 +72,6 @@ From this tab, you can view all the users who have signed up for your instance.
|
||||

|
||||
|
||||
<Note>
|
||||
Note that rate limit configuration is a paid feature. Please contact sales@infisical.com to purchase a license for its use.
|
||||
Note that rate limit configuration is a paid feature. Please contact
|
||||
sales@infisical.com to purchase a license for its use.
|
||||
</Note>
|
||||
|
81
docs/documentation/platform/gateways/overview.mdx
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Gateway"
|
||||
sidebarTitle: "Overview"
|
||||
description: "How to access private network resources from Infisical"
|
||||
---
|
||||
|
||||
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
|
||||
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
|
||||
Common use cases include generating dynamic credentials or rotating credentials for private databases.
|
||||
|
||||
<Info>
|
||||
**Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is
|
||||
available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please
|
||||
contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an
|
||||
enterprise license.
|
||||
</Info>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network.
|
||||
It’s a lightweight daemon packaged within the Infisical CLI, making it easy to deploy and manage. Once set up, the Gateway establishes a connection with a relay server, ensuring that all communication between Infisical and your Gateway is fully end-to-end encrypted.
|
||||
This setup guarantees that only the platform and your Gateway can decrypt the transmitted information, keeping communication with your resources secure, private and isolated.
|
||||
|
||||
## Deployment
|
||||
|
||||
The Infisical Gateway is seamlessly integrated into the Infisical CLI under the `gateway` command, making it simple to deploy and manage.
|
||||
You can install the Gateway in all the same ways you install the Infisical CLI—whether via npm, Docker, or a binary.
|
||||
For detailed installation instructions, refer to the Infisical [CLI Installation instructions](/cli/overview).
|
||||
|
||||
To function, the Gateway must authenticate with Infisical. This requires a machine identity configured with the appropriate permissions to create and manage a Gateway.
|
||||
Once authenticated, the Gateway establishes a secure connection with Infisical to allow your private resources to be reachable.
|
||||
|
||||
### Deployment process
|
||||
|
||||
<Steps>
|
||||
<Step title="Create a Gateway Identity">
|
||||
1. Navigate to **Organization Access Control** in your Infisical dashboard.
|
||||
2. Create a dedicated machine identity for your Gateway.
|
||||
3. **Best Practice:** Assign a unique identity to each Gateway for better security and management.
|
||||

|
||||
</Step>
|
||||
|
||||
<Step title="Configure Authentication Method">
|
||||
You'll need to choose an authentication method to initiate communication with Infisical. View the available machine identity authentication methods [here](/documentation/platform/identities/machine-identities).
|
||||
</Step>
|
||||
|
||||
<Step title="Deploy the Gateway">
|
||||
Use the Infisical CLI to deploy the Gateway. You can log in with your machine identity and start the Gateway in one command. The example below demonstrates how to deploy the Gateway using the Universal Auth method:
|
||||
```bash
|
||||
infisical gateway --token $(infisical login --method=universal-auth --client-id=<> --client-secret=<> --plain)
|
||||
```
|
||||
Alternatively, if you already have the token, use it directly with the `--token` flag:
|
||||
```bash
|
||||
infisical gateway --token <your-machine-identity-token>
|
||||
```
|
||||
Or set it as an environment variable:
|
||||
```bash
|
||||
export INFISICAL_TOKEN=<your-machine-identity-token>
|
||||
infisical gateway
|
||||
```
|
||||
<Note>
|
||||
Ensure the deployed Gateway has network access to the private resources you intend to connect with Infisical.
|
||||
</Note>
|
||||
</Step>
|
||||
|
||||
<Step title="Verify Gateway Deployment">
|
||||
To confirm your Gateway is working, check the deployment status by looking for the message **"Gateway started successfully"** in the Gateway logs. This indicates the Gateway is running properly. Next, verify its registration by opening your Infisical dashboard, navigating to **Organization Access Control**, and selecting the **Gateways** tab. Your newly deployed Gateway should appear in the list.
|
||||

|
||||
</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**.
|
||||

|
||||
In the edit modal that appears, choose the projects you want the Gateway to access and click **Save** to confirm your selections.
|
||||

|
||||
Once added to a project, the Gateway becomes available for use by any feature that supports Gateways within that project.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
BIN
docs/images/platform/admin-panels/admin-panel-general-1.png
Normal file
After Width: | Height: | Size: 477 KiB |
BIN
docs/images/platform/admin-panels/auth-consent-usage.png
Normal file
After Width: | Height: | Size: 392 KiB |
BIN
docs/images/platform/admin-panels/page-frame-usage.png
Normal file
After Width: | Height: | Size: 364 KiB |
BIN
docs/images/platform/gateways/assign-project.png
Normal file
After Width: | Height: | Size: 338 KiB |
BIN
docs/images/platform/gateways/create-identity-for-gateway.png
Normal file
After Width: | Height: | Size: 375 KiB |
BIN
docs/images/platform/gateways/dynamic-secret.png
Normal file
After Width: | Height: | Size: 384 KiB |
BIN
docs/images/platform/gateways/edit-gateway.png
Normal file
After Width: | Height: | Size: 365 KiB |
BIN
docs/images/platform/gateways/gateway-list.png
Normal file
After Width: | Height: | Size: 433 KiB |
@@ -3,18 +3,21 @@ sidebarTitle: "InfisicalDynamicSecret CRD"
|
||||
title: "Using the InfisicalDynamicSecret CRD"
|
||||
description: "Learn how to generate dynamic secret leases in Infisical and sync them to your Kubernetes cluster."
|
||||
---
|
||||
## Overview
|
||||
|
||||
The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources.
|
||||
This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret.
|
||||
## Overview
|
||||
|
||||
The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources.
|
||||
This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret.
|
||||
|
||||
This CRD offers the following features:
|
||||
|
||||
- **Generate a dynamic secret lease** in Infisical and track its lifecycle.
|
||||
- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret.
|
||||
- **Automatically rotate** the dynamic secret value before it expires to make sure your cluster always has valid credentials.
|
||||
- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A project within Infisical.
|
||||
- A [machine identity](/docs/documentation/platform/identities/overview) ready for use in Infisical that has permissions to create dynamic secret leases in the project.
|
||||
- You have already configured a dynamic secret in Infisical.
|
||||
@@ -77,16 +80,19 @@ spec:
|
||||
```
|
||||
|
||||
Apply the InfisicalDynamicSecret CRD to your cluster.
|
||||
|
||||
```bash
|
||||
kubectl apply -f dynamic-secret-crd.yaml
|
||||
```
|
||||
|
||||
After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing:
|
||||
|
||||
```bash
|
||||
kubectl get secret <managed-secret-name> -o yaml
|
||||
```
|
||||
|
||||
After getting the secret, you should should see that the secret has data that contains the lease credentials.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
@@ -102,7 +108,7 @@ kind: Secret
|
||||
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
|
||||
` https://your-self-hosted-instace.com/api`
|
||||
|
||||
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
|
||||
<Accordion title="Advanced use case">
|
||||
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
|
||||
@@ -120,36 +126,45 @@ kind: Secret
|
||||
<Accordion title="leaseTTL">
|
||||
The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret.
|
||||
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
|
||||
The following units are supported:
|
||||
- `s` for seconds (must be at least 5 seconds)
|
||||
- `m` for minutes
|
||||
- `h` for hours
|
||||
- `d` for days
|
||||
The following units are supported:
|
||||
|
||||
<Note>
|
||||
The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret.
|
||||
</Note>
|
||||
</Accordion>
|
||||
- `s` for seconds (must be at least 5 seconds)
|
||||
- `m` for minutes
|
||||
- `h` for hours
|
||||
- `d` for days
|
||||
|
||||
<Note>
|
||||
The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret.
|
||||
|
||||
</Note>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="managedSecretReference">
|
||||
The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
managedSecretReference:
|
||||
secretName: <secret-name>
|
||||
secretNamespace: default
|
||||
```
|
||||
```yaml
|
||||
spec:
|
||||
managedSecretReference:
|
||||
secretName: <secret-name>
|
||||
secretNamespace: default
|
||||
```
|
||||
|
||||
<Accordion title="managedSecretReference.secretName">
|
||||
The name of the Kubernetes secret where the dynamic secret lease should be stored.
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="managedSecretReference.secretNamespace">
|
||||
The namespace of the Kubernetes secret where the dynamic secret lease should be stored.
|
||||
</Accordion>
|
||||
<Accordion title="managedSecretReference.secretName">
|
||||
The name of the Kubernetes secret where the dynamic secret lease should be
|
||||
stored.
|
||||
</Accordion>
|
||||
|
||||
{" "}
|
||||
|
||||
<Accordion title="managedSecretReference.secretNamespace">
|
||||
The namespace of the Kubernetes secret where the dynamic secret lease should
|
||||
be stored.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="managedSecretReference.creationPolicy">
|
||||
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
|
||||
@@ -165,32 +180,36 @@ kind: Secret
|
||||
</Tip>
|
||||
|
||||
This field is optional.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="managedSecretReference.secretType">
|
||||
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
|
||||
|
||||
This field is optional.
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="leaseRevocationPolicy">
|
||||
|
||||
The field is optional and will default to `None` if not defined.
|
||||
The field is optional and will default to `None` if not defined.
|
||||
|
||||
The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted.
|
||||
The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted.
|
||||
|
||||
Valid values are `None` and `Revoke`.
|
||||
Valid values are `None` and `Revoke`.
|
||||
|
||||
Behavior of each policy:
|
||||
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
|
||||
- `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted.
|
||||
Behavior of each policy:
|
||||
|
||||
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
|
||||
- `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted.
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
leaseRevocationPolicy: Revoke
|
||||
```
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
leaseRevocationPolicy: Revoke
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="dynamicSecret">
|
||||
@@ -205,29 +224,37 @@ kind: Secret
|
||||
secretsPath: <secrets-path>
|
||||
```
|
||||
|
||||
<Accordion title="dynamicSecret.secretName">
|
||||
The name of the dynamic secret.
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.projectId">
|
||||
The project ID of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
<Accordion title="dynamicSecret.secretName">
|
||||
The name of the dynamic secret.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="dynamicSecret.environmentSlug">
|
||||
The environment slug of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.secretsPath">
|
||||
The path of where the dynamic secret is stored in Infisical. The root path is `/`.
|
||||
</Accordion>
|
||||
<Accordion title="dynamicSecret.projectId">
|
||||
The project ID of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.environmentSlug">
|
||||
The environment slug of where the dynamic secret is stored in Infisical.
|
||||
</Accordion>
|
||||
|
||||
{" "}
|
||||
|
||||
<Accordion title="dynamicSecret.secretsPath">
|
||||
The path of where the dynamic secret is stored in Infisical. The root path is
|
||||
`/`.
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="authentication">
|
||||
|
||||
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
|
||||
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
|
||||
|
||||
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
|
||||
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
|
||||
|
||||
<Accordion title="universalAuth">
|
||||
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
|
||||
@@ -246,7 +273,7 @@ kind: Secret
|
||||
spec:
|
||||
universalAuth:
|
||||
credentialsRef:
|
||||
secretName: <secret-name>
|
||||
secretName: <secret-name>
|
||||
secretNamespace: <secret-namespace>
|
||||
```
|
||||
|
||||
@@ -282,6 +309,7 @@ kind: Secret
|
||||
name: <secret-name>
|
||||
namespace: <secret-namespace>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="awsIamAuth">
|
||||
@@ -316,12 +344,12 @@ kind: Secret
|
||||
azureAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="gcpIamAuth">
|
||||
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
|
||||
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
|
||||
|
||||
|
||||
Valid fields:
|
||||
- `identityId`: The identity ID of the machine identity you created.
|
||||
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
|
||||
@@ -334,6 +362,7 @@ kind: Secret
|
||||
identityId: <machine-identity-id>
|
||||
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="gcpIdTokenAuth">
|
||||
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
|
||||
@@ -349,11 +378,11 @@ kind: Secret
|
||||
gcpIdTokenAuth:
|
||||
identityId: <machine-identity-id>
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
<Accordion title="tls">
|
||||
This block defines the TLS settings to use for connecting to the Infisical
|
||||
instance.
|
||||
@@ -376,11 +405,11 @@ kind: Secret
|
||||
secretNamespace: default
|
||||
key: ca.crt
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
### Applying the InfisicalDynamicSecret CRD to your cluster
|
||||
|
||||
Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster.
|
||||
@@ -396,7 +425,7 @@ To address this, we've added functionality to automatically redeploy your deploy
|
||||
|
||||
#### Enabling auto redeploy
|
||||
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
|
||||
|
||||
```yaml
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
|
@@ -547,12 +547,14 @@ The `managedSecretReference` field is deprecated and will be removed in a future
|
||||
Replace it with `managedKubeSecretReferences`, which now accepts an array of references to support multiple managed secrets in a single InfisicalSecret CRD.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
managedKubeSecretReferences:
|
||||
- secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan"
|
||||
managedKubeSecretReferences:
|
||||
- secretName: managed-secret
|
||||
secretNamespace: default
|
||||
creationPolicy: "Orphan"
|
||||
```
|
||||
|
||||
</Note>
|
||||
|
||||
<Accordion title="managedKubeSecretReferences">
|
||||
@@ -666,13 +668,13 @@ The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a bas
|
||||
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
|
||||
|
||||
```yaml
|
||||
managedKubeSecretReferences:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
managedKubeSecretReferences:
|
||||
secretName: managed-secret
|
||||
secretNamespace: default
|
||||
template:
|
||||
includeAllSecrets: true
|
||||
data:
|
||||
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
@@ -866,7 +868,7 @@ To address this, we added functionality to automatically redeploy your deploymen
|
||||
|
||||
#### Enabling auto redeploy
|
||||
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
|
||||
To enable auto redeployment you simply have to add the following annotation to the deployment, statefulset, or daemonset that consumes a managed secret.
|
||||
|
||||
```yaml
|
||||
secrets.infisical.com/auto-reload: "true"
|
||||
@@ -948,4 +950,4 @@ metadata:
|
||||
type: Opaque
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</Accordion>
|
||||
|
@@ -201,6 +201,10 @@
|
||||
"documentation/platform/dynamic-secrets/totp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Gateway",
|
||||
"pages": ["documentation/platform/gateways/overview"]
|
||||
},
|
||||
"documentation/platform/project-templates",
|
||||
{
|
||||
"group": "Workflow Integrations",
|
||||
|
1271
frontend/package-lock.json
generated
@@ -58,6 +58,7 @@
|
||||
"classnames": "^2.5.1",
|
||||
"cva": "npm:class-variance-authority@^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dompurify": "^3.2.4",
|
||||
"file-saver": "^2.0.5",
|
||||
"framer-motion": "^11.14.1",
|
||||
"i18next": "^24.1.0",
|
||||
@@ -79,9 +80,11 @@
|
||||
"react-hook-form": "^7.54.0",
|
||||
"react-i18next": "^15.2.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^10.0.1",
|
||||
"react-select": "^5.9.0",
|
||||
"react-toastify": "^10.0.6",
|
||||
"redaxios": "^0.5.1",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1",
|
||||
|
1
frontend/public/lotties/gateway.json
Normal file
20
frontend/src/components/page-frames/Banner.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import DOMPurify from "dompurify";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
|
||||
import { useServerConfig } from "@app/context";
|
||||
|
||||
export const Banner = () => {
|
||||
const { config } = useServerConfig();
|
||||
|
||||
// eslint-disable-next-line react/no-danger-with-children
|
||||
return config.pageFrameContent ? (
|
||||
<div className="h-[3vh] w-full text-center">
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
|
||||
{DOMPurify.sanitize(config.pageFrameContent)}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
};
|
@@ -45,7 +45,11 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||
{title && (
|
||||
<DialogPrimitive.Title>
|
||||
<CardTitle subTitle={subTitle}>{title}</CardTitle>
|
||||
</DialogPrimitive.Title>
|
||||
)}
|
||||
<CardBody
|
||||
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
|
@@ -59,7 +59,8 @@ export const leaveConfirmDefaultMessage =
|
||||
|
||||
export enum SessionStorageKeys {
|
||||
CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN",
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL"
|
||||
ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL",
|
||||
AUTH_CONSENT = "AUTH_CONSENT"
|
||||
}
|
||||
|
||||
export const secretTagsColors = [
|
||||
|
@@ -7,6 +7,14 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgGatewayPermissionActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
ListGateways = "list-gateways",
|
||||
EditGateways = "edit-gateways",
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@@ -25,7 +33,8 @@ export enum OrgPermissionSubjects {
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip"
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@@ -68,7 +77,8 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway];
|
||||
// TODO(scott): add back once org UI refactored
|
||||
// | [
|
||||
// OrgPermissionAppConnectionActions,
|
||||
|
@@ -22,6 +22,8 @@ export type TServerConfig = {
|
||||
defaultAuthOrgAuthMethod?: string | null;
|
||||
defaultAuthOrgAuthEnforced?: boolean | null;
|
||||
enabledLoginMethods: LoginMethod[];
|
||||
authConsentContent?: string;
|
||||
pageFrameContent?: string;
|
||||
};
|
||||
|
||||
export type TCreateAdminUserDTO = {
|
||||
|
@@ -54,6 +54,7 @@ export type TDynamicSecretProvider =
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
gatewayId?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
2
frontend/src/hooks/api/gateways/index.tsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { useDeleteGatewayById, useUpdateGatewayById } from "./mutation";
|
||||
export { gatewaysQueryKeys } from "./queries";
|
30
frontend/src/hooks/api/gateways/mutation.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { gatewaysQueryKeys } from "./queries";
|
||||
import { TUpdateGatewayDTO } from "./types";
|
||||
|
||||
export const useDeleteGatewayById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
return apiRequest.delete(`/api/v1/gateways/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(gatewaysQueryKeys.list());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateGatewayById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => {
|
||||
return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(gatewaysQueryKeys.list());
|
||||
}
|
||||
});
|
||||
};
|
33
frontend/src/hooks/api/gateways/queries.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types";
|
||||
|
||||
export const gatewaysQueryKeys = {
|
||||
allKey: () => ["gateways"],
|
||||
listKey: () => [...gatewaysQueryKeys.allKey(), "list"],
|
||||
list: () =>
|
||||
queryOptions({
|
||||
queryKey: gatewaysQueryKeys.listKey(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
|
||||
return data.gateways;
|
||||
}
|
||||
}),
|
||||
listProjectGatewayKey: ({ projectId }: TListProjectGatewayDTO) => [
|
||||
...gatewaysQueryKeys.allKey(),
|
||||
"list",
|
||||
{ projectId }
|
||||
],
|
||||
listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
|
||||
queryOptions({
|
||||
queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
|
||||
`/api/v1/gateways/projects/${projectId}`
|
||||
);
|
||||
return data.gateways;
|
||||
}
|
||||
})
|
||||
};
|
49
frontend/src/hooks/api/gateways/types.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export type TGateway = {
|
||||
id: string;
|
||||
identityId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
issuedAt: string;
|
||||
serialNumber: string;
|
||||
heartbeat: string;
|
||||
identity: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TProjectGateway = {
|
||||
id: string;
|
||||
identityId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
issuedAt: string;
|
||||
serialNumber: string;
|
||||
heartbeat: string;
|
||||
projectGatewayId: string;
|
||||
identity: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateGatewayDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
projectIds?: string[];
|
||||
};
|
||||
|
||||
export type TDeleteGatewayDTO = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TListProjectGatewayDTO = {
|
||||
projectId: string;
|
||||
};
|
@@ -10,6 +10,7 @@ export * from "./certificates";
|
||||
export * from "./certificateTemplates";
|
||||
export * from "./dynamicSecret";
|
||||
export * from "./dynamicSecretLease";
|
||||
export * from "./gateways";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identityProjectAdditionalPrivilege";
|
||||
|
@@ -42,6 +42,7 @@ export type SubscriptionPlan = {
|
||||
has_used_trial: boolean;
|
||||
caCrl: boolean;
|
||||
instanceUserManagement: boolean;
|
||||
gateway: boolean;
|
||||
externalKms: boolean;
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
|
@@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
import { WishForm } from "@app/components/features/WishForm";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/v2";
|
||||
import { envConfig } from "@app/config/env";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
|
||||
@@ -18,10 +20,14 @@ import { INFISICAL_SUPPORT_OPTIONS } from "../OrganizationLayout/components/Mini
|
||||
|
||||
export const AdminLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { config } = useServerConfig();
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
|
||||
<Banner />
|
||||
<div className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden md:flex`}>
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<aside className="dark w-full border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
|
||||
@@ -69,7 +75,6 @@ export const AdminLayout = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
</nav>
|
||||
</aside>
|
||||
<main className="flex-1 overflow-y-auto overflow-x-hidden bg-bunker-800 dark:[color-scheme:dark]">
|
||||
@@ -83,6 +88,7 @@ export const AdminLayout = () => {
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -6,6 +6,7 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
|
||||
import { Banner } from "@app/components/page-frames/Banner";
|
||||
import {
|
||||
BreadcrumbContainer,
|
||||
Menu,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
MenuItem,
|
||||
TBreadcrumbFormat
|
||||
} from "@app/components/v2";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
|
||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||
@@ -22,6 +24,7 @@ import { SidebarHeader } from "./components/SidebarHeader";
|
||||
export const OrganizationLayout = () => {
|
||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||
const location = useLocation();
|
||||
const { config } = useServerConfig();
|
||||
const isOrganizationSpecificPage = location.pathname.startsWith("/organization");
|
||||
const breadcrumbs =
|
||||
isOrganizationSpecificPage && matches && "breadcrumbs" in matches
|
||||
@@ -45,9 +48,14 @@ export const OrganizationLayout = () => {
|
||||
] as string[]
|
||||
).includes(location.pathname);
|
||||
|
||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex">
|
||||
<Banner />
|
||||
<div
|
||||
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
|
||||
>
|
||||
{!window.isSecureContext && <InsecureConnectionBanner />}
|
||||
<div className="flex flex-grow flex-col overflow-y-hidden md:flex-row">
|
||||
<MinimizedOrgSidebar />
|
||||
@@ -101,6 +109,13 @@ export const OrganizationLayout = () => {
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
|
||||
Gateways
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
@@ -137,6 +152,7 @@ export const OrganizationLayout = () => {
|
||||
{` ${t("common.no-mobile")} `}
|
||||
</p>
|
||||
</div>
|
||||
<Banner />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
faBook,
|
||||
faCheck,
|
||||
faCog,
|
||||
faDoorClosed,
|
||||
faEnvelope,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
@@ -352,6 +353,13 @@ export const MinimizedOrgSidebar = () => {
|
||||
App Connections
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon className="w-3" icon={faDoorClosed} />}
|
||||
>
|
||||
Gateways
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||
|
@@ -19,7 +19,12 @@ import "./index.css";
|
||||
import "./translation";
|
||||
// don't want to use this?
|
||||
// have a look at the Quick start guide
|
||||
// for passing in lng and translations on init
|
||||
// for passing in lng and translations on init/
|
||||
|
||||
// https://vite.dev/guide/build#load-error-handling
|
||||
window.addEventListener("vite:preloadError", () => {
|
||||
window.location.reload(); // for example, refresh the page
|
||||
});
|
||||
|
||||
// Create a new router instance
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
@@ -19,7 +19,8 @@ import {
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs
|
||||
Tabs,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useServerConfig, useUser } from "@app/context";
|
||||
import {
|
||||
@@ -55,7 +56,9 @@ const formSchema = z.object({
|
||||
trustSamlEmails: z.boolean(),
|
||||
trustLdapEmails: z.boolean(),
|
||||
trustOidcEmails: z.boolean(),
|
||||
defaultAuthOrgId: z.string()
|
||||
defaultAuthOrgId: z.string(),
|
||||
authConsentContent: z.string().optional(),
|
||||
pageFrameContent: z.string().optional()
|
||||
});
|
||||
|
||||
type TDashboardForm = z.infer<typeof formSchema>;
|
||||
@@ -80,7 +83,9 @@ export const OverviewPage = () => {
|
||||
trustSamlEmails: config.trustSamlEmails,
|
||||
trustLdapEmails: config.trustLdapEmails,
|
||||
trustOidcEmails: config.trustOidcEmails,
|
||||
defaultAuthOrgId: config.defaultAuthOrgId ?? ""
|
||||
defaultAuthOrgId: config.defaultAuthOrgId ?? "",
|
||||
authConsentContent: config.authConsentContent,
|
||||
pageFrameContent: config.pageFrameContent
|
||||
}
|
||||
});
|
||||
|
||||
@@ -95,7 +100,14 @@ export const OverviewPage = () => {
|
||||
const isNotAllowed = !user?.superAdmin;
|
||||
const onFormSubmit = async (formData: TDashboardForm) => {
|
||||
try {
|
||||
const { allowedSignUpDomain, trustSamlEmails, trustLdapEmails, trustOidcEmails } = formData;
|
||||
const {
|
||||
allowedSignUpDomain,
|
||||
trustSamlEmails,
|
||||
trustLdapEmails,
|
||||
trustOidcEmails,
|
||||
authConsentContent,
|
||||
pageFrameContent
|
||||
} = formData;
|
||||
|
||||
await updateServerConfig({
|
||||
defaultAuthOrgId: defaultAuthOrgId || null,
|
||||
@@ -103,7 +115,9 @@ export const OverviewPage = () => {
|
||||
allowedSignUpDomain: signUpMode === SignUpModes.Anyone ? allowedSignUpDomain : null,
|
||||
trustSamlEmails,
|
||||
trustLdapEmails,
|
||||
trustOidcEmails
|
||||
trustOidcEmails,
|
||||
authConsentContent,
|
||||
pageFrameContent
|
||||
});
|
||||
createNotification({
|
||||
text: "Successfully changed sign up setting.",
|
||||
@@ -324,6 +338,51 @@ export const OverviewPage = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-start">
|
||||
<div className="mb-2 text-xl font-semibold text-mineshaft-100">Notices</div>
|
||||
<div className="mb-4 max-w-lg text-sm text-mineshaft-400">
|
||||
Configure system-wide notification banners and security messages. These
|
||||
settings control the text displayed to users during authentication and
|
||||
throughout their session
|
||||
</div>
|
||||
<Controller
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Auth Consent Content"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Auth Consent Message"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="authConsentContent"
|
||||
/>
|
||||
<Controller
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Page Frame Content"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Page Frame Content"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar h-48 max-w-lg !resize-none bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="pageFrameContent"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
|
@@ -1,7 +1,15 @@
|
||||
import { createFileRoute, redirect, stripSearchParams } from "@tanstack/react-router";
|
||||
import { useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { createFileRoute, Outlet, redirect, stripSearchParams } from "@tanstack/react-router";
|
||||
import { zodValidator } from "@tanstack/zod-adapter";
|
||||
import { addSeconds, formatISO } from "date-fns";
|
||||
import DOMPurify from "dompurify";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button } from "@app/components/v2";
|
||||
import { SessionStorageKeys } from "@app/const";
|
||||
import { useServerConfig } from "@app/context";
|
||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||
import { setAuthToken } from "@app/hooks/api/reactQuery";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -10,6 +18,56 @@ const QueryParamsSchema = z.object({
|
||||
callback_port: z.coerce.number().optional().catch(undefined)
|
||||
});
|
||||
|
||||
export const AuthConsentWrapper = () => {
|
||||
const { config } = useServerConfig();
|
||||
const [hasConsented, setHasConsented] = useState(() => {
|
||||
const consentInfo = sessionStorage.getItem(SessionStorageKeys.AUTH_CONSENT);
|
||||
if (!consentInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { expiry, data } = JSON.parse(consentInfo);
|
||||
if (new Date() > new Date(expiry)) {
|
||||
sessionStorage.removeItem(SessionStorageKeys.AUTH_CONSENT);
|
||||
return false;
|
||||
}
|
||||
|
||||
return data === "true";
|
||||
});
|
||||
|
||||
const handleConsent = () => {
|
||||
sessionStorage.setItem(
|
||||
SessionStorageKeys.AUTH_CONSENT,
|
||||
JSON.stringify({
|
||||
expiry: formatISO(addSeconds(new Date(), 60)),
|
||||
data: "true"
|
||||
})
|
||||
);
|
||||
|
||||
setHasConsented(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{config.authConsentContent && !hasConsented && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-mineshaft-700/80 bg-opacity-90">
|
||||
<div className="max-h-[80vh] w-4/12 overflow-y-auto rounded-lg bg-bunker-800 p-6 text-white">
|
||||
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
|
||||
{DOMPurify.sanitize(config.authConsentContent)}
|
||||
</ReactMarkdown>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<Button onClick={handleConsent} colorSchema="secondary">
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_restrict-login-signup")({
|
||||
validateSearch: zodValidator(QueryParamsSchema),
|
||||
search: {
|
||||
@@ -45,5 +103,6 @@ export const Route = createFileRoute("/_restrict-login-signup")({
|
||||
throw redirect({
|
||||
to: `/organization/${ProjectType.SecretManager}/overview` as const
|
||||
});
|
||||
}
|
||||
},
|
||||
component: AuthConsentWrapper
|
||||
});
|
||||
|
@@ -0,0 +1,260 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faPlug,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format, formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
PageHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
|
||||
|
||||
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
||||
|
||||
export const GatewayListPage = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, {
|
||||
onSuccess: () => {
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully delete gateway"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGateway = gateways?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>Infisical | Gateways</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
className="w-full"
|
||||
title={
|
||||
<div className="flex w-full items-center">
|
||||
<span>Gateways</span>
|
||||
<a
|
||||
className="-mt-1.5"
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
description="Create and configure gateway to access private network resources from Infisical"
|
||||
/>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search gateway..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Cert Issued At</Th>
|
||||
<Th>Projects</Th>
|
||||
<Th>Identity</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
<Tooltip
|
||||
asChild={false}
|
||||
className="normal-case"
|
||||
content="The last known healthcheck. Triggers every 1 hour."
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewayLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>{el.name}</Td>
|
||||
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
|
||||
<Td>
|
||||
{el.projects.map((projectDetails) => (
|
||||
<Tag key={projectDetails.id} size="xs">
|
||||
{projectDetails.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
<Td>{el.identity.name}</Td>
|
||||
<Td>
|
||||
{el.heartbeat
|
||||
? formatRelative(new Date(el.heartbeat), new Date())
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAppConnectionActions.Delete}
|
||||
a={OrgPermissionSubjects.AppConnections}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteGateway", el)}
|
||||
>
|
||||
Delete Gateway
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={popUp.editDetails.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||
>
|
||||
<ModalContent title="Edit Gateway">
|
||||
<EditGatewayDetailsModal
|
||||
gatewayDetails={popUp.editDetails.data}
|
||||
onClose={() => handlePopUpToggle("editDetails")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewayLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
? "No Gateways match search..."
|
||||
: "No Gateways have been configured"
|
||||
}
|
||||
icon={gateways?.length ? faSearch : faPlug}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGateway.isOpen}
|
||||
title={`Are you sure want to delete gateway ${
|
||||
(popUp?.deleteGateway?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionAppConnectionActions.Read,
|
||||
subject: OrgPermissionSubjects.AppConnections
|
||||
}
|
||||
);
|
@@ -0,0 +1,113 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
|
||||
import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api";
|
||||
import { TGateway } from "@app/hooks/api/gateways/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type Props = {
|
||||
gatewayDetails: TGateway;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
projects: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: gatewayDetails?.name
|
||||
}
|
||||
});
|
||||
|
||||
const updateGatewayById = useUpdateGatewayById();
|
||||
// when gateway goes to other products switch to all
|
||||
const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces(
|
||||
{
|
||||
type: ProjectType.SecretManager
|
||||
}
|
||||
);
|
||||
|
||||
const onFormSubmit = ({ name, projects }: FormData) => {
|
||||
if (isSubmitting) return;
|
||||
updateGatewayById.mutate(
|
||||
{
|
||||
id: gatewayDetails.id,
|
||||
name,
|
||||
projectIds: projects.map((el) => el.id)
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated gateway"
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message} isRequired>
|
||||
<Input {...field} placeholder="db-subnet-1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="projects"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="Projects"
|
||||
tooltipText="Select the project(s) that you'd like to add this gateway to"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={secretManagerWorkspaces}
|
||||
placeholder="Select projects..."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti
|
||||
isLoading={isSecretManagerLoading}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
|
||||
Update
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@@ -0,0 +1,23 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { GatewayListPage } from "./GatewayListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
)({
|
||||
component: GatewayListPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Home",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||
link: linkOptions({ to: "/organization/secret-manager/overview" })
|
||||
},
|
||||
{
|
||||
label: "Gateways"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
|
||||
import { OrgPermissionSubjects } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionKmipActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
@@ -34,6 +35,15 @@ const kmipPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const orgGatewayPermissionSchema = z
|
||||
.object({
|
||||
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
|
||||
[OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(),
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(),
|
||||
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const adminConsolePermissionSchmea = z
|
||||
.object({
|
||||
"access-all-projects": z.boolean().optional()
|
||||
@@ -72,7 +82,8 @@ export const formSchema = z.object({
|
||||
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
|
||||
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
|
||||
"app-connections": appConnectionsPermissionSchema,
|
||||
kmip: kmipPermissionSchema
|
||||
kmip: kmipPermissionSchema,
|
||||
gateway: orgGatewayPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
@@ -0,0 +1,182 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" },
|
||||
{ action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" },
|
||||
{ action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" },
|
||||
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" }
|
||||
] as const;
|
||||
|
||||
export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.gateway"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSION_ACTIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.[OrgGatewayPermissionActions.ListGateways]) return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.gateway",
|
||||
{
|
||||
[OrgGatewayPermissionActions.ListGateways]: true,
|
||||
[OrgGatewayPermissionActions.EditGateways]: true,
|
||||
[OrgGatewayPermissionActions.CreateGateways]: true,
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
"permissions.gateway",
|
||||
{
|
||||
[OrgGatewayPermissionActions.ListGateways]: true,
|
||||
[OrgGatewayPermissionActions.EditGateways]: false,
|
||||
[OrgGatewayPermissionActions.CreateGateways]: false,
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case Permission.NoAccess:
|
||||
default:
|
||||
setValue(
|
||||
"permissions.gateway",
|
||||
{
|
||||
[OrgGatewayPermissionActions.ListGateways]: false,
|
||||
[OrgGatewayPermissionActions.EditGateways]: false,
|
||||
[OrgGatewayPermissionActions.CreateGateways]: false,
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td>
|
||||
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td>Gateways</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td
|
||||
colSpan={3}
|
||||
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.gateway.${action}`}
|
||||
key={`permissions.gateway.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.gateways.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@@ -74,7 +74,7 @@ type Props = {
|
||||
title: string;
|
||||
formName: keyof Omit<
|
||||
Exclude<TFormSchema["permissions"], undefined>,
|
||||
"workspace" | "organization-admin-console" | "kmip"
|
||||
"workspace" | "organization-admin-console" | "kmip" | "gateway"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
|
@@ -14,6 +14,7 @@ import {
|
||||
TFormSchema
|
||||
} from "../OrgRoleModifySection.utils";
|
||||
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
||||
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
@@ -171,6 +172,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgGatewayPermissionRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgRoleWorkspaceRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -18,7 +19,8 @@ import {
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -32,7 +34,8 @@ const formSchema = z.object({
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
@@ -124,6 +127,8 @@ export const SqlDatabaseInputForm = ({
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
@@ -137,6 +142,9 @@ export const SqlDatabaseInputForm = ({
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||
);
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
@@ -222,6 +230,42 @@ export const SqlDatabaseInputForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.projectGatewayId"
|
||||
defaultValue=""
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
label="Gateway"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isProjectGatewaysLoading}
|
||||
placeholder="Internet gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value={null as unknown as string}
|
||||
onClick={() => onChange(undefined)}
|
||||
>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{projectGateways?.map((el) => (
|
||||
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -18,7 +19,8 @@ import {
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -33,7 +35,8 @@ const formSchema = z.object({
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional()
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().optional()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
@@ -81,6 +84,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
@@ -94,8 +98,15 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||
);
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
const selectedProjectGatewayId = watch("inputs.projectGatewayId");
|
||||
const isGatewayInActive =
|
||||
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
@@ -109,7 +120,10 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs,
|
||||
inputs: {
|
||||
...inputs,
|
||||
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
|
||||
},
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
@@ -176,6 +190,44 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.projectGatewayId"
|
||||
defaultValue=""
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message) || isGatewayInActive}
|
||||
errorText={
|
||||
isGatewayInActive && selectedProjectGatewayId
|
||||
? `Project Gateway ${selectedProjectGatewayId} is removed`
|
||||
: error?.message
|
||||
}
|
||||
label="Gateway"
|
||||
helperText=""
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isProjectGatewaysLoading}
|
||||
placeholder="Internet Gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={null as unknown as string} onClick={() => onChange(undefined)}>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{projectGateways?.map((el) => (
|
||||
<SelectItem value={el.projectGatewayId} key={el.id}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
||||
<div className="flex flex-col">
|
||||
|
@@ -60,6 +60,7 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz
|
||||
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
|
||||
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
|
||||
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
|
||||
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
|
||||
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
|
||||
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
|
||||
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
|
||||
@@ -209,6 +210,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
|
||||
@@ -454,6 +459,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
|
||||
getParentRoute: () => organizationLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport.update({
|
||||
id: '/gateways',
|
||||
path: '/gateways',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
|
||||
id: '/app-connections',
|
||||
@@ -625,6 +638,14 @@ const organizationCertManagerOverviewPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const organizationGatewaysGatewayListPageRouteRoute =
|
||||
organizationGatewaysGatewayListPageRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAppConnectionsAppConnectionsPageRouteRoute =
|
||||
organizationAppConnectionsAppConnectionsPageRouteImport.update({
|
||||
id: '/',
|
||||
@@ -1886,6 +1907,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
|
||||
path: '/gateways'
|
||||
fullPath: '/organization/gateways'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
|
||||
path: '/secret-manager/$projectId'
|
||||
@@ -1907,6 +1935,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
|
||||
path: '/'
|
||||
fullPath: '/organization/gateways/'
|
||||
preLoaderRoute: typeof organizationGatewaysGatewayListPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
|
||||
path: '/cert-manager/overview'
|
||||
@@ -2900,6 +2935,21 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren {
|
||||
organizationGatewaysGatewayListPageRouteRoute: typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren =
|
||||
{
|
||||
organizationGatewaysGatewayListPageRouteRoute:
|
||||
organizationGatewaysGatewayListPageRouteRoute,
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
|
||||
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
|
||||
@@ -2909,6 +2959,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
|
||||
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
|
||||
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@@ -2933,6 +2984,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
|
||||
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
|
||||
organizationCertManagerOverviewPageRouteRoute:
|
||||
organizationCertManagerOverviewPageRouteRoute,
|
||||
organizationGroupDetailsByIDPageRouteRoute:
|
||||
@@ -3608,9 +3661,11 @@ export interface FileRoutesByFullPath {
|
||||
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
|
||||
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
|
||||
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
|
||||
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
|
||||
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
|
||||
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@@ -3780,6 +3835,7 @@ export interface FileRoutesByTo {
|
||||
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
|
||||
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
|
||||
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
|
||||
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@@ -3956,9 +4012,11 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@@ -4136,9 +4194,11 @@ export interface FileRouteTypes {
|
||||
| '/cert-manager/$projectId'
|
||||
| '/kms/$projectId'
|
||||
| '/organization/app-connections'
|
||||
| '/organization/gateways'
|
||||
| '/secret-manager/$projectId'
|
||||
| '/ssh/$projectId'
|
||||
| '/organization/app-connections/'
|
||||
| '/organization/gateways/'
|
||||
| '/organization/cert-manager/overview'
|
||||
| '/organization/groups/$groupId'
|
||||
| '/organization/identities/$identityId'
|
||||
@@ -4307,6 +4367,7 @@ export interface FileRouteTypes {
|
||||
| '/secret-manager/$projectId'
|
||||
| '/ssh/$projectId'
|
||||
| '/organization/app-connections'
|
||||
| '/organization/gateways'
|
||||
| '/organization/cert-manager/overview'
|
||||
| '/organization/groups/$groupId'
|
||||
| '/organization/identities/$identityId'
|
||||
@@ -4481,9 +4542,11 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
|
||||
@@ -4848,6 +4911,7 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
|
||||
@@ -4919,6 +4983,13 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout",
|
||||
@@ -4937,6 +5008,10 @@ export const routeTree = rootRoute
|
||||
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/": {
|
||||
"filePath": "organization/Gateways/GatewayListPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/gateways"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
|
||||
"filePath": "organization/CertManagerOverviewPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
|
@@ -29,7 +29,8 @@ const organizationRoutes = route("/organization", [
|
||||
"/$appConnection/oauth/callback",
|
||||
"organization/AppConnections/OauthCallbackPage/route.tsx"
|
||||
)
|
||||
])
|
||||
]),
|
||||
route("/gateways", [index("organization/Gateways/GatewayListPage/route.tsx")])
|
||||
]);
|
||||
|
||||
const secretManagerRoutes = route("/secret-manager/$projectId", [
|
||||
|
@@ -13,9 +13,9 @@ type: application
|
||||
# This is the chart version. This version number should be incremented each time you make changes
|
||||
# to the chart and its templates, including the app version.
|
||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||
version: v0.8.11
|
||||
version: v0.8.12
|
||||
# This is the version number of the application being deployed. This version number should be
|
||||
# incremented each time you make changes to the application. Versions are not expected to
|
||||
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||
# It is recommended to use it with quotes.
|
||||
appVersion: "v0.8.11"
|
||||
appVersion: "v0.8.12"
|
||||
|