mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-29 22:02:57 +00:00
Compare commits
1 Commits
bitbucket-
...
snyk-fix-7
Author | SHA1 | Date | |
---|---|---|---|
d37522d633 |
backend/src
@types
db
migrations
schemas
ee
routes/v1
services
keystore
lib
server/routes
cli
docker-compose.prod.ymldocs
api-reference/overview
documentation/platform/gateways
images/platform/gateways
integrations/frameworks
mint.jsonfrontend
package-lock.jsonpackage.jsonrouteTree.gen.tsroutes.ts
public/lotties
src
components/v2
context/OrgPermissionContext
hooks/api
layouts/OrganizationLayout
OrganizationLayout.tsx
main.tsxcomponents
pages
organization
Gateways/GatewayListPage
RoleByIDPage/components
public/ShareSecretPage/components
secret-manager
SecretDashboardPage/components
ActionBar/CreateDynamicSecretForm
DynamicSecretListView/EditDynamicSecretForm
PitDrawer
SecretListView
integrations/BitbucketConfigurePage
sink
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -13,7 +13,6 @@ import { TCertificateEstServiceFactory } from "@app/ee/services/certificate-est/
|
||||
import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
|
||||
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
|
||||
@ -229,7 +228,6 @@ declare module "fastify" {
|
||||
secretSync: TSecretSyncServiceFactory;
|
||||
kmip: TKmipServiceFactory;
|
||||
kmipOperation: TKmipOperationServiceFactory;
|
||||
gateway: TGatewayServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
20
backend/src/@types/knex.d.ts
vendored
20
backend/src/@types/knex.d.ts
vendored
@ -68,9 +68,6 @@ import {
|
||||
TExternalKms,
|
||||
TExternalKmsInsert,
|
||||
TExternalKmsUpdate,
|
||||
TGateways,
|
||||
TGatewaysInsert,
|
||||
TGatewaysUpdate,
|
||||
TGitAppInstallSessions,
|
||||
TGitAppInstallSessionsInsert,
|
||||
TGitAppInstallSessionsUpdate,
|
||||
@ -182,9 +179,6 @@ import {
|
||||
TOrgBots,
|
||||
TOrgBotsInsert,
|
||||
TOrgBotsUpdate,
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate,
|
||||
TOrgMemberships,
|
||||
TOrgMembershipsInsert,
|
||||
TOrgMembershipsUpdate,
|
||||
@ -206,9 +200,6 @@ import {
|
||||
TProjectEnvironments,
|
||||
TProjectEnvironmentsInsert,
|
||||
TProjectEnvironmentsUpdate,
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate,
|
||||
TProjectKeys,
|
||||
TProjectKeysInsert,
|
||||
TProjectKeysUpdate,
|
||||
@ -939,16 +930,5 @@ declare module "knex/types/tables" {
|
||||
TKmipClientCertificatesInsert,
|
||||
TKmipClientCertificatesUpdate
|
||||
>;
|
||||
[TableName.Gateway]: KnexOriginal.CompositeTableType<TGateways, TGatewaysInsert, TGatewaysUpdate>;
|
||||
[TableName.ProjectGateway]: KnexOriginal.CompositeTableType<
|
||||
TProjectGateways,
|
||||
TProjectGatewaysInsert,
|
||||
TProjectGatewaysUpdate
|
||||
>;
|
||||
[TableName.OrgGatewayConfig]: KnexOriginal.CompositeTableType<
|
||||
TOrgGatewayConfig,
|
||||
TOrgGatewayConfigInsert,
|
||||
TOrgGatewayConfigUpdate
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
@ -1,115 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.OrgGatewayConfig))) {
|
||||
await knex.schema.createTable(TableName.OrgGatewayConfig, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.string("rootCaKeyAlgorithm").notNullable();
|
||||
|
||||
t.datetime("rootCaIssuedAt").notNullable();
|
||||
t.datetime("rootCaExpiration").notNullable();
|
||||
t.string("rootCaSerialNumber").notNullable();
|
||||
t.binary("encryptedRootCaCertificate").notNullable();
|
||||
t.binary("encryptedRootCaPrivateKey").notNullable();
|
||||
|
||||
t.datetime("clientCaIssuedAt").notNullable();
|
||||
t.datetime("clientCaExpiration").notNullable();
|
||||
t.string("clientCaSerialNumber");
|
||||
t.binary("encryptedClientCaCertificate").notNullable();
|
||||
t.binary("encryptedClientCaPrivateKey").notNullable();
|
||||
|
||||
t.string("clientCertSerialNumber").notNullable();
|
||||
t.string("clientCertKeyAlgorithm").notNullable();
|
||||
t.datetime("clientCertIssuedAt").notNullable();
|
||||
t.datetime("clientCertExpiration").notNullable();
|
||||
t.binary("encryptedClientCertificate").notNullable();
|
||||
t.binary("encryptedClientPrivateKey").notNullable();
|
||||
|
||||
t.datetime("gatewayCaIssuedAt").notNullable();
|
||||
t.datetime("gatewayCaExpiration").notNullable();
|
||||
t.string("gatewayCaSerialNumber").notNullable();
|
||||
t.binary("encryptedGatewayCaCertificate").notNullable();
|
||||
t.binary("encryptedGatewayCaPrivateKey").notNullable();
|
||||
|
||||
t.uuid("orgId").notNullable();
|
||||
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||
t.unique("orgId");
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.Gateway))) {
|
||||
await knex.schema.createTable(TableName.Gateway, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("name").notNullable();
|
||||
t.string("serialNumber").notNullable();
|
||||
t.string("keyAlgorithm").notNullable();
|
||||
t.datetime("issuedAt").notNullable();
|
||||
t.datetime("expiration").notNullable();
|
||||
t.datetime("heartbeat");
|
||||
|
||||
t.binary("relayAddress").notNullable();
|
||||
|
||||
t.uuid("orgGatewayRootCaId").notNullable();
|
||||
t.foreign("orgGatewayRootCaId").references("id").inTable(TableName.OrgGatewayConfig).onDelete("CASCADE");
|
||||
|
||||
t.uuid("identityId").notNullable();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.Gateway);
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasTable(TableName.ProjectGateway))) {
|
||||
await knex.schema.createTable(TableName.ProjectGateway, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
|
||||
t.string("projectId").notNullable();
|
||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||
|
||||
t.uuid("gatewayId").notNullable();
|
||||
t.foreign("gatewayId").references("id").inTable(TableName.Gateway).onDelete("CASCADE");
|
||||
|
||||
t.timestamps(true, true, true);
|
||||
});
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||
}
|
||||
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "gatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
// not setting a foreign constraint so that cascade effects are not triggered
|
||||
if (!doesGatewayColExist) {
|
||||
t.uuid("projectGatewayId");
|
||||
t.foreign("projectGatewayId").references("id").inTable(TableName.ProjectGateway);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||
const doesGatewayColExist = await knex.schema.hasColumn(TableName.DynamicSecret, "projectGatewayId");
|
||||
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||
if (doesGatewayColExist) t.dropColumn("projectGatewayId");
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.ProjectGateway);
|
||||
await dropOnUpdateTrigger(knex, TableName.ProjectGateway);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.Gateway);
|
||||
await dropOnUpdateTrigger(knex, TableName.Gateway);
|
||||
|
||||
await knex.schema.dropTableIfExists(TableName.OrgGatewayConfig);
|
||||
await dropOnUpdateTrigger(knex, TableName.OrgGatewayConfig);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "@app/db/schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
for await (const tableName of [
|
||||
TableName.SecretV2,
|
||||
TableName.SecretVersionV2,
|
||||
TableName.SecretApprovalRequestSecretV2
|
||||
]) {
|
||||
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||
|
||||
if (hasReminderNoteCol) {
|
||||
await knex.schema.alterTable(tableName, (t) => {
|
||||
t.string("reminderNote", 1024).alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
for await (const tableName of [
|
||||
TableName.SecretV2,
|
||||
TableName.SecretVersionV2,
|
||||
TableName.SecretApprovalRequestSecretV2
|
||||
]) {
|
||||
const hasReminderNoteCol = await knex.schema.hasColumn(tableName, "reminderNote");
|
||||
|
||||
if (hasReminderNoteCol) {
|
||||
await knex.schema.alterTable(tableName, (t) => {
|
||||
t.string("reminderNote").alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -26,8 +26,7 @@ export const DynamicSecretsSchema = z.object({
|
||||
statusDetails: z.string().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
encryptedInput: zodBuffer,
|
||||
projectGatewayId: z.string().uuid().nullable().optional()
|
||||
encryptedInput: zodBuffer
|
||||
});
|
||||
|
||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||
|
@ -1,29 +0,0 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const GatewaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
serialNumber: z.string(),
|
||||
keyAlgorithm: z.string(),
|
||||
issuedAt: z.date(),
|
||||
expiration: z.date(),
|
||||
heartbeat: z.date().nullable().optional(),
|
||||
relayAddress: zodBuffer,
|
||||
orgGatewayRootCaId: z.string().uuid(),
|
||||
identityId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TGateways = z.infer<typeof GatewaysSchema>;
|
||||
export type TGatewaysInsert = Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>;
|
||||
export type TGatewaysUpdate = Partial<Omit<z.input<typeof GatewaysSchema>, TImmutableDBKeys>>;
|
@ -20,7 +20,6 @@ export * from "./certificates";
|
||||
export * from "./dynamic-secret-leases";
|
||||
export * from "./dynamic-secrets";
|
||||
export * from "./external-kms";
|
||||
export * from "./gateways";
|
||||
export * from "./git-app-install-sessions";
|
||||
export * from "./git-app-org";
|
||||
export * from "./group-project-membership-roles";
|
||||
@ -58,7 +57,6 @@ export * from "./ldap-group-maps";
|
||||
export * from "./models";
|
||||
export * from "./oidc-configs";
|
||||
export * from "./org-bots";
|
||||
export * from "./org-gateway-config";
|
||||
export * from "./org-memberships";
|
||||
export * from "./org-roles";
|
||||
export * from "./organizations";
|
||||
@ -67,7 +65,6 @@ export * from "./pki-collection-items";
|
||||
export * from "./pki-collections";
|
||||
export * from "./project-bots";
|
||||
export * from "./project-environments";
|
||||
export * from "./project-gateways";
|
||||
export * from "./project-keys";
|
||||
export * from "./project-memberships";
|
||||
export * from "./project-roles";
|
||||
|
@ -113,10 +113,6 @@ export enum TableName {
|
||||
SecretApprovalRequestSecretTagV2 = "secret_approval_request_secret_tags_v2",
|
||||
SnapshotSecretV2 = "secret_snapshot_secrets_v2",
|
||||
ProjectSplitBackfillIds = "project_split_backfill_ids",
|
||||
// Gateway
|
||||
OrgGatewayConfig = "org_gateway_config",
|
||||
Gateway = "gateways",
|
||||
ProjectGateway = "project_gateways",
|
||||
// junction tables with tags
|
||||
SecretV2JnTag = "secret_v2_tag_junction",
|
||||
JnSecretTag = "secret_tag_junction",
|
||||
|
@ -1,43 +0,0 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const OrgGatewayConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
rootCaKeyAlgorithm: z.string(),
|
||||
rootCaIssuedAt: z.date(),
|
||||
rootCaExpiration: z.date(),
|
||||
rootCaSerialNumber: z.string(),
|
||||
encryptedRootCaCertificate: zodBuffer,
|
||||
encryptedRootCaPrivateKey: zodBuffer,
|
||||
clientCaIssuedAt: z.date(),
|
||||
clientCaExpiration: z.date(),
|
||||
clientCaSerialNumber: z.string().nullable().optional(),
|
||||
encryptedClientCaCertificate: zodBuffer,
|
||||
encryptedClientCaPrivateKey: zodBuffer,
|
||||
clientCertSerialNumber: z.string(),
|
||||
clientCertKeyAlgorithm: z.string(),
|
||||
clientCertIssuedAt: z.date(),
|
||||
clientCertExpiration: z.date(),
|
||||
encryptedClientCertificate: zodBuffer,
|
||||
encryptedClientPrivateKey: zodBuffer,
|
||||
gatewayCaIssuedAt: z.date(),
|
||||
gatewayCaExpiration: z.date(),
|
||||
gatewayCaSerialNumber: z.string(),
|
||||
encryptedGatewayCaCertificate: zodBuffer,
|
||||
encryptedGatewayCaPrivateKey: zodBuffer,
|
||||
orgId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TOrgGatewayConfig = z.infer<typeof OrgGatewayConfigSchema>;
|
||||
export type TOrgGatewayConfigInsert = Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>;
|
||||
export type TOrgGatewayConfigUpdate = Partial<Omit<z.input<typeof OrgGatewayConfigSchema>, TImmutableDBKeys>>;
|
@ -1,20 +0,0 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const ProjectGatewaysSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
projectId: z.string(),
|
||||
gatewayId: z.string().uuid(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
||||
export type TProjectGateways = z.infer<typeof ProjectGatewaysSchema>;
|
||||
export type TProjectGatewaysInsert = Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>;
|
||||
export type TProjectGatewaysUpdate = Partial<Omit<z.input<typeof ProjectGatewaysSchema>, TImmutableDBKeys>>;
|
@ -1,265 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { GatewaysSchema } from "@app/db/schemas";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
const SanitizedGatewaySchema = GatewaysSchema.pick({
|
||||
id: true,
|
||||
identityId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
issuedAt: true,
|
||||
serialNumber: true,
|
||||
heartbeat: true
|
||||
});
|
||||
|
||||
const isValidRelayAddress = (relayAddress: string) => {
|
||||
const [ip, port] = relayAddress.split(":");
|
||||
return isValidIp(ip) && Number(port) <= 65535 && Number(port) >= 40000;
|
||||
};
|
||||
|
||||
export const registerGatewayRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/register-identity",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
turnServerUsername: z.string(),
|
||||
turnServerPassword: z.string(),
|
||||
turnServerRealm: z.string(),
|
||||
turnServerAddress: z.string(),
|
||||
infisicalStaticIp: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const relayDetails = await server.services.gateway.getGatewayRelayDetails(
|
||||
req.permission.id,
|
||||
req.permission.orgId,
|
||||
req.permission.authMethod
|
||||
);
|
||||
return relayDetails;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/exchange-cert",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
relayAddress: z.string().refine(isValidRelayAddress, { message: "Invalid relay address" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
serialNumber: z.string(),
|
||||
privateKey: z.string(),
|
||||
certificate: z.string(),
|
||||
certificateChain: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const gatewayCertificates = await server.services.gateway.exchangeAllocatedRelayAddress({
|
||||
identityOrg: req.permission.orgId,
|
||||
identityId: req.permission.id,
|
||||
relayAddress: req.body.relayAddress,
|
||||
identityOrgAuthMethod: req.permission.authMethod
|
||||
});
|
||||
return gatewayCertificates;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/heartbeat",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.gateway.heartbeat({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { message: "Successfully registered heartbeat" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateways: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projects: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.array()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gateway.listGateways({
|
||||
orgPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/projects/:projectId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
projectId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateways: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
}),
|
||||
projectGatewayId: z.string()
|
||||
}).array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateways = await server.services.gateway.getProjectGateways({
|
||||
projectId: req.params.projectId,
|
||||
projectPermission: req.permission
|
||||
});
|
||||
return { gateways };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema.extend({
|
||||
identity: z.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.getGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
name: slugSchema({ field: "name" }).optional(),
|
||||
projectIds: z.string().array().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.updateGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id,
|
||||
name: req.body.name,
|
||||
projectIds: req.body.projectIds
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
gateway: SanitizedGatewaySchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN, AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const gateway = await server.services.gateway.deleteGatewayById({
|
||||
orgPermission: req.permission,
|
||||
id: req.params.id
|
||||
});
|
||||
return { gateway };
|
||||
}
|
||||
});
|
||||
};
|
@ -7,7 +7,6 @@ import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||
import { registerGatewayRouter } from "./gateway-router";
|
||||
import { registerGroupRouter } from "./group-router";
|
||||
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
|
||||
import { registerKmipRouter } from "./kmip-router";
|
||||
@ -68,8 +67,6 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
||||
{ prefix: "/dynamic-secrets" }
|
||||
);
|
||||
|
||||
await server.register(registerGatewayRouter, { prefix: "/gateways" });
|
||||
|
||||
await server.register(
|
||||
async (pkiRouter) => {
|
||||
await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
|
||||
|
@ -1,31 +1,20 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { getDbConnectionHost } from "@app/lib/knex";
|
||||
|
||||
export const verifyHostInputValidity = (host: string, isGateway = false) => {
|
||||
export const verifyHostInputValidity = (host: string) => {
|
||||
const appCfg = getConfig();
|
||||
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
|
||||
// no need for validation when it's dev
|
||||
if (appCfg.NODE_ENV === "development") return;
|
||||
|
||||
if (host === "host.docker.internal") throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
!isGateway &&
|
||||
// localhost
|
||||
// internal ips
|
||||
(host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
(host === "host.docker.internal" || host.match(/^10\.\d+\.\d+\.\d+/) || host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
|
||||
if (
|
||||
host === "localhost" ||
|
||||
host === "127.0.0.1" ||
|
||||
(dbHost?.length === host.length && crypto.timingSafeEqual(Buffer.from(dbHost || ""), Buffer.from(host)))
|
||||
) {
|
||||
if (host === "localhost" || host === "127.0.0.1" || dbHost === host) {
|
||||
throw new BadRequestError({ message: "Invalid db host" });
|
||||
}
|
||||
};
|
||||
|
@ -16,7 +16,6 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
||||
|
||||
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
|
||||
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
|
||||
import { TProjectGatewayDALFactory } from "../gateway/project-gateway-dal";
|
||||
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
|
||||
import {
|
||||
DynamicSecretStatus,
|
||||
@ -45,7 +44,6 @@ type TDynamicSecretServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
projectGatewayDAL: Pick<TProjectGatewayDALFactory, "findOne">;
|
||||
};
|
||||
|
||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||
@ -59,8 +57,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
permissionService,
|
||||
dynamicSecretQueueService,
|
||||
projectDAL,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
kmsService
|
||||
}: TDynamicSecretServiceFactoryDep) => {
|
||||
const create = async ({
|
||||
path,
|
||||
@ -111,18 +108,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
const selectedProvider = dynamicSecretProviders[provider.type];
|
||||
const inputs = await selectedProvider.validateProviderInputs(provider.inputs);
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (inputs && typeof inputs === "object" && "projectGatewayId" in inputs && inputs.projectGatewayId) {
|
||||
const projectGatewayId = inputs.projectGatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@ -138,8 +123,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
maxTTL,
|
||||
defaultTTL,
|
||||
folderId: folder.id,
|
||||
name,
|
||||
projectGatewayId: selectedGatewayId
|
||||
name
|
||||
});
|
||||
return dynamicSecretCfg;
|
||||
};
|
||||
@ -211,23 +195,6 @@ export const dynamicSecretServiceFactory = ({
|
||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||
|
||||
let selectedGatewayId: string | null = null;
|
||||
if (
|
||||
updatedInput &&
|
||||
typeof updatedInput === "object" &&
|
||||
"projectGatewayId" in updatedInput &&
|
||||
updatedInput?.projectGatewayId
|
||||
) {
|
||||
const projectGatewayId = updatedInput.projectGatewayId as string;
|
||||
|
||||
const projectGateway = await projectGatewayDAL.findOne({ id: projectGatewayId, projectId });
|
||||
if (!projectGateway)
|
||||
throw new NotFoundError({
|
||||
message: `Project gateway with ${projectGatewayId} not found`
|
||||
});
|
||||
selectedGatewayId = projectGateway.id;
|
||||
}
|
||||
|
||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||
|
||||
@ -237,8 +204,7 @@ export const dynamicSecretServiceFactory = ({
|
||||
defaultTTL,
|
||||
name: newName ?? name,
|
||||
status: null,
|
||||
statusDetails: null,
|
||||
projectGatewayId: selectedGatewayId
|
||||
statusDetails: null
|
||||
});
|
||||
|
||||
return updatedDynamicCfg;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { SnowflakeProvider } from "@app/ee/services/dynamic-secret/providers/snowflake";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { AwsElastiCacheDatabaseProvider } from "./aws-elasticache";
|
||||
import { AwsIamProvider } from "./aws-iam";
|
||||
import { AzureEntraIDProvider } from "./azure-entra-id";
|
||||
@ -17,14 +16,8 @@ import { SapHanaProvider } from "./sap-hana";
|
||||
import { SqlDatabaseProvider } from "./sql-database";
|
||||
import { TotpProvider } from "./totp";
|
||||
|
||||
type TBuildDynamicSecretProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const buildDynamicSecretProviders = ({
|
||||
gatewayService
|
||||
}: TBuildDynamicSecretProviderDTO): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider({ gatewayService }),
|
||||
export const buildDynamicSecretProviders = (): Record<DynamicSecretProviders, TDynamicProviderFns> => ({
|
||||
[DynamicSecretProviders.SqlDatabase]: SqlDatabaseProvider(),
|
||||
[DynamicSecretProviders.Cassandra]: CassandraProvider(),
|
||||
[DynamicSecretProviders.AwsIam]: AwsIamProvider(),
|
||||
[DynamicSecretProviders.Redis]: RedisDatabaseProvider(),
|
||||
|
@ -103,8 +103,7 @@ export const DynamicSecretSqlDBSchema = z.object({
|
||||
creationStatement: z.string().trim(),
|
||||
revocationStatement: z.string().trim(),
|
||||
renewStatement: z.string().trim().optional(),
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().nullable().optional()
|
||||
ca: z.string().optional()
|
||||
});
|
||||
|
||||
export const DynamicSecretCassandraSchema = z.object({
|
||||
|
@ -3,10 +3,8 @@ import knex from "knex";
|
||||
import { customAlphabet } from "nanoid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { withGatewayProxy } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||
import { verifyHostInputValidity } from "../dynamic-secret-fns";
|
||||
import { DynamicSecretSqlDBSchema, SqlProviders, TDynamicProviderFns } from "./models";
|
||||
|
||||
@ -27,14 +25,10 @@ const generateUsername = (provider: SqlProviders) => {
|
||||
return alphaNumericNanoId(32);
|
||||
};
|
||||
|
||||
type TSqlDatabaseProviderDTO = {
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTls">;
|
||||
};
|
||||
|
||||
export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO): TDynamicProviderFns => {
|
||||
export const SqlDatabaseProvider = (): TDynamicProviderFns => {
|
||||
const validateProviderInputs = async (inputs: unknown) => {
|
||||
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
|
||||
verifyHostInputValidity(providerInputs.host, Boolean(providerInputs.projectGatewayId));
|
||||
verifyHostInputValidity(providerInputs.host);
|
||||
return providerInputs;
|
||||
};
|
||||
|
||||
@ -51,6 +45,7 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
user: providerInputs.username,
|
||||
password: providerInputs.password,
|
||||
ssl,
|
||||
pool: { min: 0, max: 1 },
|
||||
// @ts-expect-error this is because of knexjs type signature issue. This is directly passed to driver
|
||||
// https://github.com/knex/knex/blob/b6507a7129d2b9fafebf5f831494431e64c6a8a0/lib/dialects/mssql/index.js#L66
|
||||
// https://github.com/tediousjs/tedious/blob/ebb023ed90969a7ec0e4b036533ad52739d921f7/test/config.ci.ts#L19
|
||||
@ -66,112 +61,61 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
return db;
|
||||
};
|
||||
|
||||
const gatewayProxyWrapper = async (
|
||||
providerInputs: z.infer<typeof DynamicSecretSqlDBSchema>,
|
||||
gatewayCallback: (host: string, port: number) => Promise<void>
|
||||
) => {
|
||||
const relayDetails = await gatewayService.fnGetGatewayClientTls(providerInputs.projectGatewayId as string);
|
||||
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||
await withGatewayProxy(
|
||||
async (port) => {
|
||||
await gatewayCallback("localhost", port);
|
||||
},
|
||||
{
|
||||
targetHost: providerInputs.host,
|
||||
targetPort: providerInputs.port,
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
identityId: relayDetails.identityId,
|
||||
orgId: relayDetails.orgId,
|
||||
tlsOptions: {
|
||||
ca: relayDetails.certChain,
|
||||
cert: relayDetails.certificate,
|
||||
key: relayDetails.privateKey
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const validateConnection = async (inputs: unknown) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
let isConnected = false;
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
const db = await $getClient(providerInputs);
|
||||
// oracle needs from keyword
|
||||
const testStatement = providerInputs.client === SqlProviders.Oracle ? "SELECT 1 FROM DUAL" : "SELECT 1";
|
||||
|
||||
isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
const isConnected = await db.raw(testStatement).then(() => true);
|
||||
await db.destroy();
|
||||
return isConnected;
|
||||
};
|
||||
|
||||
const create = async (inputs: unknown, expireAt: number) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = generateUsername(providerInputs.client);
|
||||
const password = generatePassword(providerInputs.client);
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
const creationStatement = handlebars.compile(providerInputs.creationStatement, { noEscape: true })({
|
||||
username,
|
||||
password,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
const queries = creationStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
});
|
||||
await db.destroy();
|
||||
return { entityId: username, data: { DB_USERNAME: username, DB_PASSWORD: password } };
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const username = entityId;
|
||||
const { database } = providerInputs;
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
try {
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await db.destroy();
|
||||
|
||||
const revokeStatement = handlebars.compile(providerInputs.revocationStatement)({ username, database });
|
||||
const queries = revokeStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
}
|
||||
});
|
||||
|
||||
await db.destroy();
|
||||
return { entityId: username };
|
||||
};
|
||||
|
||||
@ -179,35 +123,28 @@ export const SqlDatabaseProvider = ({ gatewayService }: TSqlDatabaseProviderDTO)
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
if (!providerInputs.renewStatement) return { entityId };
|
||||
|
||||
const gatewayCallback = async (host = providerInputs.host, port = providerInputs.port) => {
|
||||
const db = await $getClient({ ...providerInputs, port, host });
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
const db = await $getClient(providerInputs);
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
try {
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
});
|
||||
const expiration = new Date(expireAt).toISOString();
|
||||
const { database } = providerInputs;
|
||||
|
||||
const renewStatement = handlebars.compile(providerInputs.renewStatement)({
|
||||
username: entityId,
|
||||
expiration,
|
||||
database
|
||||
});
|
||||
|
||||
if (renewStatement) {
|
||||
const queries = renewStatement.toString().split(";").filter(Boolean);
|
||||
await db.transaction(async (tx) => {
|
||||
for (const query of queries) {
|
||||
// eslint-disable-next-line
|
||||
await tx.raw(query);
|
||||
}
|
||||
} finally {
|
||||
await db.destroy();
|
||||
}
|
||||
};
|
||||
if (providerInputs.projectGatewayId) {
|
||||
await gatewayProxyWrapper(providerInputs, gatewayCallback);
|
||||
} else {
|
||||
await gatewayCallback();
|
||||
});
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
return { entityId };
|
||||
};
|
||||
|
||||
|
@ -1,86 +0,0 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { GatewaysSchema, TableName, TGateways } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import {
|
||||
buildFindFilter,
|
||||
ormify,
|
||||
selectAllTableCols,
|
||||
sqlNestRelationships,
|
||||
TFindFilter,
|
||||
TFindOpt
|
||||
} from "@app/lib/knex";
|
||||
|
||||
export type TGatewayDALFactory = ReturnType<typeof gatewayDALFactory>;
|
||||
|
||||
export const gatewayDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.Gateway);
|
||||
|
||||
const find = async (filter: TFindFilter<TGateways>, { offset, limit, sort, tx }: TFindOpt<TGateways> = {}) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Gateway)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter(filter))
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||
.leftJoin(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||
.leftJoin(TableName.Project, `${TableName.Project}.id`, `${TableName.ProjectGateway}.projectId`)
|
||||
.select(selectAllTableCols(TableName.Gateway))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug"),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
);
|
||||
if (limit) void query.limit(limit);
|
||||
if (offset) void query.offset(offset);
|
||||
if (sort) {
|
||||
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||
}
|
||||
|
||||
const docs = await query;
|
||||
return sqlNestRelationships({
|
||||
data: docs,
|
||||
key: "id",
|
||||
parentMapper: (data) => ({
|
||||
...GatewaysSchema.parse(data),
|
||||
identity: { id: data.identityId, name: data.identityName }
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "projectId",
|
||||
label: "projects" as const,
|
||||
mapper: ({ projectId, projectName, projectSlug }) => ({
|
||||
id: projectId,
|
||||
name: projectName,
|
||||
slug: projectSlug
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find` });
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (projectId: string, tx?: Knex) => {
|
||||
try {
|
||||
const query = (tx || db)(TableName.Gateway)
|
||||
.join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.Gateway}.identityId`)
|
||||
.join(TableName.ProjectGateway, `${TableName.ProjectGateway}.gatewayId`, `${TableName.Gateway}.id`)
|
||||
.select(selectAllTableCols(TableName.Gateway))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Identity).as("identityName"),
|
||||
db.ref("id").withSchema(TableName.ProjectGateway).as("projectGatewayId")
|
||||
)
|
||||
.where({ [`${TableName.ProjectGateway}.projectId` as "projectId"]: projectId });
|
||||
|
||||
const docs = await query;
|
||||
return docs.map((el) => ({ ...el, identity: { id: el.identityId, name: el.identityName } }));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: `${TableName.Gateway}: Find by project id` });
|
||||
}
|
||||
};
|
||||
|
||||
return { ...orm, find, findByProjectId };
|
||||
};
|
@ -1,652 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { KeyStorePrefixes, PgSqlLock, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { pingGatewayAndVerify } from "@app/lib/gateway";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { getTurnCredentials } from "@app/lib/turn/credentials";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
createSerialNumber,
|
||||
keyAlgorithmToAlgCfg
|
||||
} from "@app/services/certificate-authority/certificate-authority-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGatewayDALFactory } from "./gateway-dal";
|
||||
import {
|
||||
TExchangeAllocatedRelayAddressDTO,
|
||||
TGetGatewayByIdDTO,
|
||||
TGetProjectGatewayByIdDTO,
|
||||
THeartBeatDTO,
|
||||
TListGatewaysDTO,
|
||||
TUpdateGatewayByIdDTO
|
||||
} from "./gateway-types";
|
||||
import { TOrgGatewayConfigDALFactory } from "./org-gateway-config-dal";
|
||||
import { TProjectGatewayDALFactory } from "./project-gateway-dal";
|
||||
|
||||
type TGatewayServiceFactoryDep = {
|
||||
gatewayDAL: TGatewayDALFactory;
|
||||
projectGatewayDAL: TProjectGatewayDALFactory;
|
||||
orgGatewayConfigDAL: Pick<TOrgGatewayConfigDALFactory, "findOne" | "create" | "transaction" | "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures" | "getPlan">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "decryptWithRootKey">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getProjectPermission">;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
|
||||
};
|
||||
|
||||
export type TGatewayServiceFactory = ReturnType<typeof gatewayServiceFactory>;
|
||||
const TURN_SERVER_CREDENTIALS_SCHEMA = z.object({
|
||||
username: z.string(),
|
||||
password: z.string()
|
||||
});
|
||||
|
||||
export const gatewayServiceFactory = ({
|
||||
gatewayDAL,
|
||||
licenseService,
|
||||
kmsService,
|
||||
permissionService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
}: TGatewayServiceFactoryDep) => {
|
||||
const $validateOrgAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
// if (!licenseService.onPremFeatures.gateway) {
|
||||
// throw new BadRequestError({
|
||||
// message:
|
||||
// "Gateway handshake failed due to instance plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
// });
|
||||
// }
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Gateway handshake failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.CreateGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
};
|
||||
|
||||
const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => {
|
||||
const TURN_CRED_EXPIRY = 10 * 60; // 10 minutes
|
||||
|
||||
const envCfg = getConfig();
|
||||
await $validateOrgAccessToGateway(actorOrgId, actorId, actorAuthMethod);
|
||||
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
if (!envCfg.GATEWAY_RELAY_AUTH_SECRET || !envCfg.GATEWAY_RELAY_ADDRESS || !envCfg.GATEWAY_RELAY_REALM) {
|
||||
throw new BadRequestError({
|
||||
message: "Gateway handshake failed due to missing instance configuration."
|
||||
});
|
||||
}
|
||||
|
||||
let turnServerUsername = "";
|
||||
let turnServerPassword = "";
|
||||
// keep it in redis for 5mins to avoid generating so many credentials
|
||||
const previousCredential = await keyStore.getItem(KeyStorePrefixes.GatewayIdentityCredential(actorId));
|
||||
if (previousCredential) {
|
||||
const el = await TURN_SERVER_CREDENTIALS_SCHEMA.parseAsync(
|
||||
JSON.parse(decryptor({ cipherTextBlob: Buffer.from(previousCredential, "hex") }).toString())
|
||||
);
|
||||
turnServerUsername = el.username;
|
||||
turnServerPassword = el.password;
|
||||
} else {
|
||||
const el = getTurnCredentials(actorId, envCfg.GATEWAY_RELAY_AUTH_SECRET);
|
||||
await keyStore.setItemWithExpiry(
|
||||
KeyStorePrefixes.GatewayIdentityCredential(actorId),
|
||||
TURN_CRED_EXPIRY,
|
||||
encryptor({
|
||||
plainText: Buffer.from(JSON.stringify({ username: el.username, password: el.password }))
|
||||
}).cipherTextBlob.toString("hex")
|
||||
);
|
||||
turnServerUsername = el.username;
|
||||
turnServerPassword = el.password;
|
||||
}
|
||||
|
||||
return {
|
||||
turnServerUsername,
|
||||
turnServerPassword,
|
||||
turnServerRealm: envCfg.GATEWAY_RELAY_REALM,
|
||||
turnServerAddress: envCfg.GATEWAY_RELAY_ADDRESS,
|
||||
infisicalStaticIp: envCfg.GATEWAY_INFISICAL_STATIC_IP_ADDRESS
|
||||
};
|
||||
};
|
||||
|
||||
const exchangeAllocatedRelayAddress = async ({
|
||||
identityId,
|
||||
identityOrg,
|
||||
relayAddress,
|
||||
identityOrgAuthMethod
|
||||
}: TExchangeAllocatedRelayAddressDTO) => {
|
||||
await $validateOrgAccessToGateway(identityOrg, identityId, identityOrgAuthMethod);
|
||||
const { encryptor: orgKmsEncryptor, decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityOrg
|
||||
});
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayRootCaInit(identityOrg)]);
|
||||
const existingGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: identityOrg });
|
||||
if (existingGatewayConfig) return existingGatewayConfig;
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
// generate root CA
|
||||
const rootCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const rootCaSerialNumber = createSerialNumber();
|
||||
const rootCaSkObj = crypto.KeyObject.from(rootCaKeys.privateKey);
|
||||
const rootCaIssuedAt = new Date();
|
||||
const rootCaKeyAlgorithm = CertKeyAlgorithm.RSA_2048;
|
||||
const rootCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const rootCaCert = await x509.X509CertificateGenerator.createSelfSigned({
|
||||
name: `O=${identityOrg},CN=Infisical Gateway Root CA`,
|
||||
serialNumber: rootCaSerialNumber,
|
||||
notBefore: rootCaIssuedAt,
|
||||
notAfter: rootCaExpiration,
|
||||
signingAlgorithm: alg,
|
||||
keys: rootCaKeys,
|
||||
extensions: [
|
||||
// eslint-disable-next-line no-bitwise
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
|
||||
await x509.SubjectKeyIdentifierExtension.create(rootCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
// generate client ca
|
||||
const clientCaSerialNumber = createSerialNumber();
|
||||
const clientCaIssuedAt = new Date();
|
||||
const clientCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const clientCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCaSkObj = crypto.KeyObject.from(clientCaKeys.privateKey);
|
||||
|
||||
const clientCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCaSerialNumber,
|
||||
subject: `O=${identityOrg},CN=Client Intermediate CA`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: clientCaIssuedAt,
|
||||
notAfter: clientCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: clientCaKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, 0, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
const clientKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: clientCertSerialNumber,
|
||||
subject: `O=${identityOrg},OU=gateway-client,CN=cloud`,
|
||||
issuer: clientCaCert.subject,
|
||||
notAfter: clientCaExpiration,
|
||||
notBefore: clientCaIssuedAt,
|
||||
signingKey: clientCaKeys.privateKey,
|
||||
publicKey: clientKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(clientCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(clientKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT] |
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||
]
|
||||
});
|
||||
const clientSkObj = crypto.KeyObject.from(clientKeys.privateKey);
|
||||
|
||||
// generate gateway ca
|
||||
const gatewayCaSerialNumber = createSerialNumber();
|
||||
const gatewayCaIssuedAt = new Date();
|
||||
const gatewayCaExpiration = new Date(new Date().setFullYear(2045));
|
||||
const gatewayCaKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const gatewayCaSkObj = crypto.KeyObject.from(gatewayCaKeys.privateKey);
|
||||
const gatewayCaCert = await x509.X509CertificateGenerator.create({
|
||||
serialNumber: gatewayCaSerialNumber,
|
||||
subject: `O=${identityOrg},CN=Gateway CA`,
|
||||
issuer: rootCaCert.subject,
|
||||
notBefore: gatewayCaIssuedAt,
|
||||
notAfter: gatewayCaExpiration,
|
||||
signingKey: rootCaKeys.privateKey,
|
||||
publicKey: gatewayCaKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions: [
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags.keyCertSign |
|
||||
x509.KeyUsageFlags.cRLSign |
|
||||
x509.KeyUsageFlags.digitalSignature |
|
||||
x509.KeyUsageFlags.keyEncipherment,
|
||||
true
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, 0, true),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(rootCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(gatewayCaKeys.publicKey)
|
||||
]
|
||||
});
|
||||
|
||||
return orgGatewayConfigDAL.create({
|
||||
orgId: identityOrg,
|
||||
rootCaIssuedAt,
|
||||
rootCaExpiration,
|
||||
rootCaSerialNumber,
|
||||
rootCaKeyAlgorithm,
|
||||
encryptedRootCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: rootCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedRootCaCertificate: orgKmsEncryptor({ plainText: Buffer.from(rootCaCert.rawData) }).cipherTextBlob,
|
||||
|
||||
clientCaIssuedAt,
|
||||
clientCaExpiration,
|
||||
clientCaSerialNumber,
|
||||
encryptedClientCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: clientCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedClientCaCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(clientCaCert.rawData)
|
||||
}).cipherTextBlob,
|
||||
|
||||
clientCertIssuedAt: clientCaIssuedAt,
|
||||
clientCertExpiration: clientCaExpiration,
|
||||
clientCertKeyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
clientCertSerialNumber,
|
||||
encryptedClientPrivateKey: orgKmsEncryptor({
|
||||
plainText: clientSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedClientCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(clientCert.rawData)
|
||||
}).cipherTextBlob,
|
||||
|
||||
gatewayCaIssuedAt,
|
||||
gatewayCaExpiration,
|
||||
gatewayCaSerialNumber,
|
||||
encryptedGatewayCaPrivateKey: orgKmsEncryptor({
|
||||
plainText: gatewayCaSkObj.export({
|
||||
type: "pkcs8",
|
||||
format: "der"
|
||||
})
|
||||
}).cipherTextBlob,
|
||||
encryptedGatewayCaCertificate: orgKmsEncryptor({
|
||||
plainText: Buffer.from(gatewayCaCert.rawData)
|
||||
}).cipherTextBlob
|
||||
});
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayCaAlg = keyAlgorithmToAlgCfg(orgGatewayConfig.rootCaKeyAlgorithm as CertKeyAlgorithm);
|
||||
const gatewayCaSkObj = crypto.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedGatewayCaPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const gatewayCaPrivateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
gatewayCaSkObj.export({ format: "der", type: "pkcs8" }),
|
||||
gatewayCaAlg,
|
||||
true,
|
||||
["sign"]
|
||||
);
|
||||
|
||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||
const gatewayKeys = await crypto.subtle.generateKey(alg, true, ["sign", "verify"]);
|
||||
const certIssuedAt = new Date();
|
||||
// then need to periodically init
|
||||
const certExpireAt = new Date(new Date().setMonth(new Date().getMonth() + 1));
|
||||
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
await x509.AuthorityKeyIdentifierExtension.create(gatewayCaCert, false),
|
||||
await x509.SubjectKeyIdentifierExtension.create(gatewayKeys.publicKey),
|
||||
new x509.CertificatePolicyExtension(["2.5.29.32.0"]), // anyPolicy
|
||||
new x509.KeyUsagesExtension(
|
||||
// eslint-disable-next-line no-bitwise
|
||||
x509.KeyUsageFlags[CertKeyUsage.DIGITAL_SIGNATURE] | x509.KeyUsageFlags[CertKeyUsage.KEY_ENCIPHERMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||
// san
|
||||
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: relayAddress.split(":")[0] }], false)
|
||||
];
|
||||
|
||||
const serialNumber = createSerialNumber();
|
||||
const privateKey = crypto.KeyObject.from(gatewayKeys.privateKey);
|
||||
const gatewayCertificate = await x509.X509CertificateGenerator.create({
|
||||
serialNumber,
|
||||
subject: `CN=${identityId},O=${identityOrg},OU=Gateway`,
|
||||
issuer: gatewayCaCert.subject,
|
||||
notBefore: certIssuedAt,
|
||||
notAfter: certExpireAt,
|
||||
signingKey: gatewayCaPrivateKey,
|
||||
publicKey: gatewayKeys.publicKey,
|
||||
signingAlgorithm: alg,
|
||||
extensions
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
// just for local development
|
||||
const formatedRelayAddress =
|
||||
appCfg.NODE_ENV === "development" ? relayAddress.replace("127.0.0.1", "host.docker.internal") : relayAddress;
|
||||
|
||||
await gatewayDAL.transaction(async (tx) => {
|
||||
await tx.raw("SELECT pg_advisory_xact_lock(?)", [PgSqlLock.OrgGatewayCertExchange(identityOrg)]);
|
||||
const existingGateway = await gatewayDAL.findOne({ identityId, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
|
||||
if (existingGateway) {
|
||||
return gatewayDAL.updateById(existingGateway.id, {
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
issuedAt: certIssuedAt,
|
||||
expiration: certExpireAt,
|
||||
serialNumber,
|
||||
relayAddress: orgKmsEncryptor({
|
||||
plainText: Buffer.from(formatedRelayAddress)
|
||||
}).cipherTextBlob
|
||||
});
|
||||
}
|
||||
|
||||
return gatewayDAL.create({
|
||||
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
|
||||
issuedAt: certIssuedAt,
|
||||
expiration: certExpireAt,
|
||||
serialNumber,
|
||||
relayAddress: orgKmsEncryptor({
|
||||
plainText: Buffer.from(formatedRelayAddress)
|
||||
}).cipherTextBlob,
|
||||
identityId,
|
||||
orgGatewayRootCaId: orgGatewayConfig.id,
|
||||
name: `gateway-${alphaNumericNanoId(6).toLowerCase()}`
|
||||
});
|
||||
});
|
||||
|
||||
const gatewayCertificateChain = `${clientCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim();
|
||||
|
||||
return {
|
||||
serialNumber,
|
||||
privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) as string,
|
||||
certificate: gatewayCertificate.toString("pem"),
|
||||
certificateChain: gatewayCertificateChain
|
||||
};
|
||||
};
|
||||
|
||||
const heartbeat = async ({ orgPermission }: THeartBeatDTO) => {
|
||||
await $validateOrgAccessToGateway(orgPermission.orgId, orgPermission.id, orgPermission.authMethod);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Identity with ID ${orgPermission.id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ identityId: orgPermission.id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${orgPermission.id} not found.` });
|
||||
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const privateKey = crypto
|
||||
.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
})
|
||||
.export({ type: "pkcs8", format: "pem" });
|
||||
|
||||
const relayAddress = orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString();
|
||||
const [relayHost, relayPort] = relayAddress.split(":");
|
||||
|
||||
await pingGatewayAndVerify({
|
||||
relayHost,
|
||||
relayPort: Number(relayPort),
|
||||
tlsOptions: {
|
||||
key: privateKey,
|
||||
ca: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||
cert: clientCert.toString("pem")
|
||||
},
|
||||
identityId: orgPermission.id,
|
||||
orgId: orgPermission.orgId
|
||||
});
|
||||
|
||||
await gatewayDAL.updateById(gateway.id, { heartbeat: new Date() });
|
||||
};
|
||||
|
||||
const listGateways = async ({ orgPermission }: TListGatewaysDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) return [];
|
||||
|
||||
const gateways = await gatewayDAL.find({
|
||||
orgGatewayRootCaId: orgGatewayConfig.id
|
||||
});
|
||||
return gateways;
|
||||
};
|
||||
|
||||
const getGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.ListGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.find({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const updateGatewayById = async ({ orgPermission, id, name, projectIds }: TUpdateGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.EditGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.update({ id, orgGatewayRootCaId: orgGatewayConfig.id }, { name });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
if (projectIds) {
|
||||
await projectGatewayDAL.transaction(async (tx) => {
|
||||
await projectGatewayDAL.delete({ gatewayId: gateway.id }, tx);
|
||||
await projectGatewayDAL.insertMany(
|
||||
projectIds.map((el) => ({ gatewayId: gateway.id, projectId: el })),
|
||||
tx
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const deleteGatewayById = async ({ orgPermission, id }: TGetGatewayByIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
orgPermission.type,
|
||||
orgPermission.id,
|
||||
orgPermission.orgId,
|
||||
orgPermission.authMethod,
|
||||
orgPermission.orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionGatewayActions.DeleteGateways,
|
||||
OrgPermissionSubjects.Gateway
|
||||
);
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId });
|
||||
if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
|
||||
const [gateway] = await gatewayDAL.delete({ id, orgGatewayRootCaId: orgGatewayConfig.id });
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` });
|
||||
return gateway;
|
||||
};
|
||||
|
||||
const getProjectGateways = async ({ projectId, projectPermission }: TGetProjectGatewayByIdDTO) => {
|
||||
await permissionService.getProjectPermission({
|
||||
projectId,
|
||||
actor: projectPermission.type,
|
||||
actorId: projectPermission.id,
|
||||
actorOrgId: projectPermission.orgId,
|
||||
actorAuthMethod: projectPermission.authMethod,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
const gateways = await gatewayDAL.findByProjectId(projectId);
|
||||
return gateways;
|
||||
};
|
||||
|
||||
// this has no permission check and used for dynamic secrets directly
|
||||
// assumes permission check is already done
|
||||
const fnGetGatewayClientTls = async (projectGatewayId: string) => {
|
||||
const projectGateway = await projectGatewayDAL.findById(projectGatewayId);
|
||||
if (!projectGateway) throw new NotFoundError({ message: `Project gateway with ID ${projectGatewayId} not found.` });
|
||||
|
||||
const { gatewayId } = projectGateway;
|
||||
const gateway = await gatewayDAL.findById(gatewayId);
|
||||
if (!gateway) throw new NotFoundError({ message: `Gateway with ID ${gatewayId} not found.` });
|
||||
|
||||
const orgGatewayConfig = await orgGatewayConfigDAL.findById(gateway.orgGatewayRootCaId);
|
||||
const { decryptor: orgKmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
});
|
||||
|
||||
const rootCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedRootCaCertificate
|
||||
})
|
||||
);
|
||||
const gatewayCaCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedGatewayCaCertificate
|
||||
})
|
||||
);
|
||||
const clientCert = new x509.X509Certificate(
|
||||
orgKmsDecryptor({
|
||||
cipherTextBlob: orgGatewayConfig.encryptedClientCertificate
|
||||
})
|
||||
);
|
||||
|
||||
const clientSkObj = crypto.createPrivateKey({
|
||||
key: orgKmsDecryptor({ cipherTextBlob: orgGatewayConfig.encryptedClientPrivateKey }),
|
||||
format: "der",
|
||||
type: "pkcs8"
|
||||
});
|
||||
|
||||
return {
|
||||
relayAddress: orgKmsDecryptor({ cipherTextBlob: gateway.relayAddress }).toString(),
|
||||
privateKey: clientSkObj.export({ type: "pkcs8", format: "pem" }),
|
||||
certificate: clientCert.toString("pem"),
|
||||
certChain: `${gatewayCaCert.toString("pem")}\n${rootCaCert.toString("pem")}`.trim(),
|
||||
identityId: gateway.identityId,
|
||||
orgId: orgGatewayConfig.orgId
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getGatewayRelayDetails,
|
||||
exchangeAllocatedRelayAddress,
|
||||
listGateways,
|
||||
getGatewayById,
|
||||
updateGatewayById,
|
||||
deleteGatewayById,
|
||||
getProjectGateways,
|
||||
fnGetGatewayClientTls,
|
||||
heartbeat
|
||||
};
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { ActorAuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
export type TExchangeAllocatedRelayAddressDTO = {
|
||||
identityId: string;
|
||||
identityOrg: string;
|
||||
identityOrgAuthMethod: ActorAuthMethod;
|
||||
relayAddress: string;
|
||||
};
|
||||
|
||||
export type TListGatewaysDTO = {
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TGetGatewayByIdDTO = {
|
||||
id: string;
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TUpdateGatewayByIdDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
projectIds?: string[];
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TDeleteGatewayByIdDTO = {
|
||||
id: string;
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type TGetProjectGatewayByIdDTO = {
|
||||
projectId: string;
|
||||
projectPermission: OrgServiceActor;
|
||||
};
|
||||
|
||||
export type THeartBeatDTO = {
|
||||
orgPermission: OrgServiceActor;
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TOrgGatewayConfigDALFactory = ReturnType<typeof orgGatewayConfigDALFactory>;
|
||||
|
||||
export const orgGatewayConfigDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.OrgGatewayConfig);
|
||||
return orm;
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TProjectGatewayDALFactory = ReturnType<typeof projectGatewayDALFactory>;
|
||||
|
||||
export const projectGatewayDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.ProjectGateway);
|
||||
return orm;
|
||||
};
|
@ -51,8 +51,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
pkiEst: false,
|
||||
enforceMfa: false,
|
||||
projectTemplates: false,
|
||||
kmip: false,
|
||||
gateway: false
|
||||
kmip: false
|
||||
});
|
||||
|
||||
export const setupLicenseRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||
|
@ -69,7 +69,6 @@ export type TFeatureSet = {
|
||||
enforceMfa: boolean;
|
||||
projectTemplates: false;
|
||||
kmip: false;
|
||||
gateway: false;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
|
@ -32,14 +32,6 @@ export enum OrgPermissionAdminConsoleAction {
|
||||
AccessAllProjects = "access-all-projects"
|
||||
}
|
||||
|
||||
export enum OrgPermissionGatewayActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
ListGateways = "list-gateways",
|
||||
EditGateways = "edit-gateways",
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -58,8 +50,7 @@ export enum OrgPermissionSubjects {
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
Kmip = "kmip"
|
||||
}
|
||||
|
||||
export type AppConnectionSubjectFields = {
|
||||
@ -82,7 +73,6 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
(
|
||||
@ -190,12 +180,6 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@ -280,11 +264,6 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.EditGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
@ -321,8 +300,6 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { Redis } from "ioredis";
|
||||
|
||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
||||
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||
|
||||
export const PgSqlLock = {
|
||||
BootUpMigration: 2023,
|
||||
SuperAdminInit: 2024,
|
||||
KmsRootKeyInit: 2025,
|
||||
OrgGatewayRootCaInit: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-root-ca:${orgId}`),
|
||||
OrgGatewayCertExchange: (orgId: string) => pgAdvisoryLockHashText(`org-gateway-cert-exchange:${orgId}`)
|
||||
} as const;
|
||||
export enum PgSqlLock {
|
||||
BootUpMigration = 2023,
|
||||
SuperAdminInit = 2024,
|
||||
KmsRootKeyInit = 2025
|
||||
}
|
||||
|
||||
export type TKeyStoreFactory = ReturnType<typeof keyStoreFactory>;
|
||||
|
||||
@ -36,8 +33,7 @@ export const KeyStorePrefixes = {
|
||||
SecretSyncLastRunTimestamp: (syncId: string) => `secret-sync-last-run-${syncId}` as const,
|
||||
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
|
||||
`identity-access-token-status:${identityAccessTokenId}`,
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`,
|
||||
GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}`
|
||||
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
|
||||
};
|
||||
|
||||
export const KeyStoreTtls = {
|
||||
|
@ -184,14 +184,6 @@ const envSchema = z
|
||||
USE_PG_QUEUE: zodStrBool.default("false"),
|
||||
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false"),
|
||||
|
||||
/* Gateway----------------------------------------------------------------------------- */
|
||||
GATEWAY_INFISICAL_STATIC_IP_ADDRESS: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_ADDRESS: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_REALM: zpStr(z.string().optional()),
|
||||
GATEWAY_RELAY_AUTH_SECRET: zpStr(z.string().optional()),
|
||||
|
||||
/* ----------------------------------------------------------------------------- */
|
||||
|
||||
/* App Connections ----------------------------------------------------------------------------- */
|
||||
|
||||
// aws
|
||||
|
@ -1,29 +0,0 @@
|
||||
// used for postgres lock
|
||||
// this is something postgres does under the hood
|
||||
// convert any string to a unique number
|
||||
export const hashtext = (text: string) => {
|
||||
// Convert text to UTF8 bytes array for consistent behavior with PostgreSQL
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(text);
|
||||
|
||||
// Implementation of hash_any
|
||||
let result = 0;
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
result = ((result << 5) + result) ^ bytes[i];
|
||||
// Keep within 32-bit integer range
|
||||
// eslint-disable-next-line no-bitwise
|
||||
result >>>= 0;
|
||||
}
|
||||
|
||||
// Convert to signed 32-bit integer like PostgreSQL
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return result | 0;
|
||||
};
|
||||
|
||||
export const pgAdvisoryLockHashText = (text: string) => {
|
||||
const hash = hashtext(text);
|
||||
// Ensure positive value within PostgreSQL integer range
|
||||
return Math.abs(hash) % 2 ** 31;
|
||||
};
|
@ -1,264 +0,0 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import net from "node:net";
|
||||
import tls from "node:tls";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { logger } from "../logger";
|
||||
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
const DEFAULT_RETRY_DELAY = 1000; // 1 second
|
||||
|
||||
const createTLSConnection = (relayHost: string, relayPort: number, tlsOptions: tls.TlsOptions = {}) => {
|
||||
return new Promise<tls.TLSSocket>((resolve, reject) => {
|
||||
// @ts-expect-error this is resolved in next connect
|
||||
const socket = new tls.TLSSocket(null, {
|
||||
rejectUnauthorized: true,
|
||||
...tlsOptions
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
socket.removeAllListeners();
|
||||
socket.end();
|
||||
};
|
||||
|
||||
socket.once("error", (err) => {
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
socket.connect(relayPort, relayHost, () => {
|
||||
resolve(socket);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
type TPingGatewayAndVerifyDTO = {
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions: tls.TlsOptions;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export const pingGatewayAndVerify = async ({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions = {},
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
}: TPingGatewayAndVerifyDTO) => {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt += 1) {
|
||||
try {
|
||||
const socket = await createTLSConnection(relayHost, relayPort, tlsOptions);
|
||||
socket.setTimeout(2000);
|
||||
|
||||
const pingResult = await new Promise((resolve, reject) => {
|
||||
socket.once("timeout", () => {
|
||||
socket.destroy();
|
||||
reject(new Error("Timeout"));
|
||||
});
|
||||
socket.once("close", () => {
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
socket.once("end", () => {
|
||||
socket.destroy();
|
||||
});
|
||||
socket.once("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
socket.write(Buffer.from("PING\n"), () => {
|
||||
socket.once("data", (data) => {
|
||||
const response = (data as string).toString();
|
||||
const certificate = socket.getPeerCertificate();
|
||||
|
||||
if (certificate.subject.CN !== identityId || certificate.subject.O !== orgId) {
|
||||
throw new BadRequestError({
|
||||
message: `Invalid gateway. Certificate not found for ${identityId} in organization ${orgId}`
|
||||
});
|
||||
}
|
||||
|
||||
if (response === "PONG") {
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(`Unexpected response: ${response}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
socket.end();
|
||||
return pingResult;
|
||||
} catch (err) {
|
||||
lastError = err as Error;
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, DEFAULT_RETRY_DELAY);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.error(lastError);
|
||||
throw new BadRequestError({
|
||||
message: `Failed to ping gateway after ${maxRetries} attempts. Last error: ${lastError?.message}`
|
||||
});
|
||||
};
|
||||
|
||||
interface TProxyServer {
|
||||
server: net.Server;
|
||||
port: number;
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
const setupProxyServer = ({
|
||||
targetPort,
|
||||
targetHost,
|
||||
tlsOptions = {},
|
||||
relayHost,
|
||||
relayPort
|
||||
}: {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayPort: number;
|
||||
relayHost: string;
|
||||
tlsOptions: tls.TlsOptions;
|
||||
}): Promise<TProxyServer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
server.on("connection", async (clientSocket) => {
|
||||
try {
|
||||
const targetSocket = await createTLSConnection(relayHost, relayPort, tlsOptions);
|
||||
|
||||
targetSocket.write(Buffer.from(`FORWARD-TCP ${targetHost}:${targetPort}\n`), () => {
|
||||
clientSocket.on("data", (data) => {
|
||||
const flushed = targetSocket.write(data);
|
||||
if (!flushed) {
|
||||
clientSocket.pause();
|
||||
targetSocket.once("drain", () => {
|
||||
clientSocket.resume();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
targetSocket.on("data", (data) => {
|
||||
const flushed = clientSocket.write(data as string);
|
||||
if (!flushed) {
|
||||
targetSocket.pause();
|
||||
clientSocket.once("drain", () => {
|
||||
targetSocket.resume();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
clientSocket?.unpipe();
|
||||
clientSocket?.end();
|
||||
targetSocket?.unpipe();
|
||||
targetSocket?.end();
|
||||
};
|
||||
|
||||
clientSocket.on("error", (err) => {
|
||||
logger.error(err, "Client socket error");
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
targetSocket.on("error", (err) => {
|
||||
logger.error(err, "Target socket error");
|
||||
cleanup();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
clientSocket.on("end", cleanup);
|
||||
targetSocket.on("end", cleanup);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to establish target connection:");
|
||||
clientSocket.end();
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
server.listen(0, () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
server.close();
|
||||
reject(new Error("Failed to get server port"));
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Gateway proxy started");
|
||||
resolve({
|
||||
server,
|
||||
port: address.port,
|
||||
cleanup: () => {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
interface ProxyOptions {
|
||||
targetHost: string;
|
||||
targetPort: number;
|
||||
relayHost: string;
|
||||
relayPort: number;
|
||||
tlsOptions?: tls.TlsOptions;
|
||||
maxRetries?: number;
|
||||
identityId: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
export const withGatewayProxy = async (
|
||||
callback: (port: number) => Promise<void>,
|
||||
options: ProxyOptions
|
||||
): Promise<void> => {
|
||||
const {
|
||||
relayHost,
|
||||
relayPort,
|
||||
targetHost,
|
||||
targetPort,
|
||||
tlsOptions = {},
|
||||
maxRetries = DEFAULT_MAX_RETRIES,
|
||||
identityId,
|
||||
orgId
|
||||
} = options;
|
||||
|
||||
// First, try to ping the gateway
|
||||
await pingGatewayAndVerify({
|
||||
relayHost,
|
||||
relayPort,
|
||||
tlsOptions,
|
||||
maxRetries,
|
||||
identityId,
|
||||
orgId
|
||||
});
|
||||
|
||||
// Setup the proxy server
|
||||
const { port, cleanup } = await setupProxyServer({ targetHost, targetPort, relayPort, relayHost, tlsOptions });
|
||||
|
||||
try {
|
||||
// Execute the callback with the allocated port
|
||||
await callback(port);
|
||||
} catch (err) {
|
||||
logger.error(err, "Failed to proxy");
|
||||
throw new BadRequestError({ message: (err as Error)?.message });
|
||||
} finally {
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
cleanup();
|
||||
}
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const TURN_TOKEN_TTL = 60 * 60 * 1000; // 24 hours in milliseconds
|
||||
export const getTurnCredentials = (id: string, authSecret: string, ttl = TURN_TOKEN_TTL) => {
|
||||
const timestamp = Math.floor((Date.now() + ttl) / 1000);
|
||||
const username = `${timestamp}:${id}`;
|
||||
|
||||
const hmac = crypto.createHmac("sha1", authSecret);
|
||||
hmac.update(username);
|
||||
const password = hmac.digest("base64");
|
||||
|
||||
return {
|
||||
username,
|
||||
password
|
||||
};
|
||||
};
|
@ -27,10 +27,6 @@ import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-
|
||||
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
|
||||
import { externalKmsDALFactory } from "@app/ee/services/external-kms/external-kms-dal";
|
||||
import { externalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
|
||||
import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
|
||||
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
|
||||
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
|
||||
import { groupDALFactory } from "@app/ee/services/group/group-dal";
|
||||
import { groupServiceFactory } from "@app/ee/services/group/group-service";
|
||||
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
|
||||
@ -397,10 +393,6 @@ export const registerRoutes = async (
|
||||
const kmipOrgConfigDAL = kmipOrgConfigDALFactory(db);
|
||||
const kmipOrgServerCertificateDAL = kmipOrgServerCertificateDALFactory(db);
|
||||
|
||||
const orgGatewayConfigDAL = orgGatewayConfigDALFactory(db);
|
||||
const gatewayDAL = gatewayDALFactory(db);
|
||||
const projectGatewayDAL = projectGatewayDALFactory(db);
|
||||
|
||||
const permissionService = permissionServiceFactory({
|
||||
permissionDAL,
|
||||
orgRoleDAL,
|
||||
@ -1308,19 +1300,7 @@ export const registerRoutes = async (
|
||||
kmsService
|
||||
});
|
||||
|
||||
const gatewayService = gatewayServiceFactory({
|
||||
permissionService,
|
||||
gatewayDAL,
|
||||
kmsService,
|
||||
licenseService,
|
||||
orgGatewayConfigDAL,
|
||||
keyStore,
|
||||
projectGatewayDAL
|
||||
});
|
||||
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders({
|
||||
gatewayService
|
||||
});
|
||||
const dynamicSecretProviders = buildDynamicSecretProviders();
|
||||
const dynamicSecretQueueService = dynamicSecretLeaseQueueServiceFactory({
|
||||
queueService,
|
||||
dynamicSecretLeaseDAL,
|
||||
@ -1338,10 +1318,8 @@ export const registerRoutes = async (
|
||||
folderDAL,
|
||||
permissionService,
|
||||
licenseService,
|
||||
kmsService,
|
||||
projectGatewayDAL
|
||||
kmsService
|
||||
});
|
||||
|
||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||
projectDAL,
|
||||
permissionService,
|
||||
@ -1579,8 +1557,7 @@ export const registerRoutes = async (
|
||||
appConnection: appConnectionService,
|
||||
secretSync: secretSyncService,
|
||||
kmip: kmipService,
|
||||
kmipOperation: kmipOperationService,
|
||||
gateway: gatewayService
|
||||
kmipOperation: kmipOperationService
|
||||
});
|
||||
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
@ -537,12 +537,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.CREATE.secretReminderRepeatDays),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.CREATE.secretReminderNote)
|
||||
}),
|
||||
response: {
|
||||
200: z.union([
|
||||
@ -645,12 +640,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderRepeatDays: z
|
||||
.number()
|
||||
.optional()
|
||||
@ -2063,12 +2053,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.UPDATE.skipMultilineEncoding),
|
||||
newSecretName: SecretNameSchema.optional().describe(RAW_SECRETS.UPDATE.newSecretName),
|
||||
tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds),
|
||||
secretReminderNote: z
|
||||
.string()
|
||||
.max(1024, "Secret reminder note cannot exceed 1024 characters")
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
secretReminderRepeatDays: z
|
||||
.number()
|
||||
|
17
cli/go.mod
17
cli/go.mod
@ -18,16 +18,14 @@ require (
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/muesli/roff v0.1.0
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9
|
||||
github.com/pion/logging v0.2.3
|
||||
github.com/pion/turn/v4 v4.0.0
|
||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||
github.com/rs/cors v1.11.0
|
||||
github.com/rs/zerolog v1.26.1
|
||||
github.com/spf13/cobra v1.6.1
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/term v0.29.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/term v0.27.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@ -83,10 +81,6 @@ require (
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
@ -94,7 +88,6 @@ require (
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.0 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
@ -105,9 +98,9 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/api v0.188.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
|
34
cli/go.sum
34
cli/go.sum
@ -347,18 +347,6 @@ github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5d
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 h1:lL+y4Xv20pVlCGyLzNHRC0I0rIHhIL1lTvHizoS/dU8=
|
||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
||||
github.com/pion/dtls/v3 v3.0.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U=
|
||||
github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg=
|
||||
github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI=
|
||||
github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -422,8 +410,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
@ -472,8 +458,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -574,8 +560,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -624,11 +610,11 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -638,8 +624,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
@ -544,59 +544,3 @@ func CallUpdateRawSecretsV3(httpClient *resty.Client, request UpdateRawSecretByN
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CallRegisterGatewayIdentityV1(httpClient *resty.Client) (*GetRelayCredentialsResponseV1, error) {
|
||||
var resBody GetRelayCredentialsResponseV1
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/register-identity", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, fmt.Errorf("CallRegisterGatewayIdentityV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return &resBody, nil
|
||||
}
|
||||
|
||||
func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCertRequestV1) (*ExchangeRelayCertResponseV1, error) {
|
||||
var resBody ExchangeRelayCertResponseV1
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&resBody).
|
||||
SetBody(request).
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/exchange-cert", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return nil, fmt.Errorf("CallExchangeRelayCertV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return &resBody, nil
|
||||
}
|
||||
|
||||
func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetHeader("User-Agent", USER_AGENT).
|
||||
Post(fmt.Sprintf("%v/v1/gateways/heartbeat", config.INFISICAL_URL))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("CallGatewayHeartBeatV1: Unable to complete api request [err=%w]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return fmt.Errorf("CallGatewayHeartBeatV1: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -629,22 +629,3 @@ type GetRawSecretV3ByNameResponse struct {
|
||||
} `json:"secret"`
|
||||
ETag string
|
||||
}
|
||||
|
||||
type GetRelayCredentialsResponseV1 struct {
|
||||
TurnServerUsername string `json:"turnServerUsername"`
|
||||
TurnServerPassword string `json:"turnServerPassword"`
|
||||
TurnServerRealm string `json:"turnServerRealm"`
|
||||
TurnServerAddress string `json:"turnServerAddress"`
|
||||
InfisicalStaticIp string `json:"infisicalStaticIp"`
|
||||
}
|
||||
|
||||
type ExchangeRelayCertRequestV1 struct {
|
||||
RelayAddress string `json:"relayAddress"`
|
||||
}
|
||||
|
||||
type ExchangeRelayCertResponseV1 struct {
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Certificate string `json:"certificate"`
|
||||
CertificateChain string `json:"certificateChain"`
|
||||
}
|
||||
|
@ -1,120 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
// "fmt"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/api"
|
||||
// "github.com/Infisical/infisical-merge/packages/models"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/gateway"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
||||
// "github.com/rs/zerolog/log"
|
||||
|
||||
// "github.com/go-resty/resty/v2"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var gatewayCmd = &cobra.Command{
|
||||
Example: `infisical gateway`,
|
||||
Short: "Used to infisical gateway",
|
||||
Use: "gateway",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
token, err := util.GetInfisicalToken(cmd)
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
if token == nil {
|
||||
util.HandleError(fmt.Errorf("Token not found"))
|
||||
}
|
||||
|
||||
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
sigStopCh := make(chan bool, 1)
|
||||
|
||||
ctx, cancel := context.WithCancel(cmd.Context())
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-sigCh
|
||||
close(sigStopCh)
|
||||
cancel()
|
||||
|
||||
// If we get a second signal, force exit
|
||||
<-sigCh
|
||||
log.Warn().Msgf("Force exit triggered")
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
// Main gateway retry loop with proper context handling
|
||||
retryTicker := time.NewTicker(5 * time.Second)
|
||||
defer retryTicker.Stop()
|
||||
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
gatewayInstance, err := gateway.NewGateway(token.Token)
|
||||
if err != nil {
|
||||
util.HandleError(err)
|
||||
}
|
||||
|
||||
if err = gatewayInstance.ConnectWithRelay(); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
|
||||
log.Error().Msgf("Gateway connection error with relay: %s", err)
|
||||
log.Info().Msg("Retrying connection in 5 seconds...")
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = gatewayInstance.Listen(ctx)
|
||||
if ctx.Err() != nil {
|
||||
log.Info().Msg("Gateway shutdown complete")
|
||||
return
|
||||
}
|
||||
log.Error().Msgf("Gateway listen error: %s", err)
|
||||
log.Info().Msg("Retrying connection in 5 seconds...")
|
||||
select {
|
||||
case <-retryTicker.C:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway")
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
gatewayCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
|
||||
command.Flags().MarkHidden("domain")
|
||||
command.Parent().HelpFunc()(command, strings)
|
||||
})
|
||||
gatewayCmd.Flags().String("token", "", "Connect with Infisical using machine identity access token")
|
||||
|
||||
rootCmd.AddCommand(gatewayCmd)
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
log.Info().Msgf("New connection from: %s", conn.RemoteAddr().String())
|
||||
|
||||
// Use buffered reader for better handling of fragmented data
|
||||
reader := bufio.NewReader(conn)
|
||||
for {
|
||||
msg, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return
|
||||
}
|
||||
log.Error().Msgf("Error reading command: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd := bytes.ToUpper(bytes.TrimSpace(bytes.Split(msg, []byte(" "))[0]))
|
||||
args := bytes.TrimSpace(bytes.TrimPrefix(msg, cmd))
|
||||
|
||||
switch string(cmd) {
|
||||
case "FORWARD-TCP":
|
||||
proxyAddress := string(bytes.Split(args, []byte(" "))[0])
|
||||
destTarget, err := net.Dial("tcp", proxyAddress)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to connect to target: %v", err)
|
||||
return
|
||||
}
|
||||
defer destTarget.Close()
|
||||
|
||||
// Handle buffered data
|
||||
buffered := reader.Buffered()
|
||||
if buffered > 0 {
|
||||
bufferedData := make([]byte, buffered)
|
||||
_, err := reader.Read(bufferedData)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Error reading buffered data: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = destTarget.Write(bufferedData); err != nil {
|
||||
log.Error().Msgf("Error writing buffered data: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
CopyData(conn, destTarget)
|
||||
return
|
||||
case "PING":
|
||||
if _, err := conn.Write([]byte("PONG")); err != nil {
|
||||
log.Error().Msgf("Error writing PONG response: %v", err)
|
||||
}
|
||||
return
|
||||
default:
|
||||
log.Error().Msgf("Unknown command: %s", string(cmd))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type CloseWrite interface {
|
||||
CloseWrite() error
|
||||
}
|
||||
|
||||
func CopyData(src, dst net.Conn) {
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
copyAndClose := func(dst, src net.Conn, done chan<- bool) {
|
||||
defer wg.Done()
|
||||
_, err := io.Copy(dst, src)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Error().Msgf("Copy error: %v", err)
|
||||
}
|
||||
|
||||
// Signal we're done writing
|
||||
done <- true
|
||||
|
||||
// Half close the connection if possible
|
||||
if c, ok := dst.(CloseWrite); ok {
|
||||
c.CloseWrite()
|
||||
}
|
||||
}
|
||||
|
||||
done1 := make(chan bool, 1)
|
||||
done2 := make(chan bool, 1)
|
||||
|
||||
go copyAndClose(dst, src, done1)
|
||||
go copyAndClose(src, dst, done2)
|
||||
|
||||
// Wait for both copies to complete
|
||||
<-done1
|
||||
<-done2
|
||||
wg.Wait()
|
||||
}
|
@ -1,350 +0,0 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pion/logging"
|
||||
"github.com/pion/turn/v4"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GatewayConfig struct {
|
||||
TurnServerUsername string
|
||||
TurnServerPassword string
|
||||
TurnServerAddress string
|
||||
InfisicalStaticIp string
|
||||
SerialNumber string
|
||||
PrivateKey string
|
||||
Certificate string
|
||||
CertificateChain string
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
httpClient *resty.Client
|
||||
config *GatewayConfig
|
||||
client *turn.Client
|
||||
}
|
||||
|
||||
func NewGateway(identityToken string) (Gateway, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(identityToken)
|
||||
|
||||
return Gateway{
|
||||
httpClient: httpClient,
|
||||
config: &GatewayConfig{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *Gateway) ConnectWithRelay() error {
|
||||
relayDetails, err := api.CallRegisterGatewayIdentityV1(g.httpClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relayAddress, relayPort := strings.Split(relayDetails.TurnServerAddress, ":")[0], strings.Split(relayDetails.TurnServerAddress, ":")[1]
|
||||
var conn net.Conn
|
||||
|
||||
// Dial TURN Server
|
||||
if relayPort == "5349" {
|
||||
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
|
||||
conn, err = tls.Dial("tcp", relayDetails.TurnServerAddress, &tls.Config{
|
||||
InsecureSkipVerify: false,
|
||||
ServerName: relayAddress,
|
||||
})
|
||||
} else {
|
||||
log.Info().Msgf("Provided relay port %s. Using non TLS connection.", relayPort)
|
||||
peerAddr, err := net.ResolveTCPAddr("tcp", relayDetails.TurnServerAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse turn server address: %w", err)
|
||||
}
|
||||
conn, err = net.DialTCP("tcp", nil, peerAddr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect with relay server: %w", err)
|
||||
}
|
||||
|
||||
// Start a new TURN Client and wrap our net.Conn in a STUNConn
|
||||
// This allows us to simulate datagram based communication over a net.Conn
|
||||
cfg := &turn.ClientConfig{
|
||||
STUNServerAddr: relayDetails.TurnServerAddress,
|
||||
TURNServerAddr: relayDetails.TurnServerAddress,
|
||||
Conn: turn.NewSTUNConn(conn),
|
||||
Username: relayDetails.TurnServerUsername,
|
||||
Password: relayDetails.TurnServerPassword,
|
||||
Realm: relayDetails.TurnServerRealm,
|
||||
LoggerFactory: logging.NewDefaultLoggerFactory(),
|
||||
}
|
||||
|
||||
client, err := turn.NewClient(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create relay client: %w", err)
|
||||
}
|
||||
|
||||
g.config = &GatewayConfig{
|
||||
TurnServerUsername: relayDetails.TurnServerUsername,
|
||||
TurnServerPassword: relayDetails.TurnServerPassword,
|
||||
TurnServerAddress: relayDetails.TurnServerAddress,
|
||||
InfisicalStaticIp: relayDetails.InfisicalStaticIp,
|
||||
}
|
||||
// if port not specific allow all port
|
||||
if relayDetails.InfisicalStaticIp != "" && !strings.Contains(relayDetails.InfisicalStaticIp, ":") {
|
||||
g.config.InfisicalStaticIp = g.config.InfisicalStaticIp + ":0"
|
||||
}
|
||||
|
||||
g.client = client
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Gateway) Listen(ctx context.Context) error {
|
||||
defer g.client.Close()
|
||||
err := g.client.Listen()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to listen to relay server: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg("Connected with relay")
|
||||
|
||||
// Allocate a relay socket on the TURN server. On success, it
|
||||
// will return a net.PacketConn which represents the remote
|
||||
// socket.
|
||||
relayNonTlsConn, err := g.client.AllocateTCP()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to allocate relay connection: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Msg(relayNonTlsConn.Addr().String())
|
||||
defer func() {
|
||||
if closeErr := relayNonTlsConn.Close(); closeErr != nil {
|
||||
log.Error().Msgf("Failed to close connection: %s", closeErr)
|
||||
}
|
||||
}()
|
||||
|
||||
gatewayCert, err := api.CallExchangeRelayCertV1(g.httpClient, api.ExchangeRelayCertRequestV1{
|
||||
RelayAddress: relayNonTlsConn.Addr().String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.config.SerialNumber = gatewayCert.SerialNumber
|
||||
g.config.PrivateKey = gatewayCert.PrivateKey
|
||||
g.config.Certificate = gatewayCert.Certificate
|
||||
g.config.CertificateChain = gatewayCert.CertificateChain
|
||||
|
||||
shutdownCh := make(chan bool, 1)
|
||||
|
||||
if g.config.InfisicalStaticIp != "" {
|
||||
log.Info().Msgf("Found static ip from Infisical: %s. Creating permission IP lifecycle", g.config.InfisicalStaticIp)
|
||||
peerAddr, err := net.ResolveTCPAddr("tcp", g.config.InfisicalStaticIp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to parse infisical static ip: %w", err)
|
||||
}
|
||||
g.registerPermissionLifecycle(func() error {
|
||||
err := relayNonTlsConn.CreatePermissions(peerAddr)
|
||||
return err
|
||||
}, shutdownCh)
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse cert: %s", err)
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM([]byte(gatewayCert.CertificateChain))
|
||||
|
||||
relayConn := tls.NewListener(relayNonTlsConn, &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientCAs: caCertPool,
|
||||
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||
})
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
log.Info().Msg("Gateway started successfully")
|
||||
g.registerHeartBeat(errCh, shutdownCh)
|
||||
g.registerRelayIsActive(relayNonTlsConn.Addr().String(), errCh, shutdownCh)
|
||||
|
||||
// Create a WaitGroup to track active connections
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
for {
|
||||
if relayDeadlineConn, ok := relayConn.(*net.TCPListener); ok {
|
||||
relayDeadlineConn.SetDeadline(time.Now().Add(1 * time.Second))
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-shutdownCh:
|
||||
return
|
||||
default:
|
||||
// Accept new relay connection
|
||||
conn, err := relayConn.Accept()
|
||||
if err != nil {
|
||||
// Check if it's a timeout error (which we expect due to our deadline)
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "data contains incomplete STUN or TURN frame") {
|
||||
log.Error().Msgf("Failed to accept connection: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
tlsConn, ok := conn.(*tls.Conn)
|
||||
if !ok {
|
||||
log.Error().Msg("Failed to convert to TLS connection")
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Set a deadline for the handshake to prevent hanging
|
||||
tlsConn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
err = tlsConn.Handshake()
|
||||
// Clear the deadline after handshake
|
||||
tlsConn.SetDeadline(time.Time{})
|
||||
if err != nil {
|
||||
log.Error().Msgf("TLS handshake failed: %v", err)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
// Get connection state which contains certificate information
|
||||
state := tlsConn.ConnectionState()
|
||||
if len(state.PeerCertificates) > 0 {
|
||||
organizationUnit := state.PeerCertificates[0].Subject.OrganizationalUnit
|
||||
commonName := state.PeerCertificates[0].Subject.CommonName
|
||||
if organizationUnit[0] != "gateway-client" || commonName != "cloud" {
|
||||
log.Error().Msgf("Client certificate verification failed. Received %s, %s", organizationUnit, commonName)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the connection in a goroutine
|
||||
wg.Add(1)
|
||||
go func(c net.Conn) {
|
||||
defer wg.Done()
|
||||
defer c.Close()
|
||||
|
||||
// Monitor parent context to close this connection when needed
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.Close() // Force close connection when context is canceled
|
||||
case <-shutdownCh:
|
||||
c.Close() // Force close connection when accepting loop is done
|
||||
}
|
||||
}()
|
||||
|
||||
handleConnection(c)
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Shutting down gateway...")
|
||||
case err = <-errCh:
|
||||
}
|
||||
|
||||
// Signal the accept loop to stop
|
||||
close(shutdownCh)
|
||||
|
||||
// Set a timeout for waiting on connections to close
|
||||
waitCh := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
// All connections closed normally
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Warn().Msg("Timeout waiting for connections to close gracefully")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *Gateway) registerHeartBeat(errCh chan error, done chan bool) {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
|
||||
go func() {
|
||||
time.Sleep(10 * time.Second)
|
||||
log.Info().Msg("Registering first heart beat")
|
||||
err := api.CallGatewayHeartBeatV1(g.httpClient)
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to register heartbeat: %s", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Info().Msg("Registering heart beat")
|
||||
err := api.CallGatewayHeartBeatV1(g.httpClient)
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *Gateway) registerPermissionLifecycle(permissionFn func() error, done chan bool) {
|
||||
ticker := time.NewTicker(3 * time.Minute)
|
||||
|
||||
go func() {
|
||||
// wait for 5 mins
|
||||
permissionFn()
|
||||
log.Printf("Created permission for incoming connections")
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
permissionFn()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (g *Gateway) registerRelayIsActive(serverAddr string, errCh chan error, done chan bool) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
conn, err := net.Dial("tcp", serverAddr)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
@ -56,3 +56,4 @@ volumes:
|
||||
|
||||
networks:
|
||||
infisical:
|
||||
|
||||
|
@ -11,6 +11,13 @@ To interact with the Infisical API, you will need to obtain an access token. Fol
|
||||
**FAQ**
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="What happened to the Service Token and API Key authentication modes?">
|
||||
The Service Token and API Key authentication modes are being deprecated out in favor of [Identities](/documentation/platform/identity).
|
||||
We expect to make a deprecation notice in the coming months alongside a larger deprecation initiative planned for Q1/Q2 2024.
|
||||
|
||||
With identities, we're improving significantly over the shortcomings of Service Tokens and API Keys. Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based
|
||||
permission system used by users, and comes with ample more configurable security measures.
|
||||
</Accordion>
|
||||
<Accordion title="Why can I not create, read, update, or delete an identity?">
|
||||
There are a few reasons for why this might happen:
|
||||
|
||||
|
@ -1,81 +0,0 @@
|
||||
---
|
||||
title: "Gateway"
|
||||
sidebarTitle: "Overview"
|
||||
description: "How to access private network resources from Infisical"
|
||||
---
|
||||
|
||||
The Infisical Gateway provides secure access to private resources within your network without needing direct inbound connections to your environment.
|
||||
This method keeps your resources fully protected from external access while enabling Infisical to securely interact with resources like databases.
|
||||
Common use cases include generating dynamic credentials or rotating credentials for private databases.
|
||||
|
||||
<Info>
|
||||
**Note:** Gateway is a paid feature. - **Infisical Cloud users:** Gateway is
|
||||
available under the **Enterprise Tier**. - **Self-Hosted Infisical:** Please
|
||||
contact [sales@infisical.com](mailto:sales@infisical.com) to purchase an
|
||||
enterprise license.
|
||||
</Info>
|
||||
|
||||
## How It Works
|
||||
|
||||
The Gateway serves as a secure intermediary that facilitates direct communication between the Infisical server and your private network.
|
||||
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>
|
||||
|
Binary file not shown.
Before ![]() (image error) Size: 338 KiB |
Binary file not shown.
Before ![]() (image error) Size: 375 KiB |
Binary file not shown.
Before ![]() (image error) Size: 384 KiB |
Binary file not shown.
Before ![]() (image error) Size: 365 KiB |
Binary file not shown.
Before ![]() (image error) Size: 433 KiB |
@ -1,32 +0,0 @@
|
||||
---
|
||||
title: "AB Initio"
|
||||
description: "How to use Infisical secrets in AB Initio."
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Set up and add envars to [Infisical](https://app.infisical.com).
|
||||
- Install the [Infisical CLI](https://infisical.com/docs/cli/overview) to your server.
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Authorize Infisical for AB Initio">
|
||||
Create a [machine identity](https://infisical.com/docs/documentation/platform/identities/machine-identities#machine-identities) in Infisical and give it the appropriate read permissions for the desired project and secret paths.
|
||||
</Step>
|
||||
<Step title="Add Infisical CLI to your workflow">
|
||||
Update your AB Initio workflows to use Infisical CLI to inject Infisical secrets as environment variables.
|
||||
|
||||
```bash
|
||||
# Login using the machine identity. Modify this accordingly based on the authentication method used.
|
||||
export INFISICAL_TOKEN=$(infisical login --method=universal-auth --client-id=$INFISICAL_CLIENT_ID --client-secret=$INFISICAL_CLIENT_SECRET --silent --plain)
|
||||
|
||||
# Fetch secrets from Infisical
|
||||
infisical export --projectId="<>" --env="prod" > infisical.env
|
||||
|
||||
# Inject secrets as environment variables
|
||||
source infisical.env
|
||||
```
|
||||
</Step>
|
||||
|
||||
</Steps>
|
@ -201,10 +201,6 @@
|
||||
"documentation/platform/dynamic-secrets/totp"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Gateway",
|
||||
"pages": ["documentation/platform/gateways/overview"]
|
||||
},
|
||||
"documentation/platform/project-templates",
|
||||
{
|
||||
"group": "Workflow Integrations",
|
||||
@ -519,8 +515,7 @@
|
||||
"integrations/frameworks/laravel",
|
||||
"integrations/frameworks/rails",
|
||||
"integrations/frameworks/dotnet",
|
||||
"integrations/platforms/pm2",
|
||||
"integrations/frameworks/ab-initio"
|
||||
"integrations/platforms/pm2"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
113
frontend/package-lock.json
generated
113
frontend/package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.12.0",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.3",
|
||||
@ -1605,16 +1605,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/core": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
|
||||
"integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.4.tgz",
|
||||
"integrity": "sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^5.0.0",
|
||||
"@octokit/graphql": "^8.0.0",
|
||||
"@octokit/request": "^9.0.0",
|
||||
"@octokit/request-error": "^6.0.1",
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/graphql": "^8.1.2",
|
||||
"@octokit/request": "^9.2.1",
|
||||
"@octokit/request-error": "^6.1.7",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"before-after-hook": "^3.0.2",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
@ -1623,12 +1623,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
|
||||
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
|
||||
"version": "10.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.3.tgz",
|
||||
"integrity": "sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -1636,13 +1636,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz",
|
||||
"integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==",
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.1.tgz",
|
||||
"integrity": "sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^9.0.0",
|
||||
"@octokit/types": "^13.0.0",
|
||||
"@octokit/request": "^9.2.2",
|
||||
"@octokit/types": "^13.8.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -1650,18 +1650,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/openapi-types": {
|
||||
"version": "22.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
|
||||
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
|
||||
"version": "23.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz",
|
||||
"integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.6.tgz",
|
||||
"integrity": "sha512-zcvqqf/+TicbTCa/Z+3w4eBJcAxCFymtc0UAIsR3dEVoNilWld4oXdscQ3laXamTszUZdusw97K8+DrbFiOwjw==",
|
||||
"version": "11.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.4.3.tgz",
|
||||
"integrity": "sha512-tBXaAbXkqVJlRoA/zQVe9mUdb8rScmivqtpv3ovsC5xhje/a+NOCivs7eUhWBwCApJVsR4G5HMeaLbq7PxqZGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.2"
|
||||
"@octokit/types": "^13.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@ -1683,12 +1683,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "13.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.6.tgz",
|
||||
"integrity": "sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==",
|
||||
"version": "13.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.3.1.tgz",
|
||||
"integrity": "sha512-o8uOBdsyR+WR8MK9Cco8dCgvG13H1RlM1nWnK/W7TEACQBFux/vPREgKucxUfuDQ5yi1T3hGf4C5ZmZXAERgwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.6.1"
|
||||
"@octokit/types": "^13.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
@ -1698,14 +1698,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request": {
|
||||
"version": "9.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz",
|
||||
"integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==",
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.2.tgz",
|
||||
"integrity": "sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^10.0.0",
|
||||
"@octokit/request-error": "^6.0.1",
|
||||
"@octokit/types": "^13.1.0",
|
||||
"@octokit/endpoint": "^10.1.3",
|
||||
"@octokit/request-error": "^6.1.7",
|
||||
"@octokit/types": "^13.6.2",
|
||||
"fast-content-type-parse": "^2.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@ -1713,39 +1714,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request-error": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz",
|
||||
"integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==",
|
||||
"version": "6.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.7.tgz",
|
||||
"integrity": "sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.0.0"
|
||||
"@octokit/types": "^13.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "21.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.0.2.tgz",
|
||||
"integrity": "sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==",
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
|
||||
"integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^6.1.2",
|
||||
"@octokit/plugin-paginate-rest": "^11.0.0",
|
||||
"@octokit/core": "^6.1.4",
|
||||
"@octokit/plugin-paginate-rest": "^11.4.2",
|
||||
"@octokit/plugin-request-log": "^5.3.1",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.0.0"
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "13.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.2.tgz",
|
||||
"integrity": "sha512-WpbZfZUcZU77DrSW4wbsSgTPfKcp286q3ItaIgvSbBpZJlu6mnYXAkjZz6LVZPXkEvLIM8McanyZejKTYUHipA==",
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz",
|
||||
"integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^22.2.0"
|
||||
"@octokit/openapi-types": "^23.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@peculiar/asn1-cms": {
|
||||
@ -6905,6 +6906,22 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
|
||||
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
@ -27,7 +27,7 @@
|
||||
"@headlessui/react": "^1.7.19",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@lottiefiles/dotlottie-react": "^0.12.0",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@peculiar/x509": "^1.12.3",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.3",
|
||||
|
File diff suppressed because one or more lines are too long
@ -20,7 +20,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
sideOffset={-8}
|
||||
sideOffset={10}
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={twMerge(
|
||||
|
@ -45,11 +45,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{title && (
|
||||
<DialogPrimitive.Title>
|
||||
<CardTitle subTitle={subTitle}>{title}</CardTitle>
|
||||
</DialogPrimitive.Title>
|
||||
)}
|
||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||
<CardBody
|
||||
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
|
@ -7,14 +7,6 @@ export enum OrgPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum OrgGatewayPermissionActions {
|
||||
// is there a better word for this. This mean can an identity be a gateway
|
||||
CreateGateways = "create-gateways",
|
||||
ListGateways = "list-gateways",
|
||||
EditGateways = "edit-gateways",
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -33,8 +25,7 @@ export enum OrgPermissionSubjects {
|
||||
AuditLogs = "audit-logs",
|
||||
ProjectTemplates = "project-templates",
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway"
|
||||
Kmip = "kmip"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@ -77,8 +68,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
|
||||
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway];
|
||||
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip];
|
||||
// TODO(scott): add back once org UI refactored
|
||||
// | [
|
||||
// OrgPermissionAppConnectionActions,
|
||||
|
@ -54,7 +54,6 @@ export type TDynamicSecretProvider =
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
gatewayId?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
@ -1,2 +0,0 @@
|
||||
export { useDeleteGatewayById, useUpdateGatewayById } from "./mutation";
|
||||
export { gatewaysQueryKeys } from "./queries";
|
@ -1,30 +0,0 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { gatewaysQueryKeys } from "./queries";
|
||||
import { TUpdateGatewayDTO } from "./types";
|
||||
|
||||
export const useDeleteGatewayById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
return apiRequest.delete(`/api/v1/gateways/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(gatewaysQueryKeys.list());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateGatewayById = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({ id, name, projectIds }: TUpdateGatewayDTO) => {
|
||||
return apiRequest.patch(`/api/v1/gateways/${id}`, { name, projectIds });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(gatewaysQueryKeys.list());
|
||||
}
|
||||
});
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { queryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TGateway, TListProjectGatewayDTO, TProjectGateway } from "./types";
|
||||
|
||||
export const gatewaysQueryKeys = {
|
||||
allKey: () => ["gateways"],
|
||||
listKey: () => [...gatewaysQueryKeys.allKey(), "list"],
|
||||
list: () =>
|
||||
queryOptions({
|
||||
queryKey: gatewaysQueryKeys.listKey(),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways");
|
||||
return data.gateways;
|
||||
}
|
||||
}),
|
||||
listProjectGatewayKey: ({ projectId }: TListProjectGatewayDTO) => [
|
||||
...gatewaysQueryKeys.allKey(),
|
||||
"list",
|
||||
{ projectId }
|
||||
],
|
||||
listProjectGateways: ({ projectId }: TListProjectGatewayDTO) =>
|
||||
queryOptions({
|
||||
queryKey: gatewaysQueryKeys.listProjectGatewayKey({ projectId }),
|
||||
queryFn: async () => {
|
||||
const { data } = await apiRequest.get<{ gateways: TProjectGateway[] }>(
|
||||
`/api/v1/gateways/projects/${projectId}`
|
||||
);
|
||||
return data.gateways;
|
||||
}
|
||||
})
|
||||
};
|
@ -1,49 +0,0 @@
|
||||
export type TGateway = {
|
||||
id: string;
|
||||
identityId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
issuedAt: string;
|
||||
serialNumber: string;
|
||||
heartbeat: string;
|
||||
identity: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
projects: {
|
||||
name: string;
|
||||
id: string;
|
||||
slug: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type TProjectGateway = {
|
||||
id: string;
|
||||
identityId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
issuedAt: string;
|
||||
serialNumber: string;
|
||||
heartbeat: string;
|
||||
projectGatewayId: string;
|
||||
identity: {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateGatewayDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
projectIds?: string[];
|
||||
};
|
||||
|
||||
export type TDeleteGatewayDTO = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TListProjectGatewayDTO = {
|
||||
projectId: string;
|
||||
};
|
@ -10,7 +10,6 @@ export * from "./certificates";
|
||||
export * from "./certificateTemplates";
|
||||
export * from "./dynamicSecret";
|
||||
export * from "./dynamicSecretLease";
|
||||
export * from "./gateways";
|
||||
export * from "./groups";
|
||||
export * from "./identities";
|
||||
export * from "./identityProjectAdditionalPrivilege";
|
||||
|
@ -42,7 +42,6 @@ export type SubscriptionPlan = {
|
||||
has_used_trial: boolean;
|
||||
caCrl: boolean;
|
||||
instanceUserManagement: boolean;
|
||||
gateway: boolean;
|
||||
externalKms: boolean;
|
||||
pkiEst: boolean;
|
||||
enforceMfa: boolean;
|
||||
|
@ -101,13 +101,6 @@ export const OrganizationLayout = () => {
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="gateway" iconMode="reverse">
|
||||
Gateways
|
||||
</MenuItem>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/settings">
|
||||
{({ isActive }) => (
|
||||
<MenuItem isSelected={isActive} icon="toggle-settings">
|
||||
|
@ -20,46 +20,44 @@ export const MenuIconButton = <T extends ElementType = "button">({
|
||||
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
|
||||
const iconRef = useRef<DotLottie | null>(null);
|
||||
return (
|
||||
<div className={!isSelected ? "hover:px-1" : ""}>
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={twMerge(
|
||||
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded my-1 p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
||||
isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
isSelected ? "opacity-100" : "opacity-0"
|
||||
} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
|
||||
/>
|
||||
{icon && (
|
||||
<div className="my-auto mb-2 h-6 w-6">
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="h-full w-full"
|
||||
mode={lottieIconMode}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex-grow justify-center break-words text-center"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{children}
|
||||
<Item
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={twMerge(
|
||||
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
|
||||
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
|
||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||
className
|
||||
)}
|
||||
onMouseEnter={() => iconRef.current?.play()}
|
||||
onMouseLeave={() => iconRef.current?.stop()}
|
||||
ref={inputRef}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={`${
|
||||
isSelected ? "opacity-100" : "opacity-0"
|
||||
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
|
||||
/>
|
||||
{icon && (
|
||||
<div className="my-auto mb-2 h-6 w-6">
|
||||
<DotLottieReact
|
||||
dotLottieRefCallback={(el) => {
|
||||
iconRef.current = el;
|
||||
}}
|
||||
src={`/lotties/${icon}.json`}
|
||||
loop
|
||||
className="h-full w-full"
|
||||
mode={lottieIconMode}
|
||||
/>
|
||||
</div>
|
||||
</Item>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="flex-grow justify-center break-words text-center"
|
||||
style={{ fontSize: "10px" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Item>
|
||||
);
|
||||
};
|
||||
|
@ -5,7 +5,6 @@ import {
|
||||
faBook,
|
||||
faCheck,
|
||||
faCog,
|
||||
faDoorClosed,
|
||||
faEnvelope,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
@ -85,10 +84,6 @@ export const MinimizedOrgSidebar = () => {
|
||||
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
|
||||
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
|
||||
const { subscription } = useSubscription();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openSupport, setOpenSupport] = useState(false);
|
||||
const [openUser, setOpenUser] = useState(false);
|
||||
const [openOrg, setOpenOrg] = useState(false);
|
||||
|
||||
const { user } = useUser();
|
||||
const { mutateAsync } = useGetOrgTrialUrl();
|
||||
@ -165,29 +160,23 @@ export const MinimizedOrgSidebar = () => {
|
||||
>
|
||||
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
|
||||
<div>
|
||||
<div className="flex items-center hover:bg-mineshaft-700">
|
||||
<DropdownMenu open={openOrg} onOpenChange={setOpenOrg} modal>
|
||||
<DropdownMenuTrigger
|
||||
onMouseEnter={() => setOpenOrg(true)}
|
||||
onMouseLeave={() => setOpenOrg(false)}
|
||||
asChild
|
||||
>
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pb-5 pt-6 transition-all">
|
||||
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700">
|
||||
<DropdownMenu modal>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
|
||||
{currentOrg?.name.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
onMouseEnter={() => setOpenOrg(true)}
|
||||
onMouseLeave={() => setOpenOrg(false)}
|
||||
align="start"
|
||||
side="right"
|
||||
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "220px" }}
|
||||
className="p-1 shadow-mineshaft-600 drop-shadow-md"
|
||||
style={{ minWidth: "320px" }}
|
||||
>
|
||||
<div className="px-0.5 py-1">
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800 p-1 transition-all duration-300">
|
||||
<div className="px-2 py-1">
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
|
||||
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
|
||||
{currentOrg?.name.charAt(0)}
|
||||
</div>
|
||||
@ -252,7 +241,7 @@ export const MinimizedOrgSidebar = () => {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 px-1">
|
||||
<Link to="/organization/secret-manager/overview">
|
||||
{({ isActive }) => (
|
||||
<MenuIconButton
|
||||
@ -262,7 +251,7 @@ export const MinimizedOrgSidebar = () => {
|
||||
}
|
||||
icon="sliding-carousel"
|
||||
>
|
||||
Secrets
|
||||
Secret Manager
|
||||
</MenuIconButton>
|
||||
)}
|
||||
</Link>
|
||||
@ -275,7 +264,7 @@ export const MinimizedOrgSidebar = () => {
|
||||
}
|
||||
icon="note"
|
||||
>
|
||||
PKI
|
||||
Cert Manager
|
||||
</MenuIconButton>
|
||||
)}
|
||||
</Link>
|
||||
@ -307,41 +296,31 @@ export const MinimizedOrgSidebar = () => {
|
||||
<Link to="/organization/secret-scanning">
|
||||
{({ isActive }) => (
|
||||
<MenuIconButton isSelected={isActive} icon="secret-scan">
|
||||
Scanner
|
||||
Secret Scanning
|
||||
</MenuIconButton>
|
||||
)}
|
||||
</Link>
|
||||
<Link to="/organization/secret-sharing">
|
||||
{({ isActive }) => (
|
||||
<MenuIconButton isSelected={isActive} icon="lock-closed">
|
||||
Share
|
||||
Secret Sharing
|
||||
</MenuIconButton>
|
||||
)}
|
||||
</Link>
|
||||
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenuTrigger
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
asChild
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="w-full">
|
||||
<MenuIconButton
|
||||
lottieIconMode="reverse"
|
||||
icon="settings-cog"
|
||||
isSelected={isMoreSelected}
|
||||
>
|
||||
Admin
|
||||
Org Controls
|
||||
</MenuIconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
onMouseEnter={() => setOpen(true)}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
align="start"
|
||||
side="right"
|
||||
className="p-1"
|
||||
>
|
||||
<DropdownMenuContent align="start" side="right" className="p-1">
|
||||
<DropdownMenuLabel>Organization Options</DropdownMenuLabel>
|
||||
<Link to="/organization/access-management">
|
||||
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
|
||||
@ -353,13 +332,6 @@ export const MinimizedOrgSidebar = () => {
|
||||
App Connections
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<Link to="/organization/gateways">
|
||||
<DropdownMenuItem
|
||||
icon={<FontAwesomeIcon className="w-3" icon={faDoorClosed} />}
|
||||
>
|
||||
Gateways
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://eu.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||
@ -392,24 +364,14 @@ export const MinimizedOrgSidebar = () => {
|
||||
: "mb-4"
|
||||
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
|
||||
>
|
||||
<DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
|
||||
<DropdownMenuTrigger
|
||||
onMouseEnter={() => setOpenSupport(true)}
|
||||
onMouseLeave={() => setOpenSupport(false)}
|
||||
className="w-full"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="w-full">
|
||||
<MenuIconButton>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
|
||||
Support
|
||||
</MenuIconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
onMouseEnter={() => setOpenSupport(true)}
|
||||
onMouseLeave={() => setOpenSupport(false)}
|
||||
align="end"
|
||||
side="right"
|
||||
className="p-1"
|
||||
>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
|
||||
<DropdownMenuItem key={url as string}>
|
||||
<a
|
||||
@ -457,28 +419,17 @@ export const MinimizedOrgSidebar = () => {
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<DropdownMenu open={openUser} onOpenChange={setOpenUser}>
|
||||
<DropdownMenuTrigger
|
||||
onMouseEnter={() => setOpenUser(true)}
|
||||
onMouseLeave={() => setOpenUser(false)}
|
||||
className="w-full"
|
||||
asChild
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="w-full" asChild>
|
||||
<div>
|
||||
<MenuIconButton icon="user">User</MenuIconButton>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
onMouseEnter={() => setOpenUser(true)}
|
||||
onMouseLeave={() => setOpenUser(false)}
|
||||
side="right"
|
||||
align="end"
|
||||
className="p-1"
|
||||
>
|
||||
<div className="cursor-default px-1 py-1">
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
|
||||
<div className="p-1 pr-3">
|
||||
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<div className="px-2 py-1">
|
||||
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700">
|
||||
<div className="p-2">
|
||||
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col text-white">
|
||||
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
|
||||
@ -526,11 +477,11 @@ export const MinimizedOrgSidebar = () => {
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
)}
|
||||
<div className="mt-1 border-t border-mineshaft-600 pt-1">
|
||||
<Link to="/organization/admin">
|
||||
<DropdownMenuItem>Organization Admin Console</DropdownMenuItem>
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/organization/admin">
|
||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
||||
Organization Admin Console
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<div className="mt-1 h-1 border-t border-mineshaft-600" />
|
||||
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
|
||||
Log Out
|
||||
|
@ -19,12 +19,7 @@ import "./index.css";
|
||||
import "./translation";
|
||||
// don't want to use this?
|
||||
// have a look at the Quick start guide
|
||||
// for passing in lng and translations on init/
|
||||
|
||||
// https://vite.dev/guide/build#load-error-handling
|
||||
window.addEventListener("vite:preloadError", () => {
|
||||
window.location.reload(); // for example, refresh the page
|
||||
});
|
||||
// for passing in lng and translations on init
|
||||
|
||||
// Create a new router instance
|
||||
NProgress.configure({ showSpinner: false });
|
||||
|
@ -1,260 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
import {
|
||||
faArrowUpRightFromSquare,
|
||||
faBookOpen,
|
||||
faEdit,
|
||||
faEllipsisV,
|
||||
faInfoCircle,
|
||||
faMagnifyingGlass,
|
||||
faPlug,
|
||||
faSearch,
|
||||
faTrash
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { format, formatRelative } from "date-fns";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
PageHeader,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways";
|
||||
|
||||
import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal";
|
||||
|
||||
export const GatewayListPage = withPermission(
|
||||
() => {
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"deleteGateway",
|
||||
"editDetails"
|
||||
] as const);
|
||||
|
||||
const deleteGatewayById = useDeleteGatewayById();
|
||||
|
||||
const handleDeleteGateway = async () => {
|
||||
await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, {
|
||||
onSuccess: () => {
|
||||
handlePopUpToggle("deleteGateway");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully delete gateway"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const filteredGateway = gateways?.filter((el) =>
|
||||
el.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-bunker-800">
|
||||
<Helmet>
|
||||
<title>Infisical | Gateways</title>
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Helmet>
|
||||
<div className="flex w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl">
|
||||
<PageHeader
|
||||
className="w-full"
|
||||
title={
|
||||
<div className="flex w-full items-center">
|
||||
<span>Gateways</span>
|
||||
<a
|
||||
className="-mt-1.5"
|
||||
href="https://infisical.com/docs/documentation/platform/gateways/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div className="ml-2 inline-block rounded-md bg-yellow/20 px-1.5 text-sm font-normal text-yellow opacity-80 hover:opacity-100">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
|
||||
<span>Docs</span>
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowUpRightFromSquare}
|
||||
className="mb-[0.07rem] ml-1.5 text-[10px]"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
description="Create and configure gateway to access private network resources from Infisical"
|
||||
/>
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search gateway..."
|
||||
className="flex-1"
|
||||
/>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Name</Th>
|
||||
<Th>Cert Issued At</Th>
|
||||
<Th>Projects</Th>
|
||||
<Th>Identity</Th>
|
||||
<Th>
|
||||
Health Check
|
||||
<Tooltip
|
||||
asChild={false}
|
||||
className="normal-case"
|
||||
content="The last known healthcheck. Triggers every 1 hour."
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-2" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isGatewayLoading && (
|
||||
<TableSkeleton innerKey="gateway-table" columns={4} key="gateway-table" />
|
||||
)}
|
||||
{filteredGateway?.map((el) => (
|
||||
<Tr key={el.id}>
|
||||
<Td>{el.name}</Td>
|
||||
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
|
||||
<Td>
|
||||
{el.projects.map((projectDetails) => (
|
||||
<Tag key={projectDetails.id} size="xs">
|
||||
{projectDetails.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Td>
|
||||
<Td>{el.identity.name}</Td>
|
||||
<Td>
|
||||
{el.heartbeat
|
||||
? formatRelative(new Date(el.heartbeat), new Date())
|
||||
: "-"}
|
||||
</Td>
|
||||
<Td className="w-5">
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="Options"
|
||||
colorSchema="secondary"
|
||||
className="w-6"
|
||||
variant="plain"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<OrgPermissionCan
|
||||
I={OrgGatewayPermissionActions.EditGateways}
|
||||
a={OrgPermissionSubjects.Gateway}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||
onClick={() => handlePopUpOpen("editDetails", el)}
|
||||
>
|
||||
Edit Details
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionAppConnectionActions.Delete}
|
||||
a={OrgPermissionSubjects.AppConnections}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<DropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||
className="text-red"
|
||||
onClick={() => handlePopUpOpen("deleteGateway", el)}
|
||||
>
|
||||
Delete Gateway
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
<Modal
|
||||
isOpen={popUp.editDetails.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
|
||||
>
|
||||
<ModalContent title="Edit Gateway">
|
||||
<EditGatewayDetailsModal
|
||||
gatewayDetails={popUp.editDetails.data}
|
||||
onClose={() => handlePopUpToggle("editDetails")}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{!isGatewayLoading && !filteredGateway?.length && (
|
||||
<EmptyState
|
||||
title={
|
||||
gateways?.length
|
||||
? "No Gateways match search..."
|
||||
: "No Gateways have been configured"
|
||||
}
|
||||
icon={gateways?.length ? faSearch : faPlug}
|
||||
/>
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteGateway.isOpen}
|
||||
title={`Are you sure want to delete gateway ${
|
||||
(popUp?.deleteGateway?.data as { name: string })?.name || ""
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
|
||||
deleteKey="confirm"
|
||||
onDeleteApproved={() => handleDeleteGateway()}
|
||||
/>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{
|
||||
action: OrgPermissionAppConnectionActions.Read,
|
||||
subject: OrgPermissionSubjects.AppConnections
|
||||
}
|
||||
);
|
@ -1,113 +0,0 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FilterableSelect, FormControl, Input } from "@app/components/v2";
|
||||
import { useGetUserWorkspaces, useUpdateGatewayById } from "@app/hooks/api";
|
||||
import { TGateway } from "@app/hooks/api/gateways/types";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
|
||||
type Props = {
|
||||
gatewayDetails: TGateway;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
projects: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
name: gatewayDetails?.name
|
||||
}
|
||||
});
|
||||
|
||||
const updateGatewayById = useUpdateGatewayById();
|
||||
// when gateway goes to other products switch to all
|
||||
const { data: secretManagerWorkspaces, isLoading: isSecretManagerLoading } = useGetUserWorkspaces(
|
||||
{
|
||||
type: ProjectType.SecretManager
|
||||
}
|
||||
);
|
||||
|
||||
const onFormSubmit = ({ name, projects }: FormData) => {
|
||||
if (isSubmitting) return;
|
||||
updateGatewayById.mutate(
|
||||
{
|
||||
id: gatewayDetails.id,
|
||||
name,
|
||||
projectIds: projects.map((el) => el.id)
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated gateway"
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message} isRequired>
|
||||
<Input {...field} placeholder="db-subnet-1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="projects"
|
||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
className="w-full"
|
||||
label="Projects"
|
||||
tooltipText="Select the project(s) that you'd like to add this gateway to"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<FilterableSelect
|
||||
options={secretManagerWorkspaces}
|
||||
placeholder="Select projects..."
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti
|
||||
isLoading={isSecretManagerLoading}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-4 flex items-center">
|
||||
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
|
||||
Update
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
|
||||
import { GatewayListPage } from "./GatewayListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
)({
|
||||
component: GatewayListPage,
|
||||
context: () => ({
|
||||
breadcrumbs: [
|
||||
{
|
||||
label: "Home",
|
||||
icon: () => <FontAwesomeIcon icon={faHome} />,
|
||||
link: linkOptions({ to: "/organization/secret-manager/overview" })
|
||||
},
|
||||
{
|
||||
label: "Gateways"
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
@ -3,7 +3,6 @@ import { z } from "zod";
|
||||
|
||||
import { OrgPermissionSubjects } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionAppConnectionActions,
|
||||
OrgPermissionKmipActions
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
@ -35,15 +34,6 @@ const kmipPermissionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
const orgGatewayPermissionSchema = z
|
||||
.object({
|
||||
[OrgGatewayPermissionActions.ListGateways]: z.boolean().optional(),
|
||||
[OrgGatewayPermissionActions.EditGateways]: z.boolean().optional(),
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: z.boolean().optional(),
|
||||
[OrgGatewayPermissionActions.CreateGateways]: z.boolean().optional()
|
||||
})
|
||||
.optional();
|
||||
|
||||
const adminConsolePermissionSchmea = z
|
||||
.object({
|
||||
"access-all-projects": z.boolean().optional()
|
||||
@ -82,8 +72,7 @@ export const formSchema = z.object({
|
||||
[OrgPermissionSubjects.Kms]: generalPermissionSchema,
|
||||
[OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema,
|
||||
"app-connections": appConnectionsPermissionSchema,
|
||||
kmip: kmipPermissionSchema,
|
||||
gateway: orgGatewayPermissionSchema
|
||||
kmip: kmipPermissionSchema
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
@ -1,182 +0,0 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||
|
||||
type Props = {
|
||||
isEditable: boolean;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
};
|
||||
|
||||
enum Permission {
|
||||
NoAccess = "no-access",
|
||||
ReadOnly = "read-only",
|
||||
FullAccess = "full-access",
|
||||
Custom = "custom"
|
||||
}
|
||||
|
||||
const PERMISSION_ACTIONS = [
|
||||
{ action: OrgGatewayPermissionActions.ListGateways, label: "List Gateways" },
|
||||
{ action: OrgGatewayPermissionActions.CreateGateways, label: "Create Gateways" },
|
||||
{ action: OrgGatewayPermissionActions.EditGateways, label: "Edit Gateways" },
|
||||
{ action: OrgGatewayPermissionActions.DeleteGateways, label: "Delete Gateways" }
|
||||
] as const;
|
||||
|
||||
export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props) => {
|
||||
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||
const [isCustom, setIsCustom] = useToggle();
|
||||
|
||||
const rule = useWatch({
|
||||
control,
|
||||
name: "permissions.gateway"
|
||||
});
|
||||
|
||||
const selectedPermissionCategory = useMemo(() => {
|
||||
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||
const totalActions = PERMISSION_ACTIONS.length;
|
||||
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||
|
||||
if (isCustom) return Permission.Custom;
|
||||
if (score === 0) return Permission.NoAccess;
|
||||
if (score === totalActions) return Permission.FullAccess;
|
||||
if (score === 1 && rule?.[OrgGatewayPermissionActions.ListGateways]) return Permission.ReadOnly;
|
||||
|
||||
return Permission.Custom;
|
||||
}, [rule, isCustom]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||
else setIsCustom.off();
|
||||
}, [selectedPermissionCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||
if (isRowCustom) {
|
||||
setIsRowExpanded.on();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePermissionChange = (val: Permission) => {
|
||||
if (!val) return;
|
||||
if (val === Permission.Custom) {
|
||||
setIsRowExpanded.on();
|
||||
setIsCustom.on();
|
||||
return;
|
||||
}
|
||||
setIsCustom.off();
|
||||
|
||||
switch (val) {
|
||||
case Permission.FullAccess:
|
||||
setValue(
|
||||
"permissions.gateway",
|
||||
{
|
||||
[OrgGatewayPermissionActions.ListGateways]: true,
|
||||
[OrgGatewayPermissionActions.EditGateways]: true,
|
||||
[OrgGatewayPermissionActions.CreateGateways]: true,
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: true
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
case Permission.ReadOnly:
|
||||
setValue(
|
||||
"permissions.gateway",
|
||||
{
|
||||
[OrgGatewayPermissionActions.ListGateways]: true,
|
||||
[OrgGatewayPermissionActions.EditGateways]: false,
|
||||
[OrgGatewayPermissionActions.CreateGateways]: false,
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
break;
|
||||
|
||||
case Permission.NoAccess:
|
||||
default:
|
||||
setValue(
|
||||
"permissions.gateway",
|
||||
{
|
||||
[OrgGatewayPermissionActions.ListGateways]: false,
|
||||
[OrgGatewayPermissionActions.EditGateways]: false,
|
||||
[OrgGatewayPermissionActions.CreateGateways]: false,
|
||||
[OrgGatewayPermissionActions.DeleteGateways]: false
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
onClick={() => setIsRowExpanded.toggle()}
|
||||
>
|
||||
<Td>
|
||||
<FontAwesomeIcon icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||
</Td>
|
||||
<Td>Gateways</Td>
|
||||
<Td>
|
||||
<Select
|
||||
value={selectedPermissionCategory}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={handlePermissionChange}
|
||||
isDisabled={!isEditable}
|
||||
>
|
||||
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
|
||||
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
|
||||
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||
</Select>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isRowExpanded && (
|
||||
<Tr>
|
||||
<Td
|
||||
colSpan={3}
|
||||
className={`bg-bunker-600 px-0 py-0 ${isRowExpanded && "border-mineshaft-500 p-8"}`}
|
||||
>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||
return (
|
||||
<Controller
|
||||
name={`permissions.gateway.${action}`}
|
||||
key={`permissions.gateway.${action}`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
isChecked={Boolean(field.value)}
|
||||
onCheckedChange={(e) => {
|
||||
if (!isEditable) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to update default role"
|
||||
});
|
||||
return;
|
||||
}
|
||||
field.onChange(e);
|
||||
}}
|
||||
id={`permissions.gateways.${action}`}
|
||||
>
|
||||
{label}
|
||||
</Checkbox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
2
frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx
2
frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionRow.tsx
@ -74,7 +74,7 @@ type Props = {
|
||||
title: string;
|
||||
formName: keyof Omit<
|
||||
Exclude<TFormSchema["permissions"], undefined>,
|
||||
"workspace" | "organization-admin-console" | "kmip" | "gateway"
|
||||
"workspace" | "organization-admin-console" | "kmip"
|
||||
>;
|
||||
setValue: UseFormSetValue<TFormSchema>;
|
||||
control: Control<TFormSchema>;
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
TFormSchema
|
||||
} from "../OrgRoleModifySection.utils";
|
||||
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
||||
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
|
||||
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
|
||||
import { RolePermissionRow } from "./RolePermissionRow";
|
||||
@ -172,11 +171,6 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgGatewayPermissionRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
isEditable={isCustomRole}
|
||||
/>
|
||||
<OrgRoleWorkspaceRow
|
||||
control={control}
|
||||
setValue={setValue}
|
||||
|
@ -120,7 +120,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="API Key" type="text" autoComplete="off" autoCorrect="off" spellCheck="false" />
|
||||
<Input {...field} placeholder="API Key" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
@ -155,7 +155,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
|
||||
errorText={error?.message}
|
||||
isOptional
|
||||
>
|
||||
<Input {...field} placeholder="Password" type="password" autoComplete="new-password" autoCorrect="off" spellCheck="false" aria-autocomplete="none" data-form-type="other" />
|
||||
<Input {...field} placeholder="Password" type="password" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -19,8 +18,7 @@ import {
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
@ -34,8 +32,7 @@ const formSchema = z.object({
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().optional()
|
||||
ca: z.string().optional()
|
||||
}),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
const valMs = ms(val);
|
||||
@ -127,8 +124,6 @@ export const SqlDatabaseInputForm = ({
|
||||
secretPath,
|
||||
projectSlug
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
@ -142,9 +137,6 @@ export const SqlDatabaseInputForm = ({
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||
);
|
||||
|
||||
const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
@ -230,42 +222,6 @@ export const SqlDatabaseInputForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="provider.projectGatewayId"
|
||||
defaultValue=""
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
label="Gateway"
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isProjectGatewaysLoading}
|
||||
placeholder="Internet gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem
|
||||
value={null as unknown as string}
|
||||
onClick={() => onChange(undefined)}
|
||||
>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{projectGateways?.map((el) => (
|
||||
<SelectItem value={el.projectGatewayId} key={el.projectGatewayId}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -19,8 +18,7 @@ import {
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { useUpdateDynamicSecret } from "@app/hooks/api";
|
||||
import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
|
||||
|
||||
const formSchema = z.object({
|
||||
@ -35,8 +33,7 @@ const formSchema = z.object({
|
||||
creationStatement: z.string().min(1),
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().optional()
|
||||
ca: z.string().optional()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
@ -84,7 +81,6 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
@ -98,15 +94,8 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
}
|
||||
}
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery(
|
||||
gatewaysQueryKeys.listProjectGateways({ projectId: currentWorkspace.id })
|
||||
);
|
||||
|
||||
const updateDynamicSecret = useUpdateDynamicSecret();
|
||||
const selectedProjectGatewayId = watch("inputs.projectGatewayId");
|
||||
const isGatewayInActive =
|
||||
projectGateways?.findIndex((el) => el.projectGatewayId === selectedProjectGatewayId) === -1;
|
||||
|
||||
const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => {
|
||||
// wait till previous request is finished
|
||||
@ -120,10 +109,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
data: {
|
||||
maxTTL: maxTTL || undefined,
|
||||
defaultTTL,
|
||||
inputs: {
|
||||
...inputs,
|
||||
projectGatewayId: isGatewayInActive ? null : inputs.projectGatewayId
|
||||
},
|
||||
inputs,
|
||||
newName: newName === dynamicSecret.name ? undefined : newName
|
||||
}
|
||||
});
|
||||
@ -190,44 +176,6 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="inputs.projectGatewayId"
|
||||
defaultValue=""
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message) || isGatewayInActive}
|
||||
errorText={
|
||||
isGatewayInActive && selectedProjectGatewayId
|
||||
? `Project Gateway ${selectedProjectGatewayId} is removed`
|
||||
: error?.message
|
||||
}
|
||||
label="Gateway"
|
||||
helperText=""
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
isLoading={isProjectGatewaysLoading}
|
||||
placeholder="Internet Gateway"
|
||||
position="popper"
|
||||
>
|
||||
<SelectItem value={null as unknown as string} onClick={() => onChange(undefined)}>
|
||||
Internet Gateway
|
||||
</SelectItem>
|
||||
{projectGateways?.map((el) => (
|
||||
<SelectItem value={el.projectGatewayId} key={el.id}>
|
||||
{el.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
|
||||
<div className="flex flex-col">
|
||||
|
@ -59,12 +59,7 @@ export const PitDrawer = ({
|
||||
onClick={() => onSelectSnapshot(id)}
|
||||
>
|
||||
<div className="flex w-full justify-between">
|
||||
<div>
|
||||
{(() => {
|
||||
const distance = formatDistance(new Date(createdAt), new Date());
|
||||
return distance.charAt(0).toUpperCase() + distance.slice(1) + " ago";
|
||||
})()}
|
||||
</div>
|
||||
<div>{formatDistance(new Date(createdAt), new Date())}</div>
|
||||
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
|
||||
</div>
|
||||
</Button>
|
||||
@ -75,7 +70,7 @@ export const PitDrawer = ({
|
||||
<Button
|
||||
className="mt-8 px-4 py-3 text-sm"
|
||||
isFullWidth
|
||||
variant="outline_bg"
|
||||
variant="star"
|
||||
isLoading={isFetchingNextPage}
|
||||
isDisabled={isFetchingNextPage || !hasNextPage}
|
||||
onClick={fetchNextPage}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faArrowRotateRight,
|
||||
faCheckCircle,
|
||||
faCircle,
|
||||
faCircleDot,
|
||||
faClock,
|
||||
faEyeSlash,
|
||||
faPlus,
|
||||
faShare,
|
||||
faTag,
|
||||
@ -236,102 +236,44 @@ export const SecretDetailSidebar = ({
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<DrawerContent title={`Secret – ${secret?.key}`} className="thin-scrollbar">
|
||||
<DrawerContent title="Secret">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
key="secret-value"
|
||||
isDisabled={isOverridden || !isAllowed}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<div className="ml-1 mt-1.5 flex items-center">
|
||||
<Button
|
||||
className="w-full px-2 py-[0.43rem] font-normal"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||
onClick={() => {
|
||||
const value = secret?.valueOverride ?? secret?.value;
|
||||
if (value) {
|
||||
handleSecretShare(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
|
||||
<div className="mb-4 px-4">
|
||||
<FormControl label="Key">
|
||||
<Input isDisabled {...register("key")} />
|
||||
</FormControl>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
name="skipMultilineEncoding"
|
||||
render={({ field: { value, onChange, onBlur } }) => (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="w-max text-sm text-mineshaft-300">
|
||||
Multi-line encoding
|
||||
<Tooltip
|
||||
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<Switch
|
||||
id="skipmultiencoding-option"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
onBlur={onBlur}
|
||||
isDisabled={!isAllowed}
|
||||
className="items-center justify-between"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
key="secret-value"
|
||||
isDisabled={isOverridden || !isAllowed}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
autoFocus={false}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={`mb-4 w-full border-t border-mineshaft-600 ${isOverridden ? "block" : "hidden"}`}
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
@ -342,243 +284,194 @@ export const SecretDetailSidebar = ({
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="flex items-center justify-between px-4 pb-4">
|
||||
<span className="w-max text-sm text-mineshaft-300">
|
||||
Override with a personal value
|
||||
<Tooltip
|
||||
content="Override the secret value with a personal value that does not get shared with other users and machines."
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<Switch
|
||||
isDisabled={!isAllowed}
|
||||
id="personal-override"
|
||||
onCheckedChange={handleOverrideClick}
|
||||
isChecked={isOverridden}
|
||||
className="justify-start"
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
isDisabled={!isAllowed}
|
||||
id="personal-override"
|
||||
onCheckedChange={handleOverrideClick}
|
||||
isChecked={isOverridden}
|
||||
>
|
||||
Override with a personal value
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{isOverridden && (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Override Value" className="px-4">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-4 mt-2 flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
|
||||
<div
|
||||
className={`flex justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
|
||||
>
|
||||
<div
|
||||
className={`text-sm text-mineshaft-300 ${tagFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
|
||||
>
|
||||
Tags
|
||||
</div>
|
||||
<div>
|
||||
<FormControl>
|
||||
<div
|
||||
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
|
||||
>
|
||||
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
onClose={() => {
|
||||
if (cannotEditSecret) {
|
||||
createNotification({ type: "error", text: "Access denied" });
|
||||
return;
|
||||
}
|
||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||
if (tag) handleTagSelect(tag);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="add"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="rounded-md"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="start" side="right" className="z-[100]">
|
||||
<DropdownMenuLabel className="pl-2">
|
||||
Add tags to this secret
|
||||
</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={tagId}
|
||||
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="p-2">
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
onClick={onCreateTag}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{isOverridden && (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value Override">
|
||||
<InfisicalSecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}
|
||||
)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={`flex justify-between px-4 text-mineshaft-100 ${metadataFormFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
|
||||
>
|
||||
<div
|
||||
className={`text-sm text-mineshaft-300 ${metadataFormFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
|
||||
>
|
||||
Metadata
|
||||
</div>
|
||||
<FormControl>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secretMetadata.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} className="max-h-8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel
|
||||
label="Value"
|
||||
className="text-xs text-mineshaft-400"
|
||||
isOptional
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secretMetadata.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} className="max-h-8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 max-h-8"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
|
||||
<IconButton
|
||||
ariaLabel="Add Key"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="rounded-md"
|
||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
<FormControl label="Metadata">
|
||||
<div className="flex flex-col space-y-2">
|
||||
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||
<div className="flex-grow">
|
||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secretMetadata.${i}.key`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} className="max-h-8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
{i === 0 && (
|
||||
<FormLabel
|
||||
label="Value"
|
||||
className="text-xs text-mineshaft-400"
|
||||
isOptional
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secretMetadata.${i}.value`}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error?.message)}
|
||||
errorText={error?.message}
|
||||
className="mb-0"
|
||||
>
|
||||
<Input {...field} className="max-h-8" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<IconButton
|
||||
ariaLabel="delete key"
|
||||
className="bottom-0.5 max-h-8"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.remove(i)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</FormControl>
|
||||
))}
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
variant="outline_bg"
|
||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
||||
>
|
||||
Add Key
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl label="Comments & Notes">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 bg-bunker-800 text-sm"
|
||||
{...register("comment")}
|
||||
readOnly={isReadOnly}
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<FormControl label="Tags" className="">
|
||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
||||
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
onClose={() => {
|
||||
if (cannotEditSecret) {
|
||||
createNotification({ type: "error", text: "Access denied" });
|
||||
return;
|
||||
}
|
||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||
if (tag) handleTagSelect(tag);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{slug}</div>
|
||||
</Tag>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild>
|
||||
<IconButton
|
||||
ariaLabel="add"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
className="rounded-md"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end" className="z-[100]">
|
||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { id: tagId, slug, color } = tag;
|
||||
|
||||
const isSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={tagId}
|
||||
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="mr-2 h-2 w-2 rounded-full"
|
||||
style={{ background: color || "#bec2c8" }}
|
||||
/>
|
||||
{slug}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem asChild>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
colorSchema="primary"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
onClick={onCreateTag}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormControl label="Reminder">
|
||||
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="ml-1 mt-2 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
|
||||
<span className="text-sm text-bunker-300">
|
||||
@ -597,9 +490,9 @@ export const SecretDetailSidebar = ({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="ml-1 flex items-center space-x-2">
|
||||
<div className="ml-1 mt-2 flex items-center space-x-2">
|
||||
<Button
|
||||
className="w-full px-2 py-2 font-normal"
|
||||
className="w-full px-2 py-1"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||
onClick={() => setCreateReminderFormOpen.on()}
|
||||
@ -610,147 +503,92 @@ export const SecretDetailSidebar = ({
|
||||
</div>
|
||||
)}
|
||||
</FormControl>
|
||||
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
|
||||
<div className="mb-2 pl-1">Version History</div>
|
||||
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, secretValue, version, id }) => (
|
||||
<div className="flex flex-row">
|
||||
<div key={id} className="flex w-full flex-col space-y-1">
|
||||
<div className="flex items-center">
|
||||
<div className="w-10">
|
||||
<div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
|
||||
v{version}
|
||||
</div>
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="flex w-full cursor-default">
|
||||
<div className="relative w-10">
|
||||
<div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
|
||||
</div>
|
||||
<div className="flex flex-row">
|
||||
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
|
||||
Value:
|
||||
</div>
|
||||
<div className="group break-all pl-1 font-mono">
|
||||
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
|
||||
<button
|
||||
type="button"
|
||||
className="select-none"
|
||||
onClick={(e) => {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
navigator.clipboard.writeText(secretValue || "");
|
||||
const target = e.currentTarget;
|
||||
target.style.borderBottom = "1px dashed";
|
||||
target.style.paddingBottom = "-1px";
|
||||
|
||||
// Create and insert popup
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
|
||||
popup.textContent = "Copied!";
|
||||
target.parentElement?.appendChild(popup);
|
||||
|
||||
// Remove popup and border after delay
|
||||
setTimeout(() => {
|
||||
popup.remove();
|
||||
target.style.borderBottom = "none";
|
||||
}, 3000);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{secretValue}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.stopPropagation();
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.remove("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEyeSlash} />
|
||||
</button>
|
||||
</div>
|
||||
<span className="group-[.show-value]:hidden">
|
||||
{secretValue?.replace(/./g, "*")}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.currentTarget.closest(".group")?.classList.add("show-value");
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.currentTarget
|
||||
.closest(".group")
|
||||
?.classList.add("show-value");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
|
||||
>
|
||||
<Tooltip content="Restore Secret Value">
|
||||
<IconButton
|
||||
ariaLabel="Restore"
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
className="h-8 w-8 rounded-md"
|
||||
onClick={() => setValue("value", secretValue)}
|
||||
<FormControl label="Comments & Notes">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register("comment")}
|
||||
readOnly={isReadOnly}
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="my-2 mb-4 border-b border-mineshaft-600 pb-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="skipMultilineEncoding"
|
||||
render={({ field: { value, onChange, onBlur } }) => (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="skipmultiencoding-option"
|
||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||
isChecked={value}
|
||||
onBlur={onBlur}
|
||||
isDisabled={!isAllowed}
|
||||
className="items-center"
|
||||
>
|
||||
Multi line encoding
|
||||
<Tooltip
|
||||
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowRotateRight} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||
</Tooltip>
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-1 flex items-center space-x-4">
|
||||
<Button
|
||||
className="w-full px-2 py-1"
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
||||
onClick={() => {
|
||||
const value = secret?.valueOverride ?? secret?.value;
|
||||
if (value) {
|
||||
handleSecretShare(value);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Share Secret
|
||||
</Button>
|
||||
</div>
|
||||
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
|
||||
<div className="mb-2">Version History</div>
|
||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
|
||||
<div key={id} className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||
<div className="break-all font-mono">{secretValue}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
|
||||
<div className="mb-2 mt-4">
|
||||
<div className="mb-2">
|
||||
Access List
|
||||
<Tooltip
|
||||
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
|
||||
className="z-[100]"
|
||||
>
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
|
||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{isPending && (
|
||||
@ -770,22 +608,14 @@ export const SecretDetailSidebar = ({
|
||||
</Button>
|
||||
)}
|
||||
{!isPending && secretAccessList && (
|
||||
<div className="mb-4 flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
|
||||
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
{secretAccessList.users.length > 0 && (
|
||||
<div className="pb-3">
|
||||
<div className="mb-2 font-bold">Users</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{secretAccessList.users.map((user) => (
|
||||
<div className="rounded-md bg-bunker-500">
|
||||
<Tooltip
|
||||
content={user.allowedActions
|
||||
.map(
|
||||
(action) =>
|
||||
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
|
||||
)
|
||||
.join(", ")}
|
||||
className="z-[100]"
|
||||
>
|
||||
<div className="rounded-md bg-bunker-500 px-1">
|
||||
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]">
|
||||
<Link
|
||||
to={
|
||||
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
|
||||
@ -794,7 +624,7 @@ export const SecretDetailSidebar = ({
|
||||
projectId: currentWorkspace.id,
|
||||
membershipId: user.membershipId
|
||||
}}
|
||||
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
|
||||
className="text-secondary/80 text-sm hover:text-primary"
|
||||
>
|
||||
{user.name}
|
||||
</Link>
|
||||
@ -809,14 +639,9 @@ export const SecretDetailSidebar = ({
|
||||
<div className="mb-2 font-bold">Identities</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{secretAccessList.identities.map((identity) => (
|
||||
<div className="rounded-md bg-bunker-500">
|
||||
<div className="rounded-md bg-bunker-500 px-1">
|
||||
<Tooltip
|
||||
content={identity.allowedActions
|
||||
.map(
|
||||
(action) =>
|
||||
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
|
||||
)
|
||||
.join(", ")}
|
||||
content={identity.allowedActions.join(", ")}
|
||||
className="z-[100]"
|
||||
>
|
||||
<Link
|
||||
@ -827,7 +652,7 @@ export const SecretDetailSidebar = ({
|
||||
projectId: currentWorkspace.id,
|
||||
identityId: identity.id
|
||||
}}
|
||||
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
|
||||
className="text-secondary/80 text-sm hover:text-primary"
|
||||
>
|
||||
{identity.name}
|
||||
</Link>
|
||||
@ -842,14 +667,9 @@ export const SecretDetailSidebar = ({
|
||||
<div className="mb-2 font-bold">Groups</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{secretAccessList.groups.map((group) => (
|
||||
<div className="rounded-md bg-bunker-500">
|
||||
<div className="rounded-md bg-bunker-500 px-1">
|
||||
<Tooltip
|
||||
content={group.allowedActions
|
||||
.map(
|
||||
(action) =>
|
||||
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
|
||||
)
|
||||
.join(", ")}
|
||||
content={group.allowedActions.join(", ")}
|
||||
className="z-[100]"
|
||||
>
|
||||
<Link
|
||||
@ -857,7 +677,7 @@ export const SecretDetailSidebar = ({
|
||||
params={{
|
||||
groupId: group.id
|
||||
}}
|
||||
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
|
||||
className="text-secondary/80 text-sm hover:text-primary"
|
||||
>
|
||||
{group.name}
|
||||
</Link>
|
||||
@ -871,7 +691,7 @@ export const SecretDetailSidebar = ({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<div className="mb-2 flex items-center space-x-4">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
@ -885,7 +705,6 @@ export const SecretDetailSidebar = ({
|
||||
<Button
|
||||
isFullWidth
|
||||
type="submit"
|
||||
variant="outline_bg"
|
||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
@ -903,17 +722,9 @@ export const SecretDetailSidebar = ({
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="Delete Secret"
|
||||
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={onDeleteSecret}
|
||||
>
|
||||
<Tooltip content="Delete Secret">
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
|
@ -45,33 +45,17 @@ const formSchema = z
|
||||
.object({
|
||||
secretPath: z.string().default("/"),
|
||||
sourceEnvironment: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetRepo: z.object({ name: z.string(), appId: z.string().optional() }).nullish(),
|
||||
targetRepo: z.object({ name: z.string(), appId: z.string() }),
|
||||
targetWorkspace: z.object({ name: z.string(), slug: z.string() }),
|
||||
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).nullish(),
|
||||
targetEnvironment: z.object({ name: z.string(), uuid: z.string() }).optional(),
|
||||
scope: z.object({ label: z.string(), value: z.nativeEnum(BitbucketScope) })
|
||||
})
|
||||
.superRefine((val, ctx) => {
|
||||
if (!val.targetWorkspace) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["targetWorkspace"],
|
||||
message: "Bitbucket Workspace required"
|
||||
});
|
||||
}
|
||||
|
||||
if (!val.targetRepo) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["targetRepo"],
|
||||
message: "Bitbucket Repo required"
|
||||
});
|
||||
}
|
||||
|
||||
if (val.scope.value === BitbucketScope.Env && !val.targetEnvironment) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["targetEnvironment"],
|
||||
message: "Bitbucket Deployment Environment required"
|
||||
message: "Required"
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -82,7 +66,7 @@ export const BitbucketConfigurePage = () => {
|
||||
const navigate = useNavigate();
|
||||
const createIntegration = useCreateIntegration();
|
||||
|
||||
const { watch, control, handleSubmit, setValue, reset } = useForm<TFormData>({
|
||||
const { watch, control, reset, handleSubmit } = useForm<TFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
secretPath: "/"
|
||||
@ -106,15 +90,14 @@ export const BitbucketConfigurePage = () => {
|
||||
workspaceSlug: bitBucketWorkspace?.slug
|
||||
});
|
||||
|
||||
const { data: bitbucketEnvironments, isPending: isBitbucketEnvironmentsLoading } =
|
||||
useGetIntegrationAuthBitBucketEnvironments(
|
||||
{
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
workspaceSlug: bitBucketWorkspace?.slug ?? "",
|
||||
repoSlug: bitBucketRepo?.appId ?? ""
|
||||
},
|
||||
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
|
||||
);
|
||||
const { data: bitbucketEnvironments } = useGetIntegrationAuthBitBucketEnvironments(
|
||||
{
|
||||
integrationAuthId: (integrationAuthId as string) ?? "",
|
||||
workspaceSlug: bitBucketWorkspace?.slug,
|
||||
repoSlug: bitBucketRepo?.appId
|
||||
},
|
||||
{ enabled: Boolean(bitBucketWorkspace?.slug && bitBucketRepo?.appId) }
|
||||
);
|
||||
|
||||
const onSubmit = async ({
|
||||
targetRepo,
|
||||
@ -124,8 +107,6 @@ export const BitbucketConfigurePage = () => {
|
||||
targetEnvironment,
|
||||
scope
|
||||
}: TFormData) => {
|
||||
if (!targetRepo || !targetWorkspace) return;
|
||||
|
||||
try {
|
||||
await createIntegration.mutateAsync({
|
||||
integrationAuthId,
|
||||
@ -166,14 +147,7 @@ export const BitbucketConfigurePage = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
bitBucketWorkspace ||
|
||||
bitBucketRepo ||
|
||||
!bitbucketRepos ||
|
||||
!bitbucketWorkspaces ||
|
||||
!currentWorkspace
|
||||
)
|
||||
return;
|
||||
if (!bitbucketRepos || !bitbucketWorkspaces || !currentWorkspace) return;
|
||||
|
||||
reset({
|
||||
targetRepo: bitbucketRepos[0],
|
||||
@ -250,17 +224,12 @@ export const BitbucketConfigurePage = () => {
|
||||
getOptionValue={(option) => option.slug}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(option) => {
|
||||
onChange(option);
|
||||
setValue("targetRepo", null);
|
||||
setValue("targetEnvironment", null);
|
||||
}}
|
||||
onChange={onChange}
|
||||
options={bitbucketWorkspaces}
|
||||
placeholder={
|
||||
bitbucketWorkspaces?.length ? "Select a workspace..." : "No workspaces found..."
|
||||
}
|
||||
isLoading={isBitbucketWorkspacesLoading}
|
||||
isDisabled={!bitbucketWorkspaces?.length || isBitbucketWorkspacesLoading}
|
||||
isDisabled={!bitbucketWorkspaces?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
@ -274,16 +243,12 @@ export const BitbucketConfigurePage = () => {
|
||||
getOptionValue={(option) => option.appId!}
|
||||
value={value}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(option) => {
|
||||
onChange(option);
|
||||
setValue("targetEnvironment", null);
|
||||
}}
|
||||
onChange={onChange}
|
||||
options={bitbucketRepos}
|
||||
placeholder={
|
||||
bitbucketRepos?.length ? "Select a repository..." : "No repositories found..."
|
||||
}
|
||||
isLoading={isBitbucketReposLoading}
|
||||
isDisabled={!bitbucketRepos?.length || isBitbucketReposLoading}
|
||||
isDisabled={!bitbucketRepos?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
@ -325,8 +290,7 @@ export const BitbucketConfigurePage = () => {
|
||||
? "Select an environment..."
|
||||
: "No environments found..."
|
||||
}
|
||||
isLoading={isBitbucketEnvironmentsLoading && Boolean(bitBucketRepo)}
|
||||
isDisabled={!bitbucketEnvironments?.length || isBitbucketEnvironmentsLoading}
|
||||
isDisabled={!bitbucketEnvironments?.length}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
@ -60,7 +60,6 @@ import { Route as organizationKmsOverviewPageRouteImport } from './pages/organiz
|
||||
import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages/organization/IdentityDetailsByIDPage/route'
|
||||
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
|
||||
import { Route as organizationCertManagerOverviewPageRouteImport } from './pages/organization/CertManagerOverviewPage/route'
|
||||
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
|
||||
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
|
||||
import { Route as projectAccessControlPageRouteSshImport } from './pages/project/AccessControlPage/route-ssh'
|
||||
import { Route as projectAccessControlPageRouteSecretManagerImport } from './pages/project/AccessControlPage/route-secret-manager'
|
||||
@ -210,10 +209,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways',
|
||||
)()
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport =
|
||||
createFileRoute(
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections',
|
||||
@ -459,14 +454,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRoute =
|
||||
getParentRoute: () => organizationLayoutRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport.update({
|
||||
id: '/gateways',
|
||||
path: '/gateways',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport.update({
|
||||
id: '/app-connections',
|
||||
@ -638,14 +625,6 @@ const organizationCertManagerOverviewPageRouteRoute =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
|
||||
} as any)
|
||||
|
||||
const organizationGatewaysGatewayListPageRouteRoute =
|
||||
organizationGatewaysGatewayListPageRouteImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () =>
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute,
|
||||
} as any)
|
||||
|
||||
const organizationAppConnectionsAppConnectionsPageRouteRoute =
|
||||
organizationAppConnectionsAppConnectionsPageRouteImport.update({
|
||||
id: '/',
|
||||
@ -1907,13 +1886,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
|
||||
path: '/gateways'
|
||||
fullPath: '/organization/gateways'
|
||||
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
|
||||
path: '/secret-manager/$projectId'
|
||||
@ -1935,13 +1907,6 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof organizationAppConnectionsAppConnectionsPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
|
||||
path: '/'
|
||||
fullPath: '/organization/gateways/'
|
||||
preLoaderRoute: typeof organizationGatewaysGatewayListPageRouteImport
|
||||
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
|
||||
}
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': {
|
||||
id: '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
|
||||
path: '/cert-manager/overview'
|
||||
@ -2935,21 +2900,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithCh
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren {
|
||||
organizationGatewaysGatewayListPageRouteRoute: typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren =
|
||||
{
|
||||
organizationGatewaysGatewayListPageRouteRoute:
|
||||
organizationGatewaysGatewayListPageRouteRoute,
|
||||
}
|
||||
|
||||
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren =
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute._addFileChildren(
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren,
|
||||
)
|
||||
|
||||
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationAccessManagementPageRouteRoute: typeof organizationAccessManagementPageRouteRoute
|
||||
organizationAdminPageRouteRoute: typeof organizationAdminPageRouteRoute
|
||||
@ -2959,7 +2909,6 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
|
||||
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
|
||||
organizationSettingsPageRouteRoute: typeof organizationSettingsPageRouteRoute
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
organizationCertManagerOverviewPageRouteRoute: typeof organizationCertManagerOverviewPageRouteRoute
|
||||
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
organizationIdentityDetailsByIDPageRouteRoute: typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@ -2984,8 +2933,6 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
|
||||
organizationSettingsPageRouteRoute: organizationSettingsPageRouteRoute,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
|
||||
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
|
||||
organizationCertManagerOverviewPageRouteRoute:
|
||||
organizationCertManagerOverviewPageRouteRoute,
|
||||
organizationGroupDetailsByIDPageRouteRoute:
|
||||
@ -3661,11 +3608,9 @@ export interface FileRoutesByFullPath {
|
||||
'/cert-manager/$projectId': typeof certManagerLayoutRouteWithChildren
|
||||
'/kms/$projectId': typeof kmsLayoutRouteWithChildren
|
||||
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
|
||||
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
|
||||
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
|
||||
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@ -3835,7 +3780,6 @@ export interface FileRoutesByTo {
|
||||
'/secret-manager/$projectId': typeof secretManagerLayoutRouteWithChildren
|
||||
'/ssh/$projectId': typeof sshLayoutRouteWithChildren
|
||||
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
|
||||
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
'/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@ -4012,11 +3956,9 @@ export interface FileRoutesById {
|
||||
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutCertManagerProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/kms/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutKmsProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSshProjectIdRouteWithChildren
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview': typeof organizationCertManagerOverviewPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
|
||||
'/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId': typeof organizationIdentityDetailsByIDPageRouteRoute
|
||||
@ -4194,11 +4136,9 @@ export interface FileRouteTypes {
|
||||
| '/cert-manager/$projectId'
|
||||
| '/kms/$projectId'
|
||||
| '/organization/app-connections'
|
||||
| '/organization/gateways'
|
||||
| '/secret-manager/$projectId'
|
||||
| '/ssh/$projectId'
|
||||
| '/organization/app-connections/'
|
||||
| '/organization/gateways/'
|
||||
| '/organization/cert-manager/overview'
|
||||
| '/organization/groups/$groupId'
|
||||
| '/organization/identities/$identityId'
|
||||
@ -4367,7 +4307,6 @@ export interface FileRouteTypes {
|
||||
| '/secret-manager/$projectId'
|
||||
| '/ssh/$projectId'
|
||||
| '/organization/app-connections'
|
||||
| '/organization/gateways'
|
||||
| '/organization/cert-manager/overview'
|
||||
| '/organization/groups/$groupId'
|
||||
| '/organization/identities/$identityId'
|
||||
@ -4542,11 +4481,9 @@ export interface FileRouteTypes {
|
||||
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/kms/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways/'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
|
||||
| '/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId'
|
||||
@ -4911,7 +4848,6 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/identities/$identityId",
|
||||
@ -4983,13 +4919,6 @@ export const routeTree = rootRoute
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections/$appConnection/oauth/callback"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
|
||||
"children": [
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
|
||||
]
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId": {
|
||||
"filePath": "",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout",
|
||||
@ -5008,10 +4937,6 @@ export const routeTree = rootRoute
|
||||
"filePath": "organization/AppConnections/AppConnectionsPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/app-connections"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/": {
|
||||
"filePath": "organization/Gateways/GatewayListPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/gateways"
|
||||
},
|
||||
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/overview": {
|
||||
"filePath": "organization/CertManagerOverviewPage/route.tsx",
|
||||
"parent": "/_authenticate/_inject-org-details/_org-layout/organization"
|
||||
|
@ -29,8 +29,7 @@ const organizationRoutes = route("/organization", [
|
||||
"/$appConnection/oauth/callback",
|
||||
"organization/AppConnections/OauthCallbackPage/route.tsx"
|
||||
)
|
||||
]),
|
||||
route("/gateways", [index("organization/Gateways/GatewayListPage/route.tsx")])
|
||||
])
|
||||
]);
|
||||
|
||||
const secretManagerRoutes = route("/secret-manager/$projectId", [
|
||||
|
Reference in New Issue
Block a user