feat: add custom TLS options for bucket replication (#164)

This commit is contained in:
cxymds
2026-07-14 12:28:23 +08:00
committed by GitHub
parent 85f5465088
commit e2bf0cee64
17 changed files with 255 additions and 2 deletions
+99 -2
View File
@@ -7,6 +7,7 @@ import { RiAddLine, RiDeleteBinLine } from "@remixicon/react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Field, FieldContent, FieldError, FieldLabel } from "@/components/ui/field"
@@ -14,6 +15,7 @@ import { useBucket } from "@/hooks/use-bucket"
import { useMessage } from "@/lib/feedback/message"
import { getBytes, randomUUID } from "@/lib/functions"
import { isMissingBucketConfiguration, normalizeReplicationRulesForRolelessConfig } from "@/lib/bucket-configuration"
import { buildBucketReplicationTlsPayload, type BucketReplicationTlsMode } from "@/lib/bucket-replication-tls"
interface ReplicationNewFormProps {
open: boolean
@@ -39,6 +41,8 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
const [level, setLevel] = useState("1")
const [endpoint, setEndpoint] = useState("")
const [tls, setTls] = useState(false)
const [tlsMode, setTlsMode] = useState<BucketReplicationTlsMode>("verify")
const [caCertPem, setCaCertPem] = useState("")
const [accessKey, setAccessKey] = useState("")
const [secretKey, setSecretKey] = useState("")
const [bucket, setBucket] = useState("")
@@ -61,6 +65,7 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
accessKey?: string
secretKey?: string
timecheck?: string
caCertPem?: string
}>({})
const modeOptions = useMemo(
@@ -84,6 +89,8 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
setLevel("1")
setEndpoint("")
setTls(false)
setTlsMode("verify")
setCaCertPem("")
setAccessKey("")
setSecretKey("")
setBucket("")
@@ -134,6 +141,9 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
if (modeType === "async" && Number(timecheck) < 1) {
errors.timecheck = t("Please enter valid health check interval")
}
if (tls && tlsMode === "custom-ca" && !caCertPem.trim()) {
errors.caCertPem = t("Custom CA certificate is required")
}
setFieldErrors(errors)
const firstErrorId = errors.endpoint
? "replication-endpoint"
@@ -145,7 +155,9 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
? "replication-secret-key"
: errors.timecheck
? "replication-health-check-interval"
: null
: errors.caCertPem
? "replication-ca-certificate"
: null
if (firstErrorId) document.getElementById(firstErrorId)?.focus()
return !firstErrorId
}
@@ -161,6 +173,7 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
setSaveError("")
let remoteTargetSaved = false
try {
const tlsConfig = buildBucketReplicationTlsPayload(tls, tlsMode, caCertPem)
const config: Record<string, unknown> = {
sourcebucket: bucketName,
endpoint,
@@ -171,6 +184,8 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
},
targetbucket: bucket,
secure: tls,
skipTlsVerify: tlsConfig.skipTlsVerify,
caCertPem: tlsConfig.caCertPem,
region,
path: "auto",
api: "s3v4",
@@ -566,9 +581,91 @@ export function ReplicationNewForm({ open, onOpenChange, bucketName, onSuccess }
{t("Enable secure transport when connecting to endpoint.")}
</p>
</div>
<Switch id="replication-use-tls" name="replication-use-tls" checked={tls} onCheckedChange={setTls} />
<Switch
id="replication-use-tls"
name="replication-use-tls"
checked={tls}
disabled={submitting}
onCheckedChange={(checked) => {
setTls(checked)
if (!checked) {
setTlsMode("verify")
setCaCertPem("")
setFieldErrors((current) => ({ ...current, caCertPem: undefined }))
}
}}
/>
</div>
{tls ? (
<div className="space-y-3 border-s-2 border-border ps-4">
<Field>
<FieldLabel htmlFor="replication-tls-verification">{t("TLS Verification")}</FieldLabel>
<FieldContent>
<Select
value={tlsMode}
onValueChange={(value) => {
if (value) setTlsMode(value as BucketReplicationTlsMode)
setFieldErrors((current) => ({ ...current, caCertPem: undefined }))
}}
disabled={submitting}
>
<SelectTrigger id="replication-tls-verification" className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="verify">{t("Default certificate verification")}</SelectItem>
<SelectItem value="custom-ca">{t("Custom CA certificate")}</SelectItem>
<SelectItem value="skip">{t("Skip TLS verification")}</SelectItem>
</SelectContent>
</Select>
</FieldContent>
</Field>
{tlsMode === "custom-ca" ? (
<Field>
<FieldLabel htmlFor="replication-ca-certificate">{t("Custom CA certificate")}</FieldLabel>
<FieldContent>
<Textarea
id="replication-ca-certificate"
name="replication-ca-certificate"
value={caCertPem}
onChange={(event) => {
setCaCertPem(event.target.value)
if (event.target.value.trim()) {
setFieldErrors((current) => ({ ...current, caCertPem: undefined }))
}
}}
aria-invalid={Boolean(fieldErrors.caCertPem)}
aria-describedby={
fieldErrors.caCertPem
? "replication-ca-certificate-error"
: "replication-ca-certificate-description"
}
className="min-h-32 font-mono"
placeholder="-----BEGIN CERTIFICATE-----"
disabled={submitting}
spellCheck={false}
/>
</FieldContent>
<p id="replication-ca-certificate-description" className="text-xs text-muted-foreground">
{t("Paste the CA certificate in PEM format.")}
</p>
<FieldError id="replication-ca-certificate-error">{fieldErrors.caCertPem}</FieldError>
</Field>
) : null}
{tlsMode === "skip" ? (
<p
role="alert"
className="border border-destructive/40 bg-destructive/5 p-3 text-xs text-destructive"
>
{t("Certificate verification is disabled. Only use this for trusted networks.")}
</p>
) : null}
</div>
) : null}
<div className="flex items-center justify-between">
<div>
<label htmlFor="replication-existing-object" className="text-sm font-medium">
+6
View File
@@ -1061,6 +1061,12 @@
"Size": "الحجم",
"Skip": "تخطي",
"Skip TLS verification": "تخطي التحقق من TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "تخطي إلى المحتوى الرئيسي",
"Sort by": "ترتيب حسب",
"Source": "المصدر",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Größe",
"Skip": "Überspringen",
"Skip TLS verification": "TLS-Überprüfung überspringen",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Zum Hauptinhalt springen",
"Sort by": "Sortieren nach",
"Source": "Quelle",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Size",
"Skip": "Skip",
"Skip TLS verification": "Skip TLS verification",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Skip to main content",
"Sort by": "Sort by",
"Source": "Source",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Tamaño",
"Skip": "Omitir",
"Skip TLS verification": "Omitir verificación TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Saltar al contenido principal",
"Sort by": "Ordenar por",
"Source": "Origen",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Taille",
"Skip": "Ignorer",
"Skip TLS verification": "Ignorer la vérification TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Aller au contenu principal",
"Sort by": "Trier par",
"Source": "Source",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Ukuran",
"Skip": "Lewati",
"Skip TLS verification": "Lewati verifikasi TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Lewati ke konten utama",
"Sort by": "Urutkan berdasarkan",
"Source": "Sumber",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Dimensione",
"Skip": "Salta",
"Skip TLS verification": "Salta verifica TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Vai al contenuto principale",
"Sort by": "Ordina per",
"Source": "Sorgente",
+6
View File
@@ -1061,6 +1061,12 @@
"Size": "サイズ",
"Skip": "スキップ",
"Skip TLS verification": "TLS 検証をスキップ",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "メインコンテンツへスキップ",
"Sort by": "並び替え",
"Source": "ソース",
+6
View File
@@ -1061,6 +1061,12 @@
"Size": "크기",
"Skip": "건너뛰기",
"Skip TLS verification": "TLS 검증 건너뛰기",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "기본 콘텐츠로 건너뛰기",
"Sort by": "정렬 기준",
"Source": "소스",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Tamanho",
"Skip": "Pular",
"Skip TLS verification": "Ignorar verificação TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Ir para o conteúdo principal",
"Sort by": "Ordenar por",
"Source": "Origem",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Размер",
"Skip": "Пропустить",
"Skip TLS verification": "Пропустить проверку TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Перейти к основному содержимому",
"Sort by": "Сортировать по",
"Source": "Источник",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Boyut",
"Skip": "Atla",
"Skip TLS verification": "TLS doğrulamasını atla",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Ana içeriğe geç",
"Sort by": "Göre sırala",
"Source": "Kaynak",
+6
View File
@@ -1066,6 +1066,12 @@
"Size": "Kích thước",
"Skip": "Bỏ qua",
"Skip TLS verification": "Bỏ qua xác minh TLS",
"Certificate verification is disabled. Only use this for trusted networks.": "Certificate verification is disabled. Only use this for trusted networks.",
"Custom CA certificate": "Custom CA certificate",
"Custom CA certificate is required": "Custom CA certificate is required",
"Default certificate verification": "Default certificate verification",
"Paste the CA certificate in PEM format.": "Paste the CA certificate in PEM format.",
"TLS Verification": "TLS Verification",
"Skip to main content": "Chuyển đến nội dung chính",
"Sort by": "Sắp xếp theo",
"Source": "Nguồn",
+6
View File
@@ -1061,6 +1061,12 @@
"Size": "大小",
"Skip": "跳过",
"Skip TLS verification": "跳过 TLS 校验",
"Certificate verification is disabled. Only use this for trusted networks.": "证书校验已禁用。仅可在可信网络中使用。",
"Custom CA certificate": "自定义 CA 证书",
"Custom CA certificate is required": "必须填写自定义 CA 证书",
"Default certificate verification": "默认证书校验",
"Paste the CA certificate in PEM format.": "请粘贴 PEM 格式的 CA 证书。",
"TLS Verification": "TLS 校验",
"Skip to main content": "跳到主要内容",
"Sort by": "排序方式",
"Source": "来源",
+26
View File
@@ -0,0 +1,26 @@
export type BucketReplicationTlsMode = "verify" | "custom-ca" | "skip"
export interface BucketReplicationTlsPayload {
skipTlsVerify: boolean
caCertPem: string
}
export function buildBucketReplicationTlsPayload(
secure: boolean,
mode: BucketReplicationTlsMode,
caCertPem: string,
): BucketReplicationTlsPayload {
if (!secure) {
return { skipTlsVerify: false, caCertPem: "" }
}
if (mode === "skip") {
return { skipTlsVerify: true, caCertPem: "" }
}
if (mode === "custom-ca") {
return { skipTlsVerify: false, caCertPem: caCertPem.trim() }
}
return { skipTlsVerify: false, caCertPem: "" }
}
+46
View File
@@ -0,0 +1,46 @@
import assert from "node:assert/strict"
import test from "node:test"
import { readFile } from "node:fs/promises"
const loadTlsHelpers = () => import(new URL("../../lib/bucket-replication-tls.ts", import.meta.url).href)
test("bucket replication TLS payload defaults to certificate verification", async () => {
const { buildBucketReplicationTlsPayload } = await loadTlsHelpers()
assert.deepEqual(buildBucketReplicationTlsPayload(true, "verify", "unused"), {
skipTlsVerify: false,
caCertPem: "",
})
})
test("bucket replication TLS payload enables exactly one custom trust mode", async () => {
const { buildBucketReplicationTlsPayload } = await loadTlsHelpers()
assert.deepEqual(buildBucketReplicationTlsPayload(true, "skip", "unused"), {
skipTlsVerify: true,
caCertPem: "",
})
assert.deepEqual(buildBucketReplicationTlsPayload(true, "custom-ca", " certificate pem "), {
skipTlsVerify: false,
caCertPem: "certificate pem",
})
})
test("bucket replication TLS payload clears TLS-only settings for HTTP targets", async () => {
const { buildBucketReplicationTlsPayload } = await loadTlsHelpers()
assert.deepEqual(buildBucketReplicationTlsPayload(false, "skip", "certificate pem"), {
skipTlsVerify: false,
caCertPem: "",
})
})
test("bucket replication form submits custom TLS fields and exposes accessible validation", async () => {
const source = await readFile(new URL("../../components/replication/new-form.tsx", import.meta.url), "utf8")
assert.match(source, /buildBucketReplicationTlsPayload/)
assert.match(source, /skipTlsVerify/)
assert.match(source, /caCertPem/)
assert.match(source, /replication-ca-certificate-error/)
assert.match(source, /role="alert"/)
})