mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-29 15:09:07 +01:00
Add mtls to blueprint, policy, and oss getTraefikConfig
This commit is contained in:
@@ -14,12 +14,11 @@ import {
|
||||
type SelectedRole
|
||||
} from "@app/components/roles-selector";
|
||||
import { UsersSelector } from "@app/components/users-selector";
|
||||
import { ResourceMtlsCertificatesCredenza } from "@app/components/ResourceMtlsCertificatesCredenza";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Form, FormField } from "@app/components/ui/form";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useResourceMtls } from "@app/hooks/useResourceMtls";
|
||||
import { usePolicyMtls } from "@app/hooks/usePolicyMtls";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { resourceQueries } from "@app/lib/queries";
|
||||
@@ -33,13 +32,7 @@ import type { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toUnicode } from "punycode";
|
||||
import {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useForm, useWatch } from "react-hook-form";
|
||||
import { createPolicySchema } from ".";
|
||||
import {
|
||||
@@ -50,6 +43,7 @@ import {
|
||||
} from "./PolicyAuthMethodCredenzas";
|
||||
import { PolicyAuthOtherMethodsSection } from "./PolicyAuthOtherMethodsSection";
|
||||
import { PolicyAuthSsoSection } from "./PolicyAuthSsoSection";
|
||||
import { PolicyMtlsCertificatesCredenza } from "./PolicyMtlsCertificatesCredenza";
|
||||
import type { PolicyAuthMethodId } from "./policy-auth-method-id";
|
||||
import { SharedPolicyResourceNotice } from "./SharedPolicyResourceNotice";
|
||||
import z from "zod";
|
||||
@@ -259,9 +253,7 @@ export function PolicyAuthStackSectionEdit({
|
||||
const [editingMethod, setEditingMethod] =
|
||||
useState<PolicyAuthMethodId | null>(null);
|
||||
|
||||
const mtls = useResourceMtls();
|
||||
// mTLS is resource-level, so it stays editable when the policy is shared.
|
||||
const mtlsReadonly = readonly;
|
||||
const mtls = usePolicyMtls();
|
||||
const canSaveMtls =
|
||||
mtls.isDirty && !(mtls.enabled && !mtls.hasCertificates);
|
||||
|
||||
@@ -513,8 +505,7 @@ export function PolicyAuthStackSectionEdit({
|
||||
}),
|
||||
api.post(`/resource/${resourceId}/users`, {
|
||||
userIds: currentResourceUserIds
|
||||
}),
|
||||
...(canSaveMtls ? [mtls.save()] : [])
|
||||
})
|
||||
]);
|
||||
|
||||
toast({
|
||||
@@ -725,7 +716,7 @@ export function PolicyAuthStackSectionEdit({
|
||||
certificateCount:
|
||||
mtls.certificates.length,
|
||||
sslEnabled: mtls.sslEnabled,
|
||||
disabled: mtlsReadonly,
|
||||
disabled: authReadonly,
|
||||
onConfigure: () =>
|
||||
openMethodEditor("mtls"),
|
||||
onToggle: (active) =>
|
||||
@@ -824,7 +815,7 @@ export function PolicyAuthStackSectionEdit({
|
||||
/>
|
||||
|
||||
{mtls.supported && (
|
||||
<ResourceMtlsCertificatesCredenza
|
||||
<PolicyMtlsCertificatesCredenza
|
||||
open={editingMethod === "mtls"}
|
||||
onOpenChange={(open) => {
|
||||
if (open) return;
|
||||
@@ -834,8 +825,14 @@ export function PolicyAuthStackSectionEdit({
|
||||
mtls.setEnabled(false);
|
||||
}
|
||||
}}
|
||||
resourcePolicyId={
|
||||
policy.resourcePolicyId
|
||||
}
|
||||
certificates={mtls.certificates}
|
||||
isLoading={mtls.isLoadingCertificates}
|
||||
onMtlsDisabled={() =>
|
||||
updatePolicy({ mtlsEnabled: false })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
+25
-25
@@ -16,15 +16,14 @@ import { Input } from "@app/components/ui/input";
|
||||
import { Label } from "@app/components/ui/label";
|
||||
import { Textarea } from "@app/components/ui/textarea";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { resourceQueries } from "@app/lib/queries";
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import type {
|
||||
CreateResourceMtlsCertificateResponse,
|
||||
DeleteResourceMtlsCertificateResponse,
|
||||
ListResourceMtlsCertificatesResponse
|
||||
} from "@server/routers/resource";
|
||||
CreateResourcePolicyMtlsCertificateResponse,
|
||||
DeleteResourcePolicyMtlsCertificateResponse,
|
||||
ListResourcePolicyMtlsCertificatesResponse
|
||||
} from "@server/routers/policy";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { FileKey2, Trash2 } from "lucide-react";
|
||||
@@ -34,22 +33,26 @@ import { useRef, useState } from "react";
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const EXPIRES_SOON_MS = 30 * DAY_MS;
|
||||
|
||||
type ResourceMtlsCertificatesCredenzaProps = {
|
||||
type PolicyMtlsCertificatesCredenzaProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
certificates: ListResourceMtlsCertificatesResponse["certificates"];
|
||||
resourcePolicyId: number;
|
||||
certificates: ListResourcePolicyMtlsCertificatesResponse["certificates"];
|
||||
isLoading: boolean;
|
||||
// called when removing the last CA certificate switched mTLS off
|
||||
onMtlsDisabled: () => void;
|
||||
};
|
||||
|
||||
export function ResourceMtlsCertificatesCredenza({
|
||||
export function PolicyMtlsCertificatesCredenza({
|
||||
open,
|
||||
onOpenChange,
|
||||
resourcePolicyId,
|
||||
certificates,
|
||||
isLoading
|
||||
}: ResourceMtlsCertificatesCredenzaProps) {
|
||||
isLoading,
|
||||
onMtlsDisabled
|
||||
}: PolicyMtlsCertificatesCredenzaProps) {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { resource, updateResource } = useResourceContext();
|
||||
const api = createApiClient({ env });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -61,9 +64,7 @@ export function ResourceMtlsCertificatesCredenza({
|
||||
|
||||
async function invalidateCertificates() {
|
||||
await queryClient.invalidateQueries(
|
||||
resourceQueries.resourceMtlsCertificates({
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
orgQueries.resourcePolicyMtlsCertificates({ resourcePolicyId })
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,13 +83,12 @@ export function ResourceMtlsCertificatesCredenza({
|
||||
if (!pem.trim()) return;
|
||||
setAdding(true);
|
||||
try {
|
||||
await api.put<AxiosResponse<CreateResourceMtlsCertificateResponse>>(
|
||||
`/resource/${resource.resourceId}/mtls/certificate`,
|
||||
{
|
||||
certificate: pem,
|
||||
...(name.trim() ? { name: name.trim() } : {})
|
||||
}
|
||||
);
|
||||
await api.put<
|
||||
AxiosResponse<CreateResourcePolicyMtlsCertificateResponse>
|
||||
>(`/resource-policy/${resourcePolicyId}/mtls/certificate`, {
|
||||
certificate: pem,
|
||||
...(name.trim() ? { name: name.trim() } : {})
|
||||
});
|
||||
setPem("");
|
||||
setName("");
|
||||
await invalidateCertificates();
|
||||
@@ -114,14 +114,14 @@ export function ResourceMtlsCertificatesCredenza({
|
||||
setRemovingId(mtlsCertificateId);
|
||||
try {
|
||||
const res = await api.delete<
|
||||
AxiosResponse<DeleteResourceMtlsCertificateResponse>
|
||||
AxiosResponse<DeleteResourcePolicyMtlsCertificateResponse>
|
||||
>(
|
||||
`/resource/${resource.resourceId}/mtls/certificate/${mtlsCertificateId}`
|
||||
`/resource-policy/${resourcePolicyId}/mtls/certificate/${mtlsCertificateId}`
|
||||
);
|
||||
await invalidateCertificates();
|
||||
|
||||
if (res.data.data.mtlsDisabled) {
|
||||
updateResource({ mtlsEnabled: false });
|
||||
onMtlsDisabled();
|
||||
toast({
|
||||
title: t("mtlsCertificateRemoved"),
|
||||
description: t("mtlsLastCertificateRemoved")
|
||||
@@ -1,56 +1,58 @@
|
||||
import ResourceContext from "@app/contexts/resourceContext";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { resourceQueries } from "@app/lib/queries";
|
||||
import type { UpdateResourceResponse } from "@server/routers/resource";
|
||||
import { orgQueries } from "@app/lib/queries";
|
||||
import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
// mTLS is a resource-level setting, so it is only available when editing
|
||||
// authentication in the context of an HTTP resource.
|
||||
export function useResourceMtls() {
|
||||
// mTLS is set on the policy being edited, like the other auth methods. When
|
||||
// editing in the context of a resource it only applies to HTTP resources.
|
||||
export function usePolicyMtls() {
|
||||
const { policy, updatePolicy } = useResourcePolicyContext();
|
||||
const resourceContext = useContext(ResourceContext);
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const resource = resourceContext?.resource;
|
||||
|
||||
const supported =
|
||||
!!resource &&
|
||||
!env.flags.disableEnterpriseFeatures &&
|
||||
resource.mode === "http";
|
||||
(!resource || resource.mode === "http");
|
||||
|
||||
const { data: certificates = [], isLoading: isLoadingCertificates } =
|
||||
useQuery({
|
||||
...resourceQueries.resourceMtlsCertificates({
|
||||
resourceId: resource?.resourceId ?? 0
|
||||
...orgQueries.resourcePolicyMtlsCertificates({
|
||||
resourcePolicyId: policy.resourcePolicyId
|
||||
}),
|
||||
enabled: supported
|
||||
});
|
||||
|
||||
const [enabled, setEnabled] = useState(Boolean(resource?.mtlsEnabled));
|
||||
const [enabled, setEnabled] = useState(policy.mtlsEnabled);
|
||||
|
||||
// The server switches mTLS off when the last CA is removed.
|
||||
useEffect(() => {
|
||||
setEnabled(Boolean(resource?.mtlsEnabled));
|
||||
}, [resource?.mtlsEnabled]);
|
||||
setEnabled(policy.mtlsEnabled);
|
||||
}, [policy.mtlsEnabled]);
|
||||
|
||||
const hasCertificates = certificates.length > 0;
|
||||
const isDirty = supported && enabled !== Boolean(resource?.mtlsEnabled);
|
||||
const isDirty = supported && enabled !== policy.mtlsEnabled;
|
||||
|
||||
async function save(): Promise<AxiosResponse | void> {
|
||||
if (!supported || !isDirty) return;
|
||||
const res = await api.post<AxiosResponse<UpdateResourceResponse>>(
|
||||
`/resource/${resource!.resourceId}`,
|
||||
const res = await api.put(
|
||||
`/resource-policy/${policy.resourcePolicyId}`,
|
||||
{ mtlsEnabled: enabled }
|
||||
);
|
||||
resourceContext!.updateResource({ mtlsEnabled: enabled });
|
||||
updatePolicy({ mtlsEnabled: enabled });
|
||||
return res;
|
||||
}
|
||||
|
||||
return {
|
||||
supported,
|
||||
sslEnabled: Boolean(resource?.ssl),
|
||||
// A policy page has no single resource; there it applies to the
|
||||
// policy's resources that have SSL enabled.
|
||||
sslEnabled: resource ? Boolean(resource.ssl) : true,
|
||||
enabled,
|
||||
setEnabled,
|
||||
certificates,
|
||||
+25
-13
@@ -55,13 +55,15 @@ import type {
|
||||
ListLauncherSitesResponse,
|
||||
ListLauncherViewsResponse
|
||||
} from "@server/routers/launcher/types";
|
||||
import type { GetResourcePolicyResponse } from "@server/routers/policy";
|
||||
import type {
|
||||
GetResourcePolicyResponse,
|
||||
ListResourcePolicyMtlsCertificatesResponse
|
||||
} from "@server/routers/policy";
|
||||
import type { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||
import type {
|
||||
GetResourcePoliciesResponse,
|
||||
GetResourceWhitelistResponse,
|
||||
ListResourceAiModelsResponse,
|
||||
ListResourceMtlsCertificatesResponse,
|
||||
ListResourceNamesResponse,
|
||||
ListResourceRolesResponse,
|
||||
ListResourceRulesResponse,
|
||||
@@ -927,6 +929,27 @@ export const orgQueries = {
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
}),
|
||||
resourcePolicyMtlsCertificates: ({
|
||||
resourcePolicyId
|
||||
}: {
|
||||
resourcePolicyId: number;
|
||||
}) =>
|
||||
queryOptions({
|
||||
queryKey: [
|
||||
"RESOURCE_POLICY",
|
||||
resourcePolicyId,
|
||||
"MTLS_CERTIFICATES"
|
||||
] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListResourcePolicyMtlsCertificatesResponse>
|
||||
>(`/resource-policy/${resourcePolicyId}/mtls/certificates`, {
|
||||
signal
|
||||
});
|
||||
|
||||
return res.data.data.certificates;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
@@ -1657,17 +1680,6 @@ export const resourceQueries = {
|
||||
return res.data.data.whitelist;
|
||||
}
|
||||
}),
|
||||
resourceMtlsCertificates: ({ resourceId }: { resourceId: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES", resourceId, "MTLS_CERTIFICATES"] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListResourceMtlsCertificatesResponse>
|
||||
>(`/resource/${resourceId}/mtls/certificates`, { signal });
|
||||
|
||||
return res.data.data.certificates;
|
||||
}
|
||||
}),
|
||||
policies: ({ resourceId }: { resourceId: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES", resourceId, "POLICIES"] as const,
|
||||
|
||||
Reference in New Issue
Block a user