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

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
});
}
});

View File

@@ -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,

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
};
};

View File

@@ -1110,7 +1110,9 @@ export const registerRoutes = async (
instanceRelayConfigDAL,
orgRelayConfigDAL,
relayDAL,
kmsService
kmsService,
licenseService,
permissionService
});
const gatewayV2Service = gatewayV2ServiceFactory({

View File

@@ -21,6 +21,13 @@ export enum OrgGatewayPermissionActions {
AttachGateways = "attach-gateways"
}
export enum OrgRelayPermissionActions {
CreateRelays = "create-relays",
ListRelays = "list-relays",
EditRelays = "edit-relays",
DeleteRelays = "delete-relays"
}
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
CreateTemplates = "create-templates",
@@ -51,6 +58,7 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections",
Kmip = "kmip",
Gateway = "gateway",
Relay = "relay",
SecretShare = "secret-share",
GithubOrgSync = "github-org-sync",
GithubOrgSyncManual = "github-org-sync-manual",
@@ -135,6 +143,7 @@ export type OrgPermissionSet =
OrgPermissionSubjects.MachineIdentityAuthTemplate
]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgRelayPermissionActions, OrgPermissionSubjects.Relay]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]
| [
OrgPermissionAppConnectionActions,

View File

@@ -0,0 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";

View File

@@ -0,0 +1,17 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { relayQueryKeys } from "./queries";
export const useDeleteRelayById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => {
return apiRequest.delete(`/api/v1/relays/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: relayQueryKeys.list() });
}
});
};

View File

@@ -0,0 +1,21 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TRelay } from "./types";
export const relayQueryKeys = {
list: () => ["relays"] as const
};
const fetchRelays = async (): Promise<TRelay[]> => {
const { data } = await apiRequest.get<TRelay[]>("/api/v1/relays");
return data;
};
export const useGetRelays = () => {
return useQuery({
queryKey: relayQueryKeys.list(),
queryFn: fetchRelays
});
};

View File

@@ -0,0 +1,13 @@
export type TRelay = {
id: string;
createdAt: string;
updatedAt: string;
orgId: string | null;
identityId: string | null;
name: string;
host: string;
};
export type TDeleteRelayDTO = {
id: string;
};

View File

@@ -5,6 +5,7 @@ import {
faInfinity,
faMoneyBill,
faPlug,
faRoute,
faShare,
faTable,
faUsers,
@@ -136,6 +137,18 @@ export const OrgSidebar = ({ isHidden }: Props) => {
</MenuItem>
)}
</Link>
<Link to="/organization/relays">
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faRoute} className="mr-4" />
</div>
Relays
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
<div className="flex-grow" />

View File

@@ -0,0 +1,214 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import {
faArrowUpRightFromSquare,
faBookOpen,
faCopy,
faDoorClosed,
faEllipsisV,
faMagnifyingGlass,
faSearch,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { 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,
PageHeader,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import {
OrgPermissionSubjects,
OrgRelayPermissionActions
} from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteRelayById, useGetRelays } from "@app/hooks/api/relays";
export const RelayListPage = withPermission(
() => {
const [search, setSearch] = useState("");
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["deleteRelay"] as const);
const deleteRelayById = useDeleteRelayById();
const handleDeleteRelay = async () => {
const data = popUp.deleteRelay.data as { id: string };
await deleteRelayById.mutateAsync(data.id);
handlePopUpToggle("deleteRelay");
createNotification({
type: "success",
text: "Successfully deleted relay"
});
};
const filteredRelays = relays?.filter((el) =>
el.name.toLowerCase().includes(search.toLowerCase())
);
return (
<div className="bg-bunker-800">
<Helmet>
<title>Infisical | Relays</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>Relays</span>
<a
className="-mt-1.5"
href="https://infisical.com/docs/documentation/platform/relays/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 relays to securely 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 relay..."
className="flex-1"
/>
</div>
<TableContainer className="mt-4">
<Table>
<THead>
<Tr>
<Th className="w-1/3">Name</Th>
<Th>Host</Th>
<Th>Created</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isRelaysLoading && (
<TableSkeleton innerKey="relay-table" columns={4} key="relay-table" />
)}
{filteredRelays?.map((el) => (
<Tr key={el.id}>
<Td>
<div className="flex items-center gap-2">
<span>{el.name}</span>
{!el.orgId && (
<Tooltip content="This is an instance-level relay available to all organizations">
<span className="rounded bg-mineshaft-700 px-1.5 py-0.5 text-xs text-mineshaft-400">
Instance
</span>
</Tooltip>
)}
</div>
</Td>
<Td>{el.host}</Td>
<Td>{formatRelative(new Date(el.createdAt), 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">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={() => navigator.clipboard.writeText(el.id)}
>
Copy ID
</DropdownMenuItem>
<OrgPermissionCan
I={OrgRelayPermissionActions.DeleteRelays}
a={OrgPermissionSubjects.Relay}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed || !el.orgId}
icon={<FontAwesomeIcon icon={faTrash} />}
className="text-red"
onClick={() => handlePopUpOpen("deleteRelay", el)}
>
Delete Relay
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isRelaysLoading && !filteredRelays?.length && (
<EmptyState
title={
relays?.length
? "No Relays match search..."
: "No Relays have been configured"
}
icon={relays?.length ? faSearch : faDoorClosed}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteRelay.isOpen}
title={`Are you sure you want to delete relay ${
(popUp?.deleteRelay?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteRelay", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleDeleteRelay()}
/>
</TableContainer>
</div>
</div>
</div>
</div>
</div>
);
},
{ action: OrgRelayPermissionActions.ListRelays, subject: OrgPermissionSubjects.Relay }
);

View File

@@ -0,0 +1,16 @@
import { createFileRoute } from "@tanstack/react-router";
import { RelayListPage } from "./RelayListPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/relays/"
)({
component: RelayListPage,
context: () => ({
breadcrumbs: [
{
label: "Relays"
}
]
})
});

View File

@@ -11,7 +11,8 @@ import {
OrgPermissionIdentityActions,
OrgPermissionKmipActions,
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSecretShareAction
OrgPermissionSecretShareAction,
OrgRelayPermissionActions
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
@@ -90,6 +91,15 @@ const orgGatewayPermissionSchema = z
})
.optional();
const orgRelayPermissionSchema = z
.object({
[OrgRelayPermissionActions.ListRelays]: z.boolean().optional(),
[OrgRelayPermissionActions.EditRelays]: z.boolean().optional(),
[OrgRelayPermissionActions.DeleteRelays]: z.boolean().optional(),
[OrgRelayPermissionActions.CreateRelays]: z.boolean().optional()
})
.optional();
const machineIdentityAuthTemplatePermissionSchema = z
.object({
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(),
@@ -147,6 +157,7 @@ export const formSchema = z.object({
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema,
gateway: orgGatewayPermissionSchema,
relay: orgRelayPermissionSchema,
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
"secret-share": secretSharingPermissionSchema
})

View File

@@ -0,0 +1,180 @@
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 { OrgRelayPermissionActions } 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: OrgRelayPermissionActions.ListRelays, label: "List Relays" },
{ action: OrgRelayPermissionActions.CreateRelays, label: "Create Relays" },
{ action: OrgRelayPermissionActions.EditRelays, label: "Edit Relays" },
{ action: OrgRelayPermissionActions.DeleteRelays, label: "Delete Relays" }
] as const;
export const OrgRelayPermissionRow = ({ isEditable, control, setValue }: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.relay"
});
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?.[OrgRelayPermissionActions.ListRelays]) 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.relay",
{
[OrgRelayPermissionActions.ListRelays]: true,
[OrgRelayPermissionActions.EditRelays]: true,
[OrgRelayPermissionActions.CreateRelays]: true,
[OrgRelayPermissionActions.DeleteRelays]: true
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.relay",
{
[OrgRelayPermissionActions.ListRelays]: true,
[OrgRelayPermissionActions.EditRelays]: false,
[OrgRelayPermissionActions.CreateRelays]: false,
[OrgRelayPermissionActions.DeleteRelays]: false
},
{ shouldDirty: true }
);
break;
case Permission.NoAccess:
default:
setValue(
"permissions.relay",
{
[OrgRelayPermissionActions.ListRelays]: false,
[OrgRelayPermissionActions.EditRelays]: false,
[OrgRelayPermissionActions.CreateRelays]: false,
[OrgRelayPermissionActions.DeleteRelays]: false
},
{ shouldDirty: true }
);
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td className="w-full select-none">Relays</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<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="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.relay.${action}`}
key={`permissions.relay.${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.relays.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};

View File

@@ -69,6 +69,7 @@ type Props = {
| "organization-admin-console"
| "kmip"
| "gateway"
| "relay"
| "secret-share"
| "billing"
| "audit-logs"

View File

@@ -23,6 +23,7 @@ import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
import { OrgRelayPermissionRow } from "./OrgPermissionRelayRow";
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@@ -188,6 +189,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgRelayPermissionRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionBillingRow
control={control}
setValue={setValue}

View File

@@ -62,6 +62,7 @@ import { Route as organizationIdentityDetailsByIDPageRouteImport } from './pages
import { Route as organizationGroupDetailsByIDPageRouteImport } from './pages/organization/GroupDetailsByIDPage/route'
import { Route as organizationSettingsPageRouteImport } from './pages/organization/SettingsPage/route'
import { Route as organizationSecretSharingPageRouteImport } from './pages/organization/SecretSharingPage/route'
import { Route as organizationGatewaysRelayRelayListPageRouteImport } from './pages/organization/Gateways/Relay/RelayListPage/route'
import { Route as organizationGatewaysGatewayListPageRouteImport } from './pages/organization/Gateways/GatewayListPage/route'
import { Route as organizationAppConnectionsAppConnectionsPageRouteImport } from './pages/organization/AppConnections/AppConnectionsPage/route'
import { Route as sshLayoutImport } from './pages/ssh/layout'
@@ -255,6 +256,10 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/relays',
)()
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport =
createFileRoute(
'/_authenticate/_inject-org-details/_org-layout/organization/gateways',
@@ -559,6 +564,14 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysImport.update({
id: '/relays',
path: '/relays',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRoute,
} as any)
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport.update({
id: '/gateways',
@@ -765,6 +778,14 @@ const organizationSecretSharingPageRouteRoute =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute,
} as any)
const organizationGatewaysRelayRelayListPageRouteRoute =
organizationGatewaysRelayRelayListPageRouteImport.update({
id: '/',
path: '/',
getParentRoute: () =>
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRoute,
} as any)
const organizationGatewaysGatewayListPageRouteRoute =
organizationGatewaysGatewayListPageRouteImport.update({
id: '/',
@@ -2376,6 +2397,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/relays': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/relays'
path: '/relays'
fullPath: '/organization/relays'
preLoaderRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
path: '/secret-sharing'
@@ -2411,6 +2439,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof organizationGatewaysGatewayListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/relays/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/relays/'
path: '/'
fullPath: '/organization/relays/'
preLoaderRoute: typeof organizationGatewaysRelayRelayListPageRouteImport
parentRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysImport
}
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': {
id: '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
path: '/'
@@ -3734,6 +3769,21 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteChildren {
organizationGatewaysRelayRelayListPageRouteRoute: typeof organizationGatewaysRelayRelayListPageRouteRoute
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteChildren: AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteChildren =
{
organizationGatewaysRelayRelayListPageRouteRoute:
organizationGatewaysRelayRelayListPageRouteRoute,
}
const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteWithChildren =
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRoute._addFileChildren(
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteChildren,
)
interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteChildren {
organizationSecretSharingPageRouteRoute: typeof organizationSecretSharingPageRouteRoute
organizationSecretSharingSettingsPageRouteRoute: typeof organizationSecretSharingSettingsPageRouteRoute
@@ -3776,6 +3826,7 @@ interface AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren {
organizationProjectsPageRouteRoute: typeof organizationProjectsPageRouteRoute
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute: typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
organizationGroupDetailsByIDPageRouteRoute: typeof organizationGroupDetailsByIDPageRouteRoute
@@ -3795,6 +3846,8 @@ const AuthenticateInjectOrgDetailsOrgLayoutOrganizationRouteChildren: Authentica
AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRoute:
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren,
AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRoute:
@@ -4694,11 +4747,13 @@ export interface FileRoutesByFullPath {
'/admin/integrations': typeof adminIntegrationsPageRouteRoute
'/organization/app-connections': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationAppConnectionsRouteWithChildren
'/organization/gateways': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationGatewaysRouteWithChildren
'/organization/relays': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteWithChildren
'/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/organization/app-connections/': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways/': typeof organizationGatewaysGatewayListPageRouteRoute
'/organization/relays/': typeof organizationGatewaysRelayRelayListPageRouteRoute
'/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/organization/settings/': typeof organizationSettingsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
@@ -4915,6 +4970,7 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/organization/app-connections': typeof organizationAppConnectionsAppConnectionsPageRouteRoute
'/organization/gateways': typeof organizationGatewaysGatewayListPageRouteRoute
'/organization/relays': typeof organizationGatewaysRelayRelayListPageRouteRoute
'/organization/secret-sharing': typeof organizationSecretSharingPageRouteRoute
'/organization/settings': typeof organizationSettingsPageRouteRoute
'/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
@@ -5134,11 +5190,13 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/admin/_admin-layout/integrations': typeof adminIntegrationsPageRouteRoute
'/_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/organization/relays': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationRelaysRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSecretSharingRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/organization/settings': typeof AuthenticateInjectOrgDetailsOrgLayoutOrganizationSettingsRouteWithChildren
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId': typeof AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildren
'/_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/relays/': typeof organizationGatewaysRelayRelayListPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/': typeof organizationSecretSharingPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/settings/': typeof organizationSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId': typeof organizationGroupDetailsByIDPageRouteRoute
@@ -5365,11 +5423,13 @@ export interface FileRouteTypes {
| '/admin/integrations'
| '/organization/app-connections'
| '/organization/gateways'
| '/organization/relays'
| '/organization/secret-sharing'
| '/organization/settings'
| '/secret-manager/$projectId'
| '/organization/app-connections/'
| '/organization/gateways/'
| '/organization/relays/'
| '/organization/secret-sharing/'
| '/organization/settings/'
| '/organization/groups/$groupId'
@@ -5585,6 +5645,7 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId'
| '/organization/app-connections'
| '/organization/gateways'
| '/organization/relays'
| '/organization/secret-sharing'
| '/organization/settings'
| '/organization/groups/$groupId'
@@ -5802,11 +5863,13 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/admin/_admin-layout/integrations'
| '/_authenticate/_inject-org-details/_org-layout/organization/app-connections'
| '/_authenticate/_inject-org-details/_org-layout/organization/gateways'
| '/_authenticate/_inject-org-details/_org-layout/organization/relays'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$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/relays/'
| '/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/'
| '/_authenticate/_inject-org-details/_org-layout/organization/settings/'
| '/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId'
@@ -6228,6 +6291,7 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/projects",
"/_authenticate/_inject-org-details/_org-layout/organization/app-connections",
"/_authenticate/_inject-org-details/_org-layout/organization/gateways",
"/_authenticate/_inject-org-details/_org-layout/organization/relays",
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing",
"/_authenticate/_inject-org-details/_org-layout/organization/settings",
"/_authenticate/_inject-org-details/_org-layout/organization/groups/$groupId",
@@ -6309,6 +6373,13 @@ export const routeTree = rootRoute
"/_authenticate/_inject-org-details/_org-layout/organization/gateways/"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/relays": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
"children": [
"/_authenticate/_inject-org-details/_org-layout/organization/relays/"
]
},
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing": {
"filePath": "",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization",
@@ -6340,6 +6411,10 @@ export const routeTree = rootRoute
"filePath": "organization/Gateways/GatewayListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/gateways"
},
"/_authenticate/_inject-org-details/_org-layout/organization/relays/": {
"filePath": "organization/Gateways/Relay/RelayListPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/relays"
},
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing/": {
"filePath": "organization/SecretSharingPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"

View File

@@ -40,7 +40,8 @@ const organizationRoutes = route("/organization", [
"organization/AppConnections/OauthCallbackPage/route.tsx"
)
]),
route("/gateways", [index("organization/Gateways/GatewayListPage/route.tsx")])
route("/gateways", [index("organization/Gateways/GatewayListPage/route.tsx")]),
route("/relays", [index("organization/Gateways/Relay/RelayListPage/route.tsx")])
]);
const secretManagerRoutes = route("/projects/secret-management/$projectId", [