From f7406ea8f89156c8963ba3318fa54c81f48e86b9 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 20 Feb 2025 23:37:19 +0530 Subject: [PATCH] feat: review feedback, and more changes in cli --- .../20250212191958_create-gateway.ts | 1 + backend/src/db/schemas/gateways.ts | 1 + backend/src/ee/routes/v1/gateway-router.ts | 25 ++- .../ee/services/gateway/gateway-service.ts | 72 ++++++- .../src/ee/services/gateway/gateway-types.ts | 4 + .../ee/services/permission/org-permission.ts | 22 ++- backend/src/keystore/keystore.ts | 2 +- backend/src/lib/gateway/index.ts | 6 +- cli/packages/api/api.go | 17 ++ cli/packages/cmd/gateway.go | 61 ++++-- cli/packages/gateway/connection.go | 4 +- cli/packages/gateway/gateway.go | 187 ++++++++++++++---- frontend/src/hooks/api/gateways/types.ts | 1 + .../GatewayListPage/GatewayListPage.tsx | 7 +- .../components/OrgRoleModifySection.utils.ts | 2 +- .../OrgPermissionGatewayRow.tsx | 6 +- 16 files changed, 336 insertions(+), 82 deletions(-) diff --git a/backend/src/db/migrations/20250212191958_create-gateway.ts b/backend/src/db/migrations/20250212191958_create-gateway.ts index 87f58bc78..b91fc116c 100644 --- a/backend/src/db/migrations/20250212191958_create-gateway.ts +++ b/backend/src/db/migrations/20250212191958_create-gateway.ts @@ -52,6 +52,7 @@ export async function up(knex: Knex): Promise { t.string("keyAlgorithm").notNullable(); t.datetime("issuedAt").notNullable(); t.datetime("expiration").notNullable(); + t.datetime("heartbeat"); t.binary("relayAddress").notNullable(); diff --git a/backend/src/db/schemas/gateways.ts b/backend/src/db/schemas/gateways.ts index 7dc29fdb5..30f5f25e0 100644 --- a/backend/src/db/schemas/gateways.ts +++ b/backend/src/db/schemas/gateways.ts @@ -16,6 +16,7 @@ export const GatewaysSchema = z.object({ keyAlgorithm: z.string(), issuedAt: z.date(), expiration: z.date(), + heartbeat: z.date().nullable().optional(), relayAddress: zodBuffer, orgGatewayRootCaId: z.string().uuid(), identityId: z.string().uuid(), diff --git a/backend/src/ee/routes/v1/gateway-router.ts b/backend/src/ee/routes/v1/gateway-router.ts index 332997619..bd2ac2739 100644 --- a/backend/src/ee/routes/v1/gateway-router.ts +++ b/backend/src/ee/routes/v1/gateway-router.ts @@ -13,7 +13,8 @@ const SanitizedGatewaySchema = GatewaysSchema.pick({ createdAt: true, updatedAt: true, issuedAt: true, - serialNumber: true + serialNumber: true, + heartbeat: true }); export const registerGatewayRouter = async (server: FastifyZodProvider) => { @@ -76,6 +77,28 @@ export const registerGatewayRouter = async (server: FastifyZodProvider) => { } }); + 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: "/", diff --git a/backend/src/ee/services/gateway/gateway-service.ts b/backend/src/ee/services/gateway/gateway-service.ts index e3d07041e..560adc444 100644 --- a/backend/src/ee/services/gateway/gateway-service.ts +++ b/backend/src/ee/services/gateway/gateway-service.ts @@ -8,6 +8,7 @@ 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"; @@ -20,7 +21,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TLicenseServiceFactory } from "../license/license-service"; -import { OrgGatewayPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; +import { OrgPermissionGatewayActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { TGatewayDALFactory } from "./gateway-dal"; @@ -28,6 +29,7 @@ import { TExchangeAllocatedRelayAddressDTO, TGetGatewayByIdDTO, TGetProjectGatewayByIdDTO, + THeartBeatDTO, TListGatewaysDTO, TUpdateGatewayByIdDTO } from "./gateway-types"; @@ -77,7 +79,7 @@ export const gatewayServiceFactory = ({ actorAuthMethod, orgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGatewayActions.Create, OrgPermissionSubjects.Gateway); }; const getGatewayRelayDetails = async (actorId: string, actorOrgId: string, actorAuthMethod: ActorAuthMethod) => { @@ -412,6 +414,61 @@ export const gatewayServiceFactory = ({ }; }; + 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, @@ -420,7 +477,7 @@ export const gatewayServiceFactory = ({ orgPermission.authMethod, orgPermission.orgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGatewayActions.Read, OrgPermissionSubjects.Gateway); const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); if (!orgGatewayConfig) return []; @@ -438,7 +495,7 @@ export const gatewayServiceFactory = ({ orgPermission.authMethod, orgPermission.orgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGatewayActions.Read, OrgPermissionSubjects.Gateway); const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); @@ -455,7 +512,7 @@ export const gatewayServiceFactory = ({ orgPermission.authMethod, orgPermission.orgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Delete, OrgPermissionSubjects.Gateway); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGatewayActions.Edit, OrgPermissionSubjects.Gateway); const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); @@ -472,7 +529,7 @@ export const gatewayServiceFactory = ({ orgPermission.authMethod, orgPermission.orgId ); - ForbiddenError.from(permission).throwUnlessCan(OrgGatewayPermissionActions.Delete, OrgPermissionSubjects.Gateway); + ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGatewayActions.Delete, OrgPermissionSubjects.Gateway); const orgGatewayConfig = await orgGatewayConfigDAL.findOne({ orgId: orgPermission.orgId }); if (!orgGatewayConfig) throw new NotFoundError({ message: `Gateway with ID ${id} not found.` }); @@ -553,6 +610,7 @@ export const gatewayServiceFactory = ({ updateGatewayById, deleteGatewayById, getProjectGateways, - fnGetGatewayClientTls + fnGetGatewayClientTls, + heartbeat }; }; diff --git a/backend/src/ee/services/gateway/gateway-types.ts b/backend/src/ee/services/gateway/gateway-types.ts index 666f65e6b..823028154 100644 --- a/backend/src/ee/services/gateway/gateway-types.ts +++ b/backend/src/ee/services/gateway/gateway-types.ts @@ -32,3 +32,7 @@ export type TGetProjectGatewayByIdDTO = { projectId: string; projectPermission: OrgServiceActor; }; + +export type THeartBeatDTO = { + orgPermission: OrgServiceActor; +}; diff --git a/backend/src/ee/services/permission/org-permission.ts b/backend/src/ee/services/permission/org-permission.ts index efed6a1b2..696df74ff 100644 --- a/backend/src/ee/services/permission/org-permission.ts +++ b/backend/src/ee/services/permission/org-permission.ts @@ -32,7 +32,7 @@ export enum OrgPermissionAdminConsoleAction { AccessAllProjects = "access-all-projects" } -export enum OrgGatewayPermissionActions { +export enum OrgPermissionGatewayActions { // is there a better word for this. This mean can an identity be a gateway Create = "create", Read = "read", @@ -82,7 +82,7 @@ export type OrgPermissionSet = | [OrgPermissionActions, OrgPermissionSubjects.Kms] | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates] - | [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway] + | [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway] | [ OrgPermissionAppConnectionActions, ( @@ -190,6 +190,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionKmipActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe( + "Describe what action an entity can take." + ) }) ]); @@ -274,10 +280,10 @@ const buildAdminPermission = () => { can(OrgPermissionAppConnectionActions.Delete, OrgPermissionSubjects.AppConnections); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); - can(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway); - can(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway); - can(OrgGatewayPermissionActions.Edit, OrgPermissionSubjects.Gateway); - can(OrgGatewayPermissionActions.Delete, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.Read, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.Create, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.Edit, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.Delete, OrgPermissionSubjects.Gateway); can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole); @@ -315,8 +321,8 @@ const buildMemberPermission = () => { can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections); - can(OrgGatewayPermissionActions.Read, OrgPermissionSubjects.Gateway); - can(OrgGatewayPermissionActions.Create, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.Read, OrgPermissionSubjects.Gateway); + can(OrgPermissionGatewayActions.Create, OrgPermissionSubjects.Gateway); return rules; }; diff --git a/backend/src/keystore/keystore.ts b/backend/src/keystore/keystore.ts index 04d31fc1a..8ff07db26 100644 --- a/backend/src/keystore/keystore.ts +++ b/backend/src/keystore/keystore.ts @@ -37,7 +37,7 @@ export const KeyStorePrefixes = { IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) => `identity-access-token-status:${identityAccessTokenId}`, ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`, - GatewayIdentityCredential: (identityId: string) => `gateway-credentails:${identityId}` + GatewayIdentityCredential: (identityId: string) => `gateway-credentials:${identityId}` }; export const KeyStoreTtls = { diff --git a/backend/src/lib/gateway/index.ts b/backend/src/lib/gateway/index.ts index b4963cdff..d483b963e 100644 --- a/backend/src/lib/gateway/index.ts +++ b/backend/src/lib/gateway/index.ts @@ -36,12 +36,12 @@ type TPingGatewayAndVerifyDTO = { relayHost: string; relayPort: number; tlsOptions: tls.TlsOptions; - maxRetries: number; + maxRetries?: number; identityId: string; orgId: string; }; -const pingGatewayAndVerifyIdentity = async ({ +export const pingGatewayAndVerify = async ({ relayHost, relayPort, tlsOptions = {}, @@ -236,7 +236,7 @@ export const withGatewayProxy = async ( } = options; // First, try to ping the gateway - await pingGatewayAndVerifyIdentity({ + await pingGatewayAndVerify({ relayHost, relayPort, tlsOptions, diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 7b13c6e02..454257405 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -583,3 +583,20 @@ func CallExchangeRelayCertV1(httpClient *resty.Client, request ExchangeRelayCert 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 +} diff --git a/cli/packages/cmd/gateway.go b/cli/packages/cmd/gateway.go index 945340b22..497200647 100644 --- a/cli/packages/cmd/gateway.go +++ b/cli/packages/cmd/gateway.go @@ -5,10 +5,17 @@ import ( // "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" @@ -33,20 +40,48 @@ var gatewayCmd = &cobra.Command{ util.HandleError(fmt.Errorf("Token not found")) } - gatewayInstance, err := gateway.NewGateway(token.Token) - if err != nil { - util.HandleError(err) - } - - if err = gatewayInstance.ConnectWithRelay(); err != nil { - util.HandleError(err) - } - - if err := gatewayInstance.Listen(); err != nil { - util.HandleError(err) - } - 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) + + go func() { + <-sigCh + close(sigStopCh) + }() + + ctx, cancel := context.WithCancel(cmd.Context()) + defer cancel() + + for { + select { + case <-sigStopCh: + log.Info().Msg("Shutting down gateway") + return + default: + gatewayInstance, err := gateway.NewGateway(token.Token) + if err != nil { + util.HandleError(err) + } + + if err = gatewayInstance.ConnectWithRelay(); err != nil { + log.Error().Msgf("Gateway connection error with relay: %s", err) + log.Info().Msg("Restarting gateway...") + time.Sleep(5 * time.Second) + continue + } + + if err := gatewayInstance.Listen(ctx); err == nil { + // meaning everything went smooth and we are exiting + return + } + + log.Error().Msgf("Gateway listen error: %s", err) + log.Info().Msg("Restarting gateway...") + time.Sleep(5 * time.Second) + } + } }, } diff --git a/cli/packages/gateway/connection.go b/cli/packages/gateway/connection.go index bd91f1f61..c29804506 100644 --- a/cli/packages/gateway/connection.go +++ b/cli/packages/gateway/connection.go @@ -59,7 +59,9 @@ func handleConnection(conn net.Conn) { CopyData(conn, destTarget) return case "PING": - conn.Write([]byte("PONG")) + 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)) diff --git a/cli/packages/gateway/gateway.go b/cli/packages/gateway/gateway.go index 766af9795..cfa4fe24a 100644 --- a/cli/packages/gateway/gateway.go +++ b/cli/packages/gateway/gateway.go @@ -1,11 +1,13 @@ package gateway import ( + "context" "crypto/tls" "crypto/x509" "fmt" "net" "strings" + "sync" "time" "github.com/Infisical/infisical-merge/packages/api" @@ -91,12 +93,13 @@ func (g *Gateway) ConnectWithRelay() error { return nil } -func (g *Gateway) Listen() error { +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 @@ -106,6 +109,7 @@ func (g *Gateway) Listen() error { 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) @@ -128,20 +132,12 @@ func (g *Gateway) Listen() error { g.config.Certificate = gatewayCert.Certificate g.config.CertificateChain = gatewayCert.CertificateChain - go func() { + done := make(chan bool, 1) + + g.registerPermissionLifecycle(func() error { err := relayNonTlsConn.CreatePermissions(peerAddr) - if err != nil { - log.Error().Msgf("Failed to refresh permission: %s", err) - } - log.Printf("Created permission for incoming connections") - ticker := time.NewTicker(2 * time.Minute) // Refresh before 5-min expiry - for range ticker.C { - err := relayNonTlsConn.CreatePermissions(peerAddr) - if err != nil { - log.Error().Msgf("Failed to refresh permission: %s", err) - } - } - }() + return err + }, done) cert, err := tls.X509KeyPair([]byte(gatewayCert.Certificate), []byte(gatewayCert.PrivateKey)) if err != nil { @@ -158,41 +154,146 @@ func (g *Gateway) Listen() error { ClientAuth: tls.RequireAndVerifyClientCert, }) + errCh := make(chan error, 1) log.Info().Msg("Connector started successfully") - for { - // Accept new relay connection - conn, err := relayConn.Accept() - if err != nil { - log.Error().Msgf("Failed to accept connection: %v", err) - continue - } + g.registerHeartBeat(errCh, done) + g.registerRelayIsActive(relayNonTlsConn.Addr().String(), errCh, done) - tlsConn, ok := conn.(*tls.Conn) - if !ok { - log.Error().Msg("Failed to convert to TLS connection") - conn.Close() - continue - } + // Create a WaitGroup to track active connections + var wg sync.WaitGroup - err = tlsConn.Handshake() - if err != nil { - log.Error().Msgf("TLS handshake failed: %v", err) - conn.Close() - continue - } + go func() { + for { + select { + case <-done: + return + default: + // Accept new relay connection + conn, err := relayConn.Accept() + if err != nil { + if !strings.Contains(err.Error(), "data contains incomplete STUN or TURN frame") { + log.Error().Msgf("Failed to accept connection: %v", err) + } + 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) - continue + tlsConn, ok := conn.(*tls.Conn) + if !ok { + log.Error().Msg("Failed to convert to TLS connection") + conn.Close() + continue + } + + err = tlsConn.Handshake() + 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) + continue + } + } + + // Handle the connection in a goroutine + wg.Add(1) + go func() { + defer wg.Done() + handleConnection(conn) + }() } } + }() - // Handle the connection in a goroutine - go handleConnection(conn) + var isShutdown bool + select { + case <-ctx.Done(): + log.Info().Msg("Shutting down gateway...") + isShutdown = true + case err = <-errCh: } + + // Signal the accept loop to stop + close(done) + wg.Wait() + + if isShutdown { + log.Info().Msg("Gateway shutdown complete") + } + + return err +} + +func (g *Gateway) registerHeartBeat(errCh chan error, done chan bool) { + ticker := time.NewTicker(1 * time.Hour) + + go func() { + // wait for 5 mins + time.Sleep(5 * time.Second) + 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: + 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("Ceated 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() + } + } + } + }() } diff --git a/frontend/src/hooks/api/gateways/types.ts b/frontend/src/hooks/api/gateways/types.ts index 9d7917ef4..3c0971079 100644 --- a/frontend/src/hooks/api/gateways/types.ts +++ b/frontend/src/hooks/api/gateways/types.ts @@ -6,6 +6,7 @@ export type TGateway = { updatedAt: string; issuedAt: string; serialNumber: string; + heartbeart: string; identity: { name: string; id: string; diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx index d252f259a..9c9302b49 100644 --- a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx +++ b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx @@ -12,7 +12,7 @@ import { } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery } from "@tanstack/react-query"; -import { format } from "date-fns"; +import { format, formatRelative } from "date-fns"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; @@ -139,6 +139,11 @@ export const GatewayListPage = withPermission( {el.name} {format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")} {el.identity.name} + + {el.heartbeart + ? formatRelative(new Date(), new Date(el.heartbeart)) + : "-"} + diff --git a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts index 985bbbddd..4b3393fa8 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts +++ b/frontend/src/pages/organization/RoleByIDPage/components/OrgRoleModifySection.utils.ts @@ -83,7 +83,7 @@ export const formSchema = z.object({ [OrgPermissionSubjects.ProjectTemplates]: generalPermissionSchema, "app-connections": appConnectionsPermissionSchema, kmip: kmipPermissionSchema, - [OrgPermissionSubjects.Gateway]: orgGatewayPermissionSchema + gateway: orgGatewayPermissionSchema }) .optional() }); diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx index efafa6a42..fe160bb31 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/OrgPermissionGatewayRow.tsx @@ -76,7 +76,7 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props switch (val) { case Permission.FullAccess: setValue( - "permissions.app-connections", + "permissions.gateway", { [OrgGatewayPermissionActions.Read]: true, [OrgGatewayPermissionActions.Edit]: true, @@ -88,7 +88,7 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props break; case Permission.ReadOnly: setValue( - "permissions.app-connections", + "permissions.gateway", { [OrgGatewayPermissionActions.Read]: true, [OrgGatewayPermissionActions.Edit]: false, @@ -102,7 +102,7 @@ export const OrgGatewayPermissionRow = ({ isEditable, control, setValue }: Props case Permission.NoAccess: default: setValue( - "permissions.app-connections", + "permissions.gateway", { [OrgGatewayPermissionActions.Read]: false, [OrgGatewayPermissionActions.Edit]: false,