diff --git a/backend/internal/features/storages/models/s3/model.go b/backend/internal/features/storages/models/s3/model.go index 9201d00..b61f98f 100644 --- a/backend/internal/features/storages/models/s3/model.go +++ b/backend/internal/features/storages/models/s3/model.go @@ -3,6 +3,7 @@ package s3_storage import ( "bytes" "context" + "crypto/tls" "errors" "fmt" "io" @@ -40,6 +41,7 @@ type S3Storage struct { S3Prefix string `json:"s3Prefix" gorm:"type:text;column:s3_prefix"` S3UseVirtualHostedStyle bool `json:"s3UseVirtualHostedStyle" gorm:"default:false;column:s3_use_virtual_hosted_style"` + SkipTLSVerify bool `json:"skipTLSVerify" gorm:"default:false;column:skip_tls_verify"` } func (s *S3Storage) TableName() string { @@ -331,6 +333,7 @@ func (s *S3Storage) Update(incoming *S3Storage) { s.S3Region = incoming.S3Region s.S3Endpoint = incoming.S3Endpoint s.S3UseVirtualHostedStyle = incoming.S3UseVirtualHostedStyle + s.SkipTLSVerify = incoming.SkipTLSVerify if incoming.S3AccessKey != "" { s.S3AccessKey = incoming.S3AccessKey @@ -442,6 +445,9 @@ func (s *S3Storage) getClientParams( TLSHandshakeTimeout: s3TLSHandshakeTimeout, ResponseHeaderTimeout: s3ResponseTimeout, IdleConnTimeout: s3IdleConnTimeout, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: s.SkipTLSVerify, + }, } return endpoint, useSSL, accessKey, secretKey, bucketLookup, transport, nil diff --git a/backend/migrations/20251211164424_add_skip_tls_verify_to_s3.sql b/backend/migrations/20251211164424_add_skip_tls_verify_to_s3.sql new file mode 100644 index 0000000..d9276de --- /dev/null +++ b/backend/migrations/20251211164424_add_skip_tls_verify_to_s3.sql @@ -0,0 +1,11 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE s3_storages + ADD COLUMN skip_tls_verify BOOLEAN NOT NULL DEFAULT FALSE; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE s3_storages + DROP COLUMN skip_tls_verify; +-- +goose StatementEnd diff --git a/frontend/src/entity/storages/models/S3Storage.ts b/frontend/src/entity/storages/models/S3Storage.ts index 40c741b..88c0905 100644 --- a/frontend/src/entity/storages/models/S3Storage.ts +++ b/frontend/src/entity/storages/models/S3Storage.ts @@ -6,4 +6,5 @@ export interface S3Storage { s3Endpoint?: string; s3Prefix?: string; s3UseVirtualHostedStyle?: boolean; + skipTLSVerify?: boolean; } diff --git a/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx b/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx index a20679a..f78375d 100644 --- a/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx +++ b/frontend/src/features/storages/ui/edit/EditStorageComponent.tsx @@ -39,6 +39,7 @@ export function EditStorageComponent({ const [isTestingConnection, setIsTestingConnection] = useState(false); const [isTestConnectionSuccess, setIsTestConnectionSuccess] = useState(false); + const [connectionError, setConnectionError] = useState(); const save = async () => { if (!storage) return; @@ -60,6 +61,7 @@ export function EditStorageComponent({ if (!storage) return; setIsTestingConnection(true); + setConnectionError(undefined); try { await storageApi.testStorageConnectionDirect(storage); @@ -69,7 +71,9 @@ export function EditStorageComponent({ description: 'Storage connection tested successfully', }); } catch (e) { - alert((e as Error).message); + const errorMessage = (e as Error).message; + setConnectionError(errorMessage); + alert(errorMessage); } setIsTestingConnection(false); @@ -290,7 +294,9 @@ export function EditStorageComponent({ setUnsaved={() => { setIsUnsaved(true); setIsTestConnectionSuccess(false); + setConnectionError(undefined); }} + connectionError={connectionError} /> )} diff --git a/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx b/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx index 79436ab..2d12701 100644 --- a/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx +++ b/frontend/src/features/storages/ui/edit/storages/EditS3StorageComponent.tsx @@ -1,6 +1,6 @@ import { DownOutlined, InfoCircleOutlined, UpOutlined } from '@ant-design/icons'; import { Checkbox, Input, Tooltip } from 'antd'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import type { Storage } from '../../../../../entity/storages'; @@ -8,13 +8,27 @@ interface Props { storage: Storage; setStorage: (storage: Storage) => void; setUnsaved: () => void; + connectionError?: string; } -export function EditS3StorageComponent({ storage, setStorage, setUnsaved }: Props) { +export function EditS3StorageComponent({ + storage, + setStorage, + setUnsaved, + connectionError, +}: Props) { const hasAdvancedValues = - !!storage?.s3Storage?.s3Prefix || !!storage?.s3Storage?.s3UseVirtualHostedStyle; + !!storage?.s3Storage?.s3Prefix || + !!storage?.s3Storage?.s3UseVirtualHostedStyle || + !!storage?.s3Storage?.skipTLSVerify; const [showAdvanced, setShowAdvanced] = useState(hasAdvancedValues); + useEffect(() => { + if (connectionError?.includes('failed to verify certificate')) { + setShowAdvanced(true); + } + }, [connectionError]); + return ( <>
@@ -226,6 +240,36 @@ export function EditS3StorageComponent({ storage, setStorage, setUnsaved }: Prop
+ +
+
Skip TLS verify
+
+ { + if (!storage?.s3Storage) return; + + setStorage({ + ...storage, + s3Storage: { + ...storage.s3Storage, + skipTLSVerify: e.target.checked, + }, + }); + setUnsaved(); + }} + > + Skip TLS + + + + + +
+
)} diff --git a/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx b/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx index 4eb8fdb..d833e3a 100644 --- a/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx +++ b/frontend/src/features/storages/ui/show/storages/ShowS3StorageComponent.tsx @@ -45,6 +45,13 @@ export function ShowS3StorageComponent({ storage }: Props) { Enabled )} + + {storage?.s3Storage?.skipTLSVerify && ( +
+
Skip TLS
+ Enabled +
+ )} ); }