Merge branch 'main' into feat/terraformCloudIntegration

This commit is contained in:
carlosmonastyrski
2025-04-11 16:37:05 -03:00
126 changed files with 5117 additions and 586 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -75,6 +75,14 @@ export enum ProjectPermissionGroupActions {
GrantPrivileges = "grant-privileges"
}
export enum ProjectPermissionSshHostActions {
Read = "read",
Create = "create",
Edit = "edit",
Delete = "delete",
IssueHostCert = "issue-host-cert"
}
export enum ProjectPermissionSecretRotationActions {
Read = "read",
ReadGeneratedCredentials = "read-generated-credentials",
@@ -148,6 +156,7 @@ export enum ProjectPermissionSub {
SshCertificateAuthorities = "ssh-certificate-authorities",
SshCertificateTemplates = "ssh-certificate-templates",
SshCertificates = "ssh-certificates",
SshHosts = "ssh-hosts",
PkiAlerts = "pki-alerts",
PkiCollections = "pki-collections",
Kms = "kms",
@@ -244,6 +253,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates]
| [ProjectPermissionActions, ProjectPermissionSub.SshCertificates]
| [ProjectPermissionSshHostActions, ProjectPermissionSub.SshHosts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
| [ProjectPermissionSecretSyncActions, ProjectPermissionSub.SecretSyncs]
@@ -79,6 +79,8 @@ export type TAccessApprovalRequest = {
member: string;
status: string;
}[];
note?: string;
};
export type TAccessApproval = {
@@ -119,6 +121,7 @@ export type TProjectUserPrivilege = {
export type TCreateAccessRequestDTO = {
projectSlug: string;
note?: string;
} & Omit<TProjectUserPrivilege, "id" | "createdAt" | "updatedAt" | "slug" | "projectMembershipId">;
export type TGetAccessApprovalRequestsDTO = {
+1
View File
@@ -41,6 +41,7 @@ export * from "./serverDetails";
export * from "./serviceTokens";
export * from "./sshCa";
export * from "./sshCertificateTemplates";
export * from "./sshHost";
export * from "./ssoConfig";
export * from "./subscriptions";
export * from "./tags";
@@ -12,3 +12,47 @@ export const sshCertTypeToNameMap: { [K in SshCertType]: string } = {
[SshCertType.USER]: "User",
[SshCertType.HOST]: "Host"
};
export enum SshCaKeySource {
INTERNAL = "internal",
EXTERNAL = "external"
}
export enum SshCertKeyAlgorithm {
RSA_2048 = "RSA_2048",
RSA_4096 = "RSA_4096",
ECDSA_P256 = "EC_prime256v1",
ECDSA_P384 = "EC_secp384r1",
ED25519 = "ED25519"
}
export const sshCertKeyAlgorithmToNameMap: { [K in SshCertKeyAlgorithm]: string } = {
[SshCertKeyAlgorithm.RSA_2048]: "RSA 2048",
[SshCertKeyAlgorithm.RSA_4096]: "RSA 4096",
[SshCertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
[SshCertKeyAlgorithm.ECDSA_P384]: "ECDSA P384",
[SshCertKeyAlgorithm.ED25519]: "ED25519"
};
export const sshCertKeyAlgorithms = [
{
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.RSA_2048],
value: SshCertKeyAlgorithm.RSA_2048
},
{
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.RSA_4096],
value: SshCertKeyAlgorithm.RSA_4096
},
{
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.ECDSA_P256],
value: SshCertKeyAlgorithm.ECDSA_P256
},
{
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.ECDSA_P384],
value: SshCertKeyAlgorithm.ECDSA_P384
},
{
label: sshCertKeyAlgorithmToNameMap[SshCertKeyAlgorithm.ED25519],
value: SshCertKeyAlgorithm.ED25519
}
];
+20 -10
View File
@@ -1,5 +1,4 @@
import { CertKeyAlgorithm } from "../certificates/enums";
import { SshCaStatus, SshCertType } from "./constants";
import { SshCaKeySource, SshCaStatus, SshCertKeyAlgorithm, SshCertType } from "./constants";
export type TSshCertificate = {
id: string;
@@ -18,17 +17,28 @@ export type TSshCertificateAuthority = {
projectId: string;
status: SshCaStatus;
friendlyName: string;
keyAlgorithm: CertKeyAlgorithm;
keyAlgorithm: SshCertKeyAlgorithm;
keySource: SshCaKeySource;
createdAt: string;
updatedAt: string;
publicKey: string;
};
export type TCreateSshCaDTO = {
projectId: string;
friendlyName?: string;
keyAlgorithm: CertKeyAlgorithm;
};
export type TCreateSshCaDTO =
| {
projectId: string;
friendlyName?: string;
keySource: SshCaKeySource.INTERNAL;
keyAlgorithm: SshCertKeyAlgorithm;
}
| {
projectId: string;
friendlyName?: string;
keySource: SshCaKeySource.EXTERNAL;
keyAlgorithm: SshCertKeyAlgorithm;
publicKey: string;
privateKey: string;
};
export type TUpdateSshCaDTO = {
caId: string;
@@ -58,7 +68,7 @@ export type TSignSshKeyResponse = {
export type TIssueSshCredsDTO = {
projectId: string;
certificateTemplateId: string;
keyAlgorithm: CertKeyAlgorithm;
keyAlgorithm: SshCertKeyAlgorithm;
certType: SshCertType;
principals: string[];
ttl?: string;
@@ -70,5 +80,5 @@ export type TIssueSshCredsResponse = {
signedKey: string;
privateKey: string;
publicKey: string;
keyAlgorithm: CertKeyAlgorithm;
keyAlgorithm: SshCertKeyAlgorithm;
};
+2
View File
@@ -0,0 +1,2 @@
export { useCreateSshHost, useDeleteSshHost, useUpdateSshHost } from "./mutations";
export { fetchSshHostUserCaPublicKey, useGetSshHostById } from "./queries";
@@ -0,0 +1,45 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace/query-keys";
import { TCreateSshHostDTO, TDeleteSshHostDTO, TSshHost, TUpdateSshHostDTO } from "./types";
export const useCreateSshHost = () => {
const queryClient = useQueryClient();
return useMutation<TSshHost, object, TCreateSshHostDTO>({
mutationFn: async (body) => {
const { data: host } = await apiRequest.post("/api/v1/ssh/hosts", body);
return host;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) });
}
});
};
export const useUpdateSshHost = () => {
const queryClient = useQueryClient();
return useMutation<TSshHost, object, TUpdateSshHostDTO>({
mutationFn: async ({ sshHostId, ...body }) => {
const { data: host } = await apiRequest.patch(`/api/v1/ssh/hosts/${sshHostId}`, body);
return host;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) });
}
});
};
export const useDeleteSshHost = () => {
const queryClient = useQueryClient();
return useMutation<TSshHost, object, TDeleteSshHostDTO>({
mutationFn: async ({ sshHostId }) => {
const { data: host } = await apiRequest.delete(`/api/v1/ssh/hosts/${sshHostId}`);
return host;
},
onSuccess: ({ projectId }) => {
queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshHosts(projectId) });
}
});
};
@@ -0,0 +1,28 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSshHost } from "./types";
export const sshHostKeys = {
getSshHostById: (sshHostId: string) => [{ sshHostId }, "ssh-host"],
getSshHostUserCaPublicKey: (sshHostId: string) => [{ sshHostId }, "ssh-host-user-ca-public-key"]
};
export const useGetSshHostById = (sshHostId: string) => {
return useQuery({
queryKey: sshHostKeys.getSshHostById(sshHostId),
queryFn: async () => {
const { data: sshHost } = await apiRequest.get<TSshHost>(`/api/v1/ssh/hosts/${sshHostId}`);
return sshHost;
},
enabled: Boolean(sshHostId)
});
};
export const fetchSshHostUserCaPublicKey = async (sshHostId: string): Promise<string> => {
const { data } = await apiRequest.get<string>(
`/api/v1/ssh/hosts/${sshHostId}/user-ca-public-key`
);
return data;
};
+43
View File
@@ -0,0 +1,43 @@
export type TSshHost = {
id: string;
projectId: string;
hostname: string;
userCertTtl: string;
hostCertTtl: string;
loginMappings: {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
};
export type TCreateSshHostDTO = {
projectId: string;
hostname: string;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings: {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
};
export type TUpdateSshHostDTO = {
sshHostId: string;
hostname?: string;
userCertTtl?: string;
hostCertTtl?: string;
loginMappings?: {
loginUser: string;
allowedPrincipals: {
usernames: string[];
};
}[];
};
export type TDeleteSshHostDTO = {
sshHostId: string;
};
@@ -36,6 +36,7 @@ export {
useListWorkspaceSshCas,
useListWorkspaceSshCertificates,
useListWorkspaceSshCertificateTemplates,
useListWorkspaceSshHosts,
useNameWorkspaceSecrets,
useSearchProjects,
useToggleAutoCapitalization,
@@ -17,6 +17,7 @@ import { TPkiCollection } from "../pkiCollections/types";
import { EncryptedSecret } from "../secrets/types";
import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types";
import { TSshCertificateTemplate } from "../sshCertificateTemplates/types";
import { TSshHost } from "../sshHost/types";
import { userKeys } from "../users/query-keys";
import { TWorkspaceUser } from "../users/types";
import { ProjectSlackConfig } from "../workflowIntegrations/types";
@@ -827,6 +828,19 @@ export const useListWorkspaceSshCas = (projectId: string) => {
});
};
export const useListWorkspaceSshHosts = (projectId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSshHosts(projectId),
queryFn: async () => {
const {
data: { hosts }
} = await apiRequest.get<{ hosts: TSshHost[] }>(`/api/v2/workspace/${projectId}/ssh-hosts`);
return hosts;
},
enabled: Boolean(projectId)
});
};
export const useListWorkspaceSshCertificateTemplates = (projectId: string) => {
return useQuery({
queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId),
@@ -58,6 +58,7 @@ export const workspaceKeys = {
getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const,
allWorkspaceSshCertificates: (projectId: string) =>
[{ projectId }, "workspace-ssh-certificates"] as const,
getWorkspaceSshHosts: (projectId: string) => [{ projectId }, "workspace-ssh-hosts"] as const,
specificWorkspaceSshCertificates: ({
offset,
limit,
@@ -134,18 +134,48 @@ export const ProjectLayout = () => {
</Link>
)}
{isSSH && (
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="lock-closed">
Overview
</MenuItem>
)}
</Link>
<>
<Link
to={`/${ProjectType.SSH}/$projectId/overview` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="server">
Hosts
</MenuItem>
)}
</Link>
{/* <Link
to={`/${ProjectType.SSH}/$projectId/certificates` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="certificate" iconMode="reverse">
Certificates
</MenuItem>
)}
</Link> */}
{/* <Link
to={`/${ProjectType.SSH}/$projectId/cas` as const}
params={{
projectId: currentWorkspace.id
}}
>
{({ isActive }) => (
<MenuItem
isSelected={isActive}
icon="certificate-authority"
iconMode="reverse"
>
Certificate Authorities
</MenuItem>
)}
</Link> */}
</>
)}
{isSecretManager && (
<Link
@@ -24,7 +24,7 @@ const formatDescription = (type: ProjectType) => {
return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices.";
if (type === ProjectType.KMS)
return "Centralize the management of keys for cryptographic operations, such as encryption and decryption.";
return "Generate SSH credentials to provide secure and centralized SSH access control for your infrastructure.";
return "Infisical SSH lets you issue SSH credentials to users for short-lived, secure SSH access to infrastructure.";
};
type Props = {
@@ -67,7 +67,8 @@ const secretPermissionSchema = z.object({
z.object({
isTemporary: z.literal(false)
})
])
]),
note: z.string().optional()
});
type TSecretPermissionForm = z.infer<typeof secretPermissionSchema>;
export const SpecificPrivilegeSecretForm = ({
@@ -231,7 +232,8 @@ export const SpecificPrivilegeSecretForm = ({
action,
subject: [ProjectPermissionSub.Secrets],
conditions
}))
})),
note: data.note
});
createNotification({
@@ -541,6 +543,18 @@ export const SpecificPrivilegeSecretForm = ({
)}
</div>
</div>
<div className="mb-4 flex w-full">
<Controller
control={privilegeForm.control}
name="note"
render={({ field }) => (
<div className="w-full">
<FormLabel label="Note" className="mb-2" />
<Input {...field} isDisabled={isMemberEditDisabled} maxLength={255} />
</div>
)}
/>
</div>
{!!policies && (
<Button
type="submit"
@@ -19,6 +19,7 @@ import {
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions,
ProjectPermissionSecretSyncActions,
ProjectPermissionSshHostActions,
TPermissionCondition,
TPermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types";
@@ -108,6 +109,14 @@ const GroupPolicyActionSchema = z.object({
[ProjectPermissionGroupActions.GrantPrivileges]: z.boolean().optional()
});
const SshHostPolicyActionSchema = z.object({
[ProjectPermissionSshHostActions.Read]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Create]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Edit]: z.boolean().optional(),
[ProjectPermissionSshHostActions.Delete]: z.boolean().optional(),
[ProjectPermissionSshHostActions.IssueHostCert]: z.boolean().optional()
});
const SecretRollbackPolicyActionSchema = z.object({
read: z.boolean().optional(),
create: z.boolean().optional()
@@ -215,6 +224,12 @@ export const projectRoleFormSchema = z.object({
),
[ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SshHosts]: SshHostPolicyActionSchema.extend({
inverted: z.boolean().optional(),
conditions: ConditionSchema
})
.array()
.default([]),
[ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]),
[ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]),
[ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]),
@@ -241,6 +256,7 @@ type TConditionalFields =
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.SshHosts
| ProjectPermissionSub.SecretRotation
| ProjectPermissionSub.Identity;
@@ -251,8 +267,9 @@ export const isConditionalSubjects = (
subject === ProjectPermissionSub.DynamicSecrets ||
subject === ProjectPermissionSub.SecretImports ||
subject === ProjectPermissionSub.SecretFolders ||
subject === ProjectPermissionSub.SecretRotation ||
subject === ProjectPermissionSub.Identity;
subject === ProjectPermissionSub.Identity ||
subject === ProjectPermissionSub.SshHosts ||
subject === ProjectPermissionSub.SecretRotation;
const convertCaslConditionToFormOperator = (caslConditions: TPermissionCondition) => {
const formConditions: z.infer<typeof ConditionSchema> = [];
@@ -308,7 +325,10 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Tags,
ProjectPermissionSub.SecretRotation,
ProjectPermissionSub.Kms
ProjectPermissionSub.Kms,
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates
].includes(subject)
) {
// from above statement we are sure it won't be undefined
@@ -577,7 +597,32 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
if (canRemoveSecrets)
formVal[subject]![0][ProjectPermissionSecretSyncActions.RemoveSecrets] = true;
}
if (subject === ProjectPermissionSub.SshHosts) {
if (!formVal[subject]) formVal[subject] = [];
formVal[subject]!.push({
[ProjectPermissionSshHostActions.Edit]: action.includes(
ProjectPermissionSshHostActions.Edit
),
[ProjectPermissionSshHostActions.Delete]: action.includes(
ProjectPermissionSshHostActions.Delete
),
[ProjectPermissionSshHostActions.Create]: action.includes(
ProjectPermissionSshHostActions.Create
),
[ProjectPermissionSshHostActions.Read]: action.includes(
ProjectPermissionSshHostActions.Read
),
[ProjectPermissionSshHostActions.IssueHostCert]: action.includes(
ProjectPermissionSshHostActions.IssueHostCert
),
conditions: conditions ? convertCaslConditionToFormOperator(conditions) : [],
inverted
});
}
});
return formVal;
};
@@ -901,6 +946,16 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
{ label: "Remove", value: "delete" }
]
},
[ProjectPermissionSub.SshHosts]: {
title: "SSH Hosts",
actions: [
{ label: "Read", value: ProjectPermissionSshHostActions.Read },
{ label: "Create", value: ProjectPermissionSshHostActions.Create },
{ label: "Modify", value: ProjectPermissionSshHostActions.Edit },
{ label: "Remove", value: ProjectPermissionSshHostActions.Delete },
{ label: "Issue Host Certificate", value: ProjectPermissionSshHostActions.IssueHostCert }
]
},
[ProjectPermissionSub.PkiCollections]: {
title: "PKI Collections",
actions: [
@@ -19,6 +19,7 @@ import { ProjectPermissionSub, useWorkspace } from "@app/context";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import { evaluatePermissionsAbility } from "@app/helpers/permissions";
import { useGetProjectRoleBySlug, useUpdateProjectRole } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { GeneralPermissionConditions } from "./GeneralPermissionConditions";
import { GeneralPermissionPolicies } from "./GeneralPermissionPolicies";
@@ -33,6 +34,7 @@ import {
TFormSchema
} from "./ProjectRoleModifySection.utils";
import { SecretPermissionConditions } from "./SecretPermissionConditions";
import { SshHostPermissionConditions } from "./SshHostPermissionConditions";
type Props = {
roleSlug: string;
@@ -51,6 +53,10 @@ export const renderConditionalComponents = (
return <IdentityManagementPermissionConditions isDisabled={isDisabled} />;
}
if (subject === ProjectPermissionSub.SshHosts) {
return <SshHostPermissionConditions isDisabled={isDisabled} />;
}
return <GeneralPermissionConditions isDisabled={isDisabled} type={subject} />;
}
@@ -134,7 +140,9 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
return (
<div className="w-full">
<AccessTree permissions={formattedPermissions} />
{currentWorkspace.type === ProjectType.SecretManager && (
<AccessTree permissions={formattedPermissions} />
)}
<form
onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
@@ -0,0 +1,173 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = {
position?: number;
isDisabled?: boolean;
};
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.SshHosts;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<p className="mt-2 text-gray-300">Conditions</p>
<p className="text-sm text-mineshaft-400">
Conditions determine when a policy will be applied (always if no conditions are present).
</p>
<p className="mb-3 text-sm leading-4 text-mineshaft-400">
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="hostname">Hostname</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "hostname",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
);
};
@@ -64,7 +64,7 @@ export const SecretApprovalsPage = () => {
<Tabs defaultValue={defaultTab}>
<TabList>
<Tab value={TabSection.SecretApprovalRequests}>
Secret Requests
Change Requests
{Boolean(secretApprovalReqCount?.open) && (
<Badge className="ml-2">{secretApprovalReqCount?.open}</Badge>
)}
@@ -136,6 +136,13 @@ export const ReviewAccessRequestModal = ({
<span className="font-bold">Access Type: </span>
<span>{getAccessLabel()}</span>
</div>
{request.note && (
<div className="mt-1">
<span className="font-bold">User Note: </span>
<span>{request.note}</span>
</div>
)}
</div>
<div className="space-x-2">
@@ -1,77 +0,0 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { motion } from "framer-motion";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { SshCaSection, SshCertificatesSection } from "./components";
enum TabSections {
SshCa = "ssh-certificate-authorities",
SshCertificates = "ssh-certificates"
}
export const OverviewPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Overview"
description="Infisical SSH lets you issue SSH credentials to clients to provide short-lived, secure SSH access to infrastructure."
/>
<Tabs defaultValue={TabSections.SshCertificates}>
<TabList>
<Tab value={TabSections.SshCertificates}>SSH Certificates</Tab>
<Tab value={TabSections.SshCa}>Certificate Authorities</Tab>
</TabList>
<TabPanel value={TabSections.SshCertificates}>
<motion.div
key="panel-ssh-certificate-s"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificates}
renderGuardBanner
passThrough={false}
>
<SshCertificatesSection />
</ProjectPermissionCan>
</motion.div>
</TabPanel>
<TabPanel value={TabSections.SshCa}>
<motion.div
key="panel-ssh-certificate-authorities"
transition={{ duration: 0.15 }}
initial={{ opacity: 0, translateX: 30 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: 30 }}
>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
renderGuardBanner
passThrough={false}
>
<SshCaSection />
</ProjectPermissionCan>
</motion.div>
</TabPanel>
</Tabs>
</div>
</div>
</div>
</>
);
};
@@ -1,198 +0,0 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["sshCa"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
};
const schema = z
.object({
friendlyName: z.string(),
keyAlgorithm: z.enum([
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
])
})
.required();
export type FormData = z.infer<typeof schema>;
export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || "");
const { mutateAsync: createMutateAsync } = useCreateSshCa();
const { mutateAsync: updateMutateAsync } = useUpdateSshCa();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
friendlyName: "",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
}
});
useEffect(() => {
if (ca) {
reset({
friendlyName: ca.friendlyName,
keyAlgorithm: ca.keyAlgorithm
});
} else {
reset({
friendlyName: "",
keyAlgorithm: CertKeyAlgorithm.RSA_2048
});
}
}, [ca]);
const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => {
try {
if (!projectId) return;
if (ca) {
await updateMutateAsync({
caId: ca.id,
friendlyName
});
} else {
const { id: newCaId } = await createMutateAsync({
projectId,
friendlyName,
keyAlgorithm
});
navigate({
to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
params: {
projectId,
caId: newCaId
}
});
}
reset();
handlePopUpToggle("sshCa", false);
createNotification({
text: `Successfully ${ca ? "updated" : "created"} SSH CA`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create SSH CA",
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.sshCa?.isOpen}
onOpenChange={(isOpen) => {
reset();
handlePopUpToggle("sshCa", isOpen);
}}
>
<ModalContent title={`${ca ? "View" : "Create"} SSH CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
{ca && (
<FormControl label="CA ID">
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
defaultValue=""
name="friendlyName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Friendly Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="My SSH CA" />
</FormControl>
)}
/>
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{certKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.sshCa?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("sshCa", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -20,7 +20,7 @@ import { useDeleteSshCa, useGetSshCaById } from "@app/hooks/api";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { SshCaModal } from "../OverviewPage/components/SshCaModal";
import { SshCaModal } from "../SshCasPage/components/SshCaModal";
import { SshCaDetailsSection, SshCertificateTemplatesSection } from "./components";
const Page = () => {
@@ -8,7 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetSshCaById } from "@app/hooks/api";
import { caStatusToNameMap } from "@app/hooks/api/ca/constants";
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
import { sshCertKeyAlgorithmToNameMap } from "@app/hooks/api/sshCa/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
@@ -93,7 +93,9 @@ export const SshCaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Key Algorithm</p>
<p className="text-sm text-mineshaft-300">{certKeyAlgorithmToNameMap[ca.keyAlgorithm]}</p>
<p className="text-sm text-mineshaft-300">
{sshCertKeyAlgorithmToNameMap[ca.keyAlgorithm]}
</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Public Key</p>
@@ -22,9 +22,11 @@ import {
useListWorkspaceSshCertificateTemplates,
useSignSshKey
} from "@app/hooks/api";
import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants";
import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums";
import { SshCertType } from "@app/hooks/api/sshCa/constants";
import {
SshCertKeyAlgorithm,
sshCertKeyAlgorithms,
SshCertType
} from "@app/hooks/api/sshCa/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { SshCertificateContent } from "./SshCertificateContent";
@@ -33,10 +35,11 @@ const schema = z.object({
templateId: z.string(),
publicKey: z.string().optional(),
keyAlgorithm: z.enum([
CertKeyAlgorithm.RSA_2048,
CertKeyAlgorithm.RSA_4096,
CertKeyAlgorithm.ECDSA_P256,
CertKeyAlgorithm.ECDSA_P384
SshCertKeyAlgorithm.RSA_2048,
SshCertKeyAlgorithm.RSA_4096,
SshCertKeyAlgorithm.ECDSA_P256,
SshCertKeyAlgorithm.ECDSA_P384,
SshCertKeyAlgorithm.ED25519
]),
certType: z.nativeEnum(SshCertType),
principals: z.string(),
@@ -95,7 +98,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
keyAlgorithm: CertKeyAlgorithm.RSA_2048,
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
certType: SshCertType.USER
}
});
@@ -282,7 +285,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
<Controller
control={control}
name="keyAlgorithm"
defaultValue={CertKeyAlgorithm.RSA_2048}
defaultValue={SshCertKeyAlgorithm.ED25519}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
@@ -295,7 +298,7 @@ export const SshCertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
onValueChange={(e) => onChange(e)}
className="w-full"
>
{certKeyAlgorithms.map(({ label, value }) => (
{sshCertKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
@@ -60,8 +60,8 @@ const schema = z
allowHostCertificates: z.boolean().optional().default(false),
allowCustomKeyIds: z.boolean().optional().default(false)
})
.refine((data) => ms(data.maxTTL) > ms(data.ttl), {
message: "Max TLL must be greater than TTL",
.refine((data) => ms(data.maxTTL) >= ms(data.ttl), {
message: "Max TLL must be greater than or equal to TTL",
path: ["maxTTL"]
});
@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { SshCaSection } from "./components";
export const SshCasPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "SSH" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="SSH Certificate Authorities"
description="Manage the SSH certificate authorities used to sign user and host certificates, including custom and default CAs."
/>
<SshCaSection />
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1,297 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api";
import {
SshCaKeySource,
SshCertKeyAlgorithm,
sshCertKeyAlgorithms
} from "@app/hooks/api/sshCa/constants";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["sshCa"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void;
};
const sshCaKeySources = [
{ label: "Internal", value: SshCaKeySource.INTERNAL },
{ label: "External", value: SshCaKeySource.EXTERNAL }
];
const schema = z
.object({
friendlyName: z.string(),
keySource: z.nativeEnum(SshCaKeySource),
publicKey: z.string().optional(),
privateKey: z.string().optional(),
keyAlgorithm: z.enum([
SshCertKeyAlgorithm.RSA_2048,
SshCertKeyAlgorithm.RSA_4096,
SshCertKeyAlgorithm.ECDSA_P256,
SshCertKeyAlgorithm.ECDSA_P384,
SshCertKeyAlgorithm.ED25519
])
})
.required();
export type FormData = z.infer<typeof schema>;
export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => {
const navigate = useNavigate();
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || "");
const { mutateAsync: createMutateAsync } = useCreateSshCa();
const { mutateAsync: updateMutateAsync } = useUpdateSshCa();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
friendlyName: "",
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
keySource: SshCaKeySource.INTERNAL,
publicKey: "",
privateKey: ""
}
});
const caKeySource = watch("keySource");
useEffect(() => {
if (ca) {
reset({
friendlyName: ca.friendlyName,
keyAlgorithm: ca.keyAlgorithm,
keySource: ca.keySource,
publicKey: ca.publicKey
});
} else {
reset({
friendlyName: "",
keyAlgorithm: SshCertKeyAlgorithm.ED25519,
keySource: SshCaKeySource.INTERNAL,
publicKey: "",
privateKey: ""
});
}
}, [ca]);
const onFormSubmit = async ({
friendlyName,
keySource,
keyAlgorithm,
publicKey,
privateKey
}: FormData) => {
try {
if (!projectId) return;
if (ca) {
await updateMutateAsync({
caId: ca.id,
friendlyName
});
} else {
const { id: newCaId } = await createMutateAsync({
projectId,
friendlyName,
keySource,
keyAlgorithm,
publicKey,
privateKey
});
navigate({
to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const,
params: {
projectId,
caId: newCaId
}
});
}
reset();
handlePopUpToggle("sshCa", false);
createNotification({
text: `Successfully ${ca ? "updated" : "created"} SSH CA`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${ca ? "update" : "create"} SSH CA`,
type: "error"
});
}
};
return (
<Modal
isOpen={popUp?.sshCa?.isOpen}
onOpenChange={(isOpen) => {
reset();
handlePopUpToggle("sshCa", isOpen);
}}
>
<ModalContent title={`${ca ? "View" : "Create"} SSH CA`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
{ca && (
<FormControl label="CA ID">
<Input value={ca.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
defaultValue=""
name="friendlyName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Friendly Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="My SSH CA" />
</FormControl>
)}
/>
{caKeySource && (
<Controller
control={control}
name="keySource"
defaultValue={SshCaKeySource.INTERNAL}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Source"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{sshCaKeySources.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
{caKeySource === SshCaKeySource.INTERNAL && (
<Controller
control={control}
name="keyAlgorithm"
defaultValue={SshCertKeyAlgorithm.ED25519}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Key Algorithm"
errorText={error?.message}
isError={Boolean(error)}
isRequired={caKeySource === SshCaKeySource.INTERNAL}
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
isDisabled={Boolean(ca)}
>
{sshCertKeyAlgorithms.map(({ label, value }) => (
<SelectItem value={String(value || "")} key={label}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
)}
{caKeySource === SshCaKeySource.EXTERNAL && !ca && (
<>
<Controller
control={control}
defaultValue=""
name="publicKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Public Key"
isError={Boolean(error)}
errorText={error?.message}
isRequired={caKeySource === SshCaKeySource.EXTERNAL}
>
<Input {...field} placeholder="ssh-rsa AAA..." />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="privateKey"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Private Key"
errorText={error?.message}
isError={Boolean(error)}
isRequired={caKeySource === SshCaKeySource.EXTERNAL}
>
<TextArea {...field} placeholder="-----BEGIN OPENSSH PRIVATE KEY----- ..." />
</FormControl>
)}
/>
</>
)}
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.sshCa?.data ? "Update" : "Create"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("sshCa", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { SshCaSection } from "./SshCaSection";
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { SshCasPage } from "./SshCasPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas"
)({
component: SshCasPage
});
@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { SshCertificatesSection } from "./components";
export const SshCertsPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="SSH Certificates"
description="View and audit all issued SSH certificates, including validity and associated access metadata."
/>
<SshCertificatesSection />
</div>
</div>
</div>
</>
);
};
@@ -10,7 +10,7 @@ import { SshCertificateModal } from "../../SshCaByIDPage/components/SshCertifica
import { SshCertificatesTable } from "./SshCertificatesTable";
export const SshCertificatesSection = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["sshCertificate"] as const);
const { popUp, handlePopUpToggle, handlePopUpOpen } = usePopUp(["sshCertificate"] as const);
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
@@ -57,12 +57,12 @@ export const SshCertificatesTable = () => {
</Td>
<Td>
{certificate.notBefore
? format(new Date(certificate.notBefore), "yyyy-MM-dd")
? format(new Date(certificate.notBefore), "yyyy-MM-dd | HH:mm:ss")
: "-"}
</Td>
<Td>
{certificate.notAfter
? format(new Date(certificate.notAfter), "yyyy-MM-dd")
? format(new Date(certificate.notAfter), "yyyy-MM-dd | HH:mm:ss")
: "-"}
</Td>
</Tr>
@@ -1,2 +1 @@
export { SshCaSection } from "./SshCaSection";
export { SshCertificatesSection } from "./SshCertificatesSection";
@@ -0,0 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { SshCertsPage } from "./SshCertsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates"
)({
component: SshCertsPage
});
@@ -0,0 +1,28 @@
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { PageHeader } from "@app/components/v2";
import { SshHostsSection } from "./components";
export const SshHostsPage = () => {
const { t } = useTranslation();
return (
<>
<Helmet>
<title>{t("common.head-title", { title: "SSH" })}</title>
</Helmet>
<div className="h-full bg-bunker-800">
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader
title="Hosts"
description="Manage your SSH hosts, configure access policies, and define login behavior for secure connections."
/>
<SshHostsSection />
</div>
</div>
</div>
</>
);
};
@@ -0,0 +1,411 @@
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faChevronDown, faChevronRight, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import ms from "ms";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
FormLabel,
IconButton,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
useCreateSshHost,
useGetSshHostById,
useGetWorkspaceUsers,
useUpdateSshHost
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["sshHost"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshHost"]>, state?: boolean) => void;
};
const schema = z
.object({
hostname: z.string(),
userCertTtl: z
.string()
.trim()
.refine(
(val) => ms(val) > 0,
"TTL must be a valid time string such as 2 days, 1d, 2h 1y, ..."
)
.default("8h"),
loginMappings: z
.object({
loginUser: z.string().trim().min(1),
allowedPrincipals: z.array(z.string().trim()).default([])
})
.array()
.default([])
})
.required();
export type FormData = z.infer<typeof schema>;
export const SshHostModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || "";
const { data: members = [] } = useGetWorkspaceUsers(projectId);
const [expandedMappings, setExpandedMappings] = useState<Record<number, boolean>>({});
const { data: sshHost } = useGetSshHostById(
(popUp?.sshHost?.data as { sshHostId: string })?.sshHostId || ""
);
const { mutateAsync: createMutateAsync } = useCreateSshHost();
const { mutateAsync: updateMutateAsync } = useUpdateSshHost();
const {
control,
handleSubmit,
reset,
getValues,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
hostname: "",
userCertTtl: "8h",
loginMappings: []
}
});
const loginMappingsFormFields = useFieldArray({
control,
name: "loginMappings"
});
useEffect(() => {
if (sshHost) {
reset({
hostname: sshHost.hostname,
userCertTtl: sshHost.userCertTtl,
loginMappings: sshHost.loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: allowedPrincipals.usernames
}))
});
setExpandedMappings(
Object.fromEntries(sshHost.loginMappings.map((_, index) => [index, false]))
);
} else {
reset({
hostname: "",
userCertTtl: "8h",
loginMappings: []
});
}
}, [sshHost]);
const onFormSubmit = async ({ hostname, userCertTtl, loginMappings }: FormData) => {
try {
if (!projectId) return;
if (sshHost) {
await updateMutateAsync({
sshHostId: sshHost.id,
hostname,
userCertTtl,
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: {
usernames: allowedPrincipals
}
}))
});
} else {
await createMutateAsync({
projectId,
hostname,
userCertTtl,
loginMappings: loginMappings.map(({ loginUser, allowedPrincipals }) => ({
loginUser,
allowedPrincipals: {
usernames: allowedPrincipals
}
}))
});
}
reset();
handlePopUpToggle("sshHost", false);
createNotification({
text: `Successfully ${sshHost ? "updated" : "added"} SSH host`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${sshHost ? "update" : "add"} SSH host`,
type: "error"
});
}
};
const toggleMapping = (index: number) => {
setExpandedMappings((prev) => ({
...prev,
[index]: !prev[index]
}));
};
return (
<Modal
isOpen={popUp?.sshHost?.isOpen}
onOpenChange={(isOpen) => {
reset();
handlePopUpToggle("sshHost", isOpen);
}}
>
<ModalContent title={`${sshHost ? "View" : "Add"} SSH host`}>
<form onSubmit={handleSubmit(onFormSubmit)}>
{sshHost && (
<FormControl label="SSH Host ID">
<Input value={sshHost.id} isDisabled className="bg-white/[0.07]" />
</FormControl>
)}
<Controller
control={control}
defaultValue=""
name="hostname"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Hostname"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="host.example.com" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="userCertTtl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="User Certificate TTL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="8h, 1d, 30m" />
</FormControl>
)}
/>
<div className="mb-4 flex items-center justify-between">
<FormLabel label="Login Mappings" />
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => {
const newIndex = loginMappingsFormFields.fields.length;
loginMappingsFormFields.append({ loginUser: "", allowedPrincipals: [""] });
setExpandedMappings((prev) => ({
...prev,
[newIndex]: true
}));
}}
>
Add Login Mapping
</Button>
</div>
<div className="mb-4 flex flex-col space-y-4">
{loginMappingsFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div
key={metadataFieldId}
className="flex flex-col space-y-2 rounded-md border border-mineshaft-600 p-4"
>
<div className="mb-2 flex items-center justify-between">
<button
type="button"
className="flex cursor-pointer items-center py-1 text-sm text-mineshaft-200"
onClick={() => toggleMapping(i)}
>
<FontAwesomeIcon
icon={expandedMappings[i] ? faChevronDown : faChevronRight}
className="mr-4"
size="sm"
/>
<Controller
control={control}
name={`loginMappings.${i}.loginUser`}
render={({ field }) => (
<span className="text-sm font-medium leading-tight">
{field.value || "New Login Mapping"}
</span>
)}
/>
</button>
<IconButton
ariaLabel="delete login mapping"
variant="plain"
onClick={() => loginMappingsFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
{expandedMappings[i] && (
<>
<div>
<span className="text-xs text-mineshaft-400">Login User</span>
<Controller
control={control}
name={`loginMappings.${i}.loginUser`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input
{...field}
placeholder="ec2-user"
onChange={(e) => {
const newValue = e.target.value;
const loginMappings = getValues("loginMappings");
const isDuplicate = loginMappings.some(
(mapping, index) => index !== i && mapping.loginUser === newValue
);
if (isDuplicate) {
createNotification({
text: "This login user already exists",
type: "error"
});
return;
}
field.onChange(e);
}}
/>
</FormControl>
)}
/>
</div>
<div className="flex flex-col space-y-2">
<div className="mb-2 mt-4 flex items-center justify-between">
<FormLabel
label="Allowed Principals"
className="text-xs text-mineshaft-400"
/>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => {
const current = getValues(`loginMappings.${i}.allowedPrincipals`) ?? [];
setValue(`loginMappings.${i}.allowedPrincipals`, [...current, ""]);
}}
>
Add Principal
</Button>
</div>
<Controller
control={control}
name={`loginMappings.${i}.allowedPrincipals`}
render={({ field: { value = [], onChange }, fieldState: { error } }) => (
<div className="flex flex-col space-y-2">
{(value.length === 0 ? [""] : value).map(
(principal: string, principalIndex: number) => (
<div
key={`${metadataFieldId}-principal-${principal}`}
className="flex items-center space-x-2"
>
<div className="flex-1">
<Select
value={principal}
onValueChange={(newValue) => {
if (value.includes(newValue)) {
createNotification({
text: "This principal is already added",
type: "error"
});
return;
}
const newPrincipals = [...value];
newPrincipals[principalIndex] = newValue;
onChange(newPrincipals);
}}
placeholder="Select a member"
className="w-full"
>
{members.map((member) => (
<SelectItem
key={member.user.id}
value={member.user.username}
>
{member.user.username}
</SelectItem>
))}
</Select>
</div>
<IconButton
size="sm"
ariaLabel="delete principal"
variant="plain"
className="h-9"
onClick={() => {
const newPrincipals = value.filter(
(_, idx) => idx !== principalIndex
);
onChange(newPrincipals);
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
)
)}
{error && <span className="text-sm text-red-500">{error.message}</span>}
</div>
)}
/>
</div>
</>
)}
</div>
))}
</div>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{popUp?.sshHost?.data ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("sshHost", false)}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,91 @@
import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, DeleteActionModal } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDeleteSshHost } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { SshHostModal } from "./SshHostModal";
import { SshHostsTable } from "./SshHostsTable";
export const SshHostsSection = () => {
const { mutateAsync: deleteSshHost } = useDeleteSshHost();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"sshHost",
"deleteSshHost"
] as const);
const onRemoveSshHostSubmit = async (sshHostId: string) => {
try {
const host = await deleteSshHost({ sshHostId });
createNotification({
text: `Successfully deleted SSH host: ${host.hostname}`,
type: "success"
});
handlePopUpClose("deleteSshHost");
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete SSH host",
type: "error"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Hosts</p>
<div className="flex w-full justify-end">
<a
target="_blank"
rel="noopener noreferrer"
href="https://infisical.com/docs/documentation/platform/ssh"
>
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
Documentation{" "}
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.06rem] ml-1 text-xs"
/>
</span>
</a>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.SshHosts}
>
{(isAllowed) => (
<Button
colorSchema="primary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => handlePopUpOpen("sshHost")}
isDisabled={!isAllowed}
className="ml-4"
>
Add Host
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
<SshHostsTable handlePopUpOpen={handlePopUpOpen} />
<SshHostModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<DeleteActionModal
isOpen={popUp.deleteSshHost.isOpen}
title="Are you sure you want to remove the SSH host?"
onChange={(isOpen) => handlePopUpToggle("deleteSshHost", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onRemoveSshHostSubmit((popUp?.deleteSshHost?.data as { sshHostId: string })?.sshHostId)
}
/>
</div>
);
};
@@ -0,0 +1,180 @@
import {
faDownload,
faEllipsis,
faPencil,
faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import FileSaver from "file-saver";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { fetchSshHostUserCaPublicKey, useListWorkspaceSshHosts } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteSshHost", "sshHost"]>,
data?: object
) => void;
};
export const SshHostsTable = ({ handlePopUpOpen }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data, isPending } = useListWorkspaceSshHosts(currentWorkspace?.id || "");
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
const handleDownloadUserCaKey = async (sshHostId: string) => {
try {
const publicKey = await fetchSshHostUserCaPublicKey(sshHostId);
downloadTxtFile("infisical_user_ca.pub", publicKey);
} catch (err) {
console.error("Failed to download User CA public key", err);
createNotification({
type: "error",
text: "Failed to download User CA public key"
});
}
};
return (
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Hostname</Th>
<Th>Login User - Authorized Principals Mapping</Th>
<Th />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="org-ssh-cas" />}
{!isPending &&
data &&
data.length > 0 &&
data.map((host) => {
return (
<Tr
// className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
className="h-10"
key={`ssh-host-${host.id}`}
>
<Td>{host.hostname}</Td>
<Td>
{host.loginMappings.length === 0 ? (
<span className="italic text-mineshaft-400">None</span>
) : (
host.loginMappings.map(({ loginUser, allowedPrincipals }) => (
<div key={`${host.id}-${loginUser}`} className="mb-2">
<div className="text-mineshaft-200">{loginUser}</div>
{allowedPrincipals.usernames.map((username) => (
<div key={`${host.id}-${loginUser}-${username}`} className="ml-4">
└─ {username}
</div>
))}
</div>
))
)}
</Td>
<Td className="text-right align-middle">
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="lg" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={async (e) => {
e.stopPropagation();
handleDownloadUserCaKey(host.id);
}}
icon={<FontAwesomeIcon icon={faDownload} />}
>
Download User CA Public Key
</DropdownMenuItem>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.SshHosts}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("sshHost", {
sshHostId: host.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faPencil} />}
>
Edit SSH host
</DropdownMenuItem>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={ProjectPermissionSub.SshHosts}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteSshHost", {
sshHostId: host.id
});
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete SSH host
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isPending && data?.length === 0 && (
<EmptyState title="No SSH hosts have been added" icon={faServer} />
)}
</TableContainer>
</div>
);
};
@@ -0,0 +1 @@
export { SshHostsSection } from "./SshHostsSection";
@@ -1,9 +1,9 @@
import { createFileRoute } from "@tanstack/react-router";
import { OverviewPage } from "./OverviewPage";
import { SshHostsPage } from "./SshHostsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview"
)({
component: OverviewPage
component: SshHostsPage
});
+63 -9
View File
@@ -78,7 +78,9 @@ import { Route as secretManagerIntegrationsRouteAzureKeyVaultOauthRedirectImport
import { Route as secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectImport } from './pages/secret-manager/integrations/route-azure-app-configurations-oauth-redirect'
import { Route as projectAccessControlPageRouteCertManagerImport } from './pages/project/AccessControlPage/route-cert-manager'
import { Route as sshSettingsPageRouteImport } from './pages/ssh/SettingsPage/route'
import { Route as sshOverviewPageRouteImport } from './pages/ssh/OverviewPage/route'
import { Route as sshSshHostsPageRouteImport } from './pages/ssh/SshHostsPage/route'
import { Route as sshSshCertsPageRouteImport } from './pages/ssh/SshCertsPage/route'
import { Route as sshSshCasPageRouteImport } from './pages/ssh/SshCasPage/route'
import { Route as secretManagerSettingsPageRouteImport } from './pages/secret-manager/SettingsPage/route'
import { Route as secretManagerSecretRotationPageRouteImport } from './pages/secret-manager/SecretRotationPage/route'
import { Route as secretManagerOverviewPageRouteImport } from './pages/secret-manager/OverviewPage/route'
@@ -800,12 +802,24 @@ const sshSettingsPageRouteRoute = sshSettingsPageRouteImport.update({
getParentRoute: () => sshLayoutRoute,
} as any)
const sshOverviewPageRouteRoute = sshOverviewPageRouteImport.update({
const sshSshHostsPageRouteRoute = sshSshHostsPageRouteImport.update({
id: '/overview',
path: '/overview',
getParentRoute: () => sshLayoutRoute,
} as any)
const sshSshCertsPageRouteRoute = sshSshCertsPageRouteImport.update({
id: '/certificates',
path: '/certificates',
getParentRoute: () => sshLayoutRoute,
} as any)
const sshSshCasPageRouteRoute = sshSshCasPageRouteImport.update({
id: '/cas',
path: '/cas',
getParentRoute: () => sshLayoutRoute,
} as any)
const secretManagerSettingsPageRouteRoute =
secretManagerSettingsPageRouteImport.update({
id: '/settings',
@@ -2146,11 +2160,25 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof secretManagerSettingsPageRouteImport
parentRoute: typeof secretManagerLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas': {
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas'
path: '/cas'
fullPath: '/ssh/$projectId/cas'
preLoaderRoute: typeof sshSshCasPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates': {
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates'
path: '/certificates'
fullPath: '/ssh/$projectId/certificates'
preLoaderRoute: typeof sshSshCertsPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': {
id: '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview'
path: '/overview'
fullPath: '/ssh/$projectId/overview'
preLoaderRoute: typeof sshOverviewPageRouteImport
preLoaderRoute: typeof sshSshHostsPageRouteImport
parentRoute: typeof sshLayoutImport
}
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': {
@@ -3463,7 +3491,9 @@ const AuthenticateInjectOrgDetailsOrgLayoutSecretManagerProjectIdRouteWithChildr
)
interface sshLayoutRouteChildren {
sshOverviewPageRouteRoute: typeof sshOverviewPageRouteRoute
sshSshCasPageRouteRoute: typeof sshSshCasPageRouteRoute
sshSshCertsPageRouteRoute: typeof sshSshCertsPageRouteRoute
sshSshHostsPageRouteRoute: typeof sshSshHostsPageRouteRoute
sshSettingsPageRouteRoute: typeof sshSettingsPageRouteRoute
projectAccessControlPageRouteSshRoute: typeof projectAccessControlPageRouteSshRoute
sshSshCaByIDPageRouteRoute: typeof sshSshCaByIDPageRouteRoute
@@ -3473,7 +3503,9 @@ interface sshLayoutRouteChildren {
}
const sshLayoutRouteChildren: sshLayoutRouteChildren = {
sshOverviewPageRouteRoute: sshOverviewPageRouteRoute,
sshSshCasPageRouteRoute: sshSshCasPageRouteRoute,
sshSshCertsPageRouteRoute: sshSshCertsPageRouteRoute,
sshSshHostsPageRouteRoute: sshSshHostsPageRouteRoute,
sshSettingsPageRouteRoute: sshSettingsPageRouteRoute,
projectAccessControlPageRouteSshRoute: projectAccessControlPageRouteSshRoute,
sshSshCaByIDPageRouteRoute: sshSshCaByIDPageRouteRoute,
@@ -3755,7 +3787,9 @@ export interface FileRoutesByFullPath {
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute
'/ssh/$projectId/cas': typeof sshSshCasPageRouteRoute
'/ssh/$projectId/certificates': typeof sshSshCertsPageRouteRoute
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
@@ -3927,7 +3961,9 @@ export interface FileRoutesByTo {
'/secret-manager/$projectId/overview': typeof secretManagerOverviewPageRouteRoute
'/secret-manager/$projectId/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/secret-manager/$projectId/settings': typeof secretManagerSettingsPageRouteRoute
'/ssh/$projectId/overview': typeof sshOverviewPageRouteRoute
'/ssh/$projectId/cas': typeof sshSshCasPageRouteRoute
'/ssh/$projectId/certificates': typeof sshSshCertsPageRouteRoute
'/ssh/$projectId/overview': typeof sshSshHostsPageRouteRoute
'/ssh/$projectId/settings': typeof sshSettingsPageRouteRoute
'/cert-manager/$projectId/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
@@ -4115,7 +4151,9 @@ export interface FileRoutesById {
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview': typeof secretManagerOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secret-rotation': typeof secretManagerSecretRotationPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/settings': typeof secretManagerSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshOverviewPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas': typeof sshSshCasPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates': typeof sshSshCertsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview': typeof sshSshHostsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings': typeof sshSettingsPageRouteRoute
'/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management': typeof projectAccessControlPageRouteCertManagerRoute
'/_authenticate/_inject-org-details/_org-layout/integrations/azure-app-configuration/oauth2/callback': typeof secretManagerIntegrationsRouteAzureAppConfigurationsOauthRedirectRoute
@@ -4296,6 +4334,8 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/overview'
| '/secret-manager/$projectId/secret-rotation'
| '/secret-manager/$projectId/settings'
| '/ssh/$projectId/cas'
| '/ssh/$projectId/certificates'
| '/ssh/$projectId/overview'
| '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management'
@@ -4467,6 +4507,8 @@ export interface FileRouteTypes {
| '/secret-manager/$projectId/overview'
| '/secret-manager/$projectId/secret-rotation'
| '/secret-manager/$projectId/settings'
| '/ssh/$projectId/cas'
| '/ssh/$projectId/certificates'
| '/ssh/$projectId/overview'
| '/ssh/$projectId/settings'
| '/cert-manager/$projectId/access-management'
@@ -4653,6 +4695,8 @@ export interface FileRouteTypes {
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secret-rotation'
| '/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview'
| '/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings'
| '/_authenticate/_inject-org-details/_org-layout/cert-manager/$projectId/_cert-manager-layout/access-management'
@@ -5195,6 +5239,8 @@ export const routeTree = rootRoute
"filePath": "ssh/layout.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId",
"children": [
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings",
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/access-management",
@@ -5244,8 +5290,16 @@ export const routeTree = rootRoute
"filePath": "secret-manager/SettingsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout"
},
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/cas": {
"filePath": "ssh/SshCasPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/certificates": {
"filePath": "ssh/SshCertsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/overview": {
"filePath": "ssh/OverviewPage/route.tsx",
"filePath": "ssh/SshHostsPage/route.tsx",
"parent": "/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout"
},
"/_authenticate/_inject-org-details/_org-layout/ssh/$projectId/_ssh-layout/settings": {
+3 -1
View File
@@ -306,7 +306,9 @@ const kmsRoutes = route("/kms/$projectId", [
const sshRoutes = route("/ssh/$projectId", [
layout("ssh-layout", "ssh/layout.tsx", [
route("/overview", "ssh/OverviewPage/route.tsx"),
route("/overview", "ssh/SshHostsPage/route.tsx"),
route("/certificates", "ssh/SshCertsPage/route.tsx"),
route("/cas", "ssh/SshCasPage/route.tsx"),
route("/ca/$caId", "ssh/SshCaByIDPage/route.tsx"),
route("/settings", "ssh/SettingsPage/route.tsx"),
route("/access-management", "project/AccessControlPage/route-ssh.tsx"),