feat: add support for org relay registration

This commit is contained in:
Sheen Capadngan
2025-09-19 01:52:33 +08:00
parent 31c1006760
commit 88176daf47
18 changed files with 760 additions and 13 deletions
+49 -6
View File
@@ -1,8 +1,9 @@
import { z } from "zod";
import { RelaysSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { UnauthorizedError } from "@app/lib/errors";
import { writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -89,14 +90,56 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
throw new BadRequestError({
message: "Org relay registration is not yet supported"
});
return server.services.relay.registerRelay({
...req.body,
identityId: req.permission.id,
orgId: req.permission.orgId
orgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod
});
}
});
server.route({
method: "GET",
url: "/",
schema: {
response: {
200: RelaysSchema.array()
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.getRelays({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});
server.route({
method: "DELETE",
url: "/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: RelaysSchema
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
return server.services.relay.deleteRelay({
id: req.params.id,
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
}
});
@@ -58,6 +58,13 @@ export enum OrgPermissionGatewayActions {
AttachGateways = "attach-gateways"
}
export enum OrgPermissionRelayActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionIdentityActions {
Read = "read",
Create = "create",
@@ -109,6 +116,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share"
}
@@ -136,6 +144,7 @@ export type OrgPermissionSet =
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionRelayActions, OrgPermissionSubjects.Relay]
| [
OrgPermissionAppConnectionActions,
(
@@ -279,6 +288,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
"Describe what action an entity can take."
)
}),
z.object({
subject: z.literal(OrgPermissionSubjects.Relay).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionRelayActions).describe(
"Describe what action an entity can take."
)
})
]);
@@ -383,6 +398,11 @@ const buildAdminPermission = () => {
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
@@ -445,6 +465,10 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
+102 -4
View File
@@ -1,9 +1,11 @@
import { ForbiddenError } from "@casl/ability";
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 { 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";
import {
@@ -14,6 +16,9 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionRelayActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types";
@@ -29,12 +34,16 @@ export const relayServiceFactory = ({
instanceRelayConfigDAL,
orgRelayConfigDAL,
relayDAL,
kmsService
kmsService,
licenseService,
permissionService
}: {
instanceRelayConfigDAL: TInstanceRelayConfigDALFactory;
orgRelayConfigDAL: TOrgRelayConfigDALFactory;
relayDAL: TRelayDALFactory;
kmsService: TKmsServiceFactory;
licenseService: TLicenseServiceFactory;
permissionService: TPermissionServiceFactory;
}) => {
const $getInstanceCAs = async () => {
const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => {
@@ -819,10 +828,10 @@ export const relayServiceFactory = ({
const relayClientSshCert = await createSshCert({
caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"),
clientPublicKey: relayClientSshPublicKey,
keyId: `relay-client-${relay.id}`,
keyId: `client-${relayName}`,
principals: [gatewayId],
certType: SshCertType.USER,
requestedTtl: "30d"
requestedTtl: "1d"
});
return {
@@ -895,11 +904,13 @@ export const relayServiceFactory = ({
host,
name,
identityId,
actorAuthMethod,
orgId
}: {
host: string;
name: string;
identityId?: string;
actorAuthMethod?: ActorAuthMethod;
orgId?: string;
}) => {
let relay: TRelays;
@@ -908,6 +919,27 @@ export const relayServiceFactory = ({
await verifyHostInputValidity(host);
if (isOrgRelay) {
const orgLicensePlan = await licenseService.getPlan(orgId);
if (!orgLicensePlan.gateway) {
throw new BadRequestError({
message:
"Relay registration failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
});
}
const { permission } = await permissionService.getOrgPermission(
ActorType.IDENTITY,
identityId,
orgId,
actorAuthMethod!,
orgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionRelayActions.CreateRelays,
OrgPermissionSubjects.Relay
);
relay = await relayDAL.transaction(async (tx) => {
const existingRelay = await relayDAL.findOne(
{
@@ -995,9 +1027,75 @@ export const relayServiceFactory = ({
});
};
const getRelays = async ({
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
const instanceRelays = await relayDAL.find({
orgId: null
});
const orgRelays = await relayDAL.find({
orgId: actorOrgId
});
return [...instanceRelays, ...orgRelays];
};
const deleteRelay = async ({
id,
actorId,
actor,
actorAuthMethod,
actorOrgId
}: {
id: string;
actorId: string;
actor: ActorType;
actorAuthMethod: ActorAuthMethod;
actorOrgId: string;
}) => {
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
const relay = await relayDAL.findById(id);
if (!relay || relay.orgId !== actorOrgId) {
throw new NotFoundError({ message: "Relay not found" });
}
const deletedRelay = await relayDAL.deleteById(id);
return deletedRelay;
};
return {
registerRelay,
getCredentialsForGateway,
getCredentialsForClient
getCredentialsForClient,
getRelays,
deleteRelay
};
};
+3 -1
View File
@@ -1110,7 +1110,9 @@ export const registerRoutes = async (
instanceRelayConfigDAL,
orgRelayConfigDAL,
relayDAL,
kmsService
kmsService,
licenseService,
permissionService
});
const gatewayV2Service = gatewayV2ServiceFactory({