mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 21:29:20 +00:00
feat(relays): healthcheck
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Relay, "heartbeat"))) {
|
||||
await knex.schema.alterTable(TableName.Relay, (t) => {
|
||||
t.datetime("heartbeat");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Relay, "heartbeat")) {
|
||||
await knex.schema.alterTable(TableName.Relay, (t) => {
|
||||
t.dropColumn("heartbeat");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@ export const RelaysSchema = z.object({
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
identityId: z.string().uuid().nullable().optional(),
|
||||
name: z.string(),
|
||||
host: z.string()
|
||||
host: z.string(),
|
||||
heartbeat: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TRelays = z.infer<typeof RelaysSchema>;
|
||||
|
||||
@@ -146,4 +146,75 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/heartbeat-instance-relay",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: (req, _, next) => {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (appCfg.RELAY_AUTH_SECRET && authHeader) {
|
||||
const expectedHeader = `Bearer ${appCfg.RELAY_AUTH_SECRET}`;
|
||||
if (
|
||||
authHeader.length === expectedHeader.length &&
|
||||
crypto.nativeCrypto.timingSafeEqual(Buffer.from(authHeader), Buffer.from(expectedHeader))
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid relay auth secret"
|
||||
});
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.relay.heartbeat({
|
||||
name: req.body.name
|
||||
});
|
||||
|
||||
return { message: "Successfully triggered heartbeat" };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/heartbeat-org-relay",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
name: slugSchema({ min: 1, max: 32, field: "name" })
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
await server.services.relay.heartbeat({
|
||||
name: req.body.name,
|
||||
identityId: req.permission.id,
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
return { message: "Successfully triggered heartbeat" };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,7 +6,8 @@ import * as x509 from "@peculiar/x509";
|
||||
import { TRelays } from "@app/db/schemas";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { createRelayConnection } from "@app/lib/gateway-v2/gateway-v2";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
@@ -1056,6 +1057,78 @@ export const relayServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const heartbeat = async ({
|
||||
name,
|
||||
identityId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
}: {
|
||||
name: string;
|
||||
identityId?: string;
|
||||
actorAuthMethod?: ActorAuthMethod;
|
||||
orgId?: string;
|
||||
}) => {
|
||||
const relay = await relayDAL.findOne({
|
||||
name,
|
||||
orgId: orgId ?? null
|
||||
});
|
||||
|
||||
if (!relay) {
|
||||
throw new NotFoundError({ message: `Relay with name ${name} not found.` });
|
||||
}
|
||||
|
||||
let clientOrgId: string;
|
||||
let clientOrgName: string;
|
||||
|
||||
if (relay.orgId) {
|
||||
if (!identityId || !orgId || relay.orgId !== orgId) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "You do not have permission to perform this action on this relay."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityId,
|
||||
orgId,
|
||||
actorAuthMethod!,
|
||||
orgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionRelayActions.CreateRelays,
|
||||
OrgPermissionSubjects.Relay
|
||||
);
|
||||
clientOrgId = orgId;
|
||||
clientOrgName = orgId;
|
||||
} else {
|
||||
clientOrgId = "00000000-0000-0000-0000-000000000000";
|
||||
clientOrgName = "heartbeat";
|
||||
}
|
||||
|
||||
const relayClientCredentials = await getCredentialsForClient({
|
||||
relayId: relay.id,
|
||||
orgId: clientOrgId,
|
||||
orgName: clientOrgName,
|
||||
gatewayId: "00000000-0000-0000-0000-000000000000",
|
||||
gatewayName: "heartbeat",
|
||||
duration: 60 * 1000 // 1 minute
|
||||
});
|
||||
|
||||
try {
|
||||
await createRelayConnection({
|
||||
relayHost: relayClientCredentials.relayHost,
|
||||
clientCertificate: relayClientCredentials.clientCertificate,
|
||||
clientPrivateKey: relayClientCredentials.clientPrivateKey,
|
||||
serverCertificateChain: relayClientCredentials.serverCertificateChain
|
||||
});
|
||||
|
||||
await relayDAL.updateById(relay.id, { heartbeat: new Date() });
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
throw new BadRequestError({ message: `Relay ${name} is not reachable: ${error.message}` });
|
||||
}
|
||||
};
|
||||
|
||||
const getRelays = async ({
|
||||
actorId,
|
||||
actor,
|
||||
@@ -1125,6 +1198,7 @@ export const relayServiceFactory = ({
|
||||
getCredentialsForGateway,
|
||||
getCredentialsForClient,
|
||||
getRelays,
|
||||
deleteRelay
|
||||
deleteRelay,
|
||||
heartbeat
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ interface IGatewayRelayServer {
|
||||
getRelayError: () => string;
|
||||
}
|
||||
|
||||
const createRelayConnection = async ({
|
||||
export const createRelayConnection = async ({
|
||||
relayHost,
|
||||
clientCertificate,
|
||||
clientPrivateKey,
|
||||
|
||||
Reference in New Issue
Block a user