1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

fix(docker/tls): update tls certs for Docker API env [EE-4286] (#9112)

This commit is contained in:
Oscar Zhou 2023-06-28 08:51:58 +12:00 committed by GitHub
parent f1f46f4da1
commit f02ede00b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 184 additions and 64 deletions

View file

@ -1,15 +1,20 @@
import { useFormikContext } from 'formik';
import { FormikErrors } from 'formik';
import { SchemaOf, boolean, object } from 'yup';
import { file, withFileSize } from '@@/form-components/yup-file-validation';
import { FileUploadField } from '@@/form-components/FileUpload';
import { SwitchField } from '@@/form-components/SwitchField';
import { FormControl } from '@@/form-components/FormControl';
import { FormValues } from './types';
import { TLSConfig } from './types';
export function TLSFieldset() {
const { values, setFieldValue, errors } = useFormikContext<FormValues>();
interface Props {
values: TLSConfig;
onChange: (value: Partial<TLSConfig>) => void;
errors?: FormikErrors<TLSConfig>;
}
export function TLSFieldset({ values, onChange, errors }: Props) {
return (
<>
<div className="form-group">
@ -18,7 +23,7 @@ export function TLSFieldset() {
label="TLS"
labelClass="col-sm-3 col-lg-2"
checked={values.tls}
onChange={(checked) => setFieldValue('tls', checked)}
onChange={(checked) => handleChange({ tls: checked })}
/>
</div>
</div>
@ -30,7 +35,8 @@ export function TLSFieldset() {
<SwitchField
label="Skip Certification Verification"
checked={!!values.skipVerify}
onChange={(checked) => setFieldValue('skipVerify', checked)}
onChange={(checked) => handleChange({ skipVerify: checked })}
labelClass="col-sm-3 col-lg-2"
/>
</div>
</div>
@ -40,33 +46,33 @@ export function TLSFieldset() {
<FormControl
label="TLS CA certificate"
inputId="ca-cert-field"
errors={errors.caCertFile}
errors={errors?.caCertFile}
>
<FileUploadField
inputId="ca-cert-field"
onChange={(file) => setFieldValue('caCertFile', file)}
onChange={(file) => handleChange({ caCertFile: file })}
value={values.caCertFile}
/>
</FormControl>
<FormControl
label="TLS certificate"
inputId="cert-field"
errors={errors.certFile}
errors={errors?.certFile}
>
<FileUploadField
inputId="cert-field"
onChange={(file) => setFieldValue('certFile', file)}
onChange={(file) => handleChange({ certFile: file })}
value={values.certFile}
/>
</FormControl>
<FormControl
label="TLS key"
inputId="tls-key-field"
errors={errors.keyFile}
errors={errors?.keyFile}
>
<FileUploadField
inputId="tls-key-field"
onChange={(file) => setFieldValue('keyFile', file)}
onChange={(file) => handleChange({ keyFile: file })}
value={values.keyFile}
/>
</FormControl>
@ -76,21 +82,29 @@ export function TLSFieldset() {
)}
</>
);
function handleChange(partialValue: Partial<TLSConfig>) {
onChange(partialValue);
}
}
const MAX_FILE_SIZE = 5_242_880; // 5MB
function certValidation() {
function certValidation(optional?: boolean) {
return withFileSize(file(), MAX_FILE_SIZE).when(['tls', 'skipVerify'], {
is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify,
is: (tls: boolean, skipVerify: boolean) => tls && !skipVerify && !optional,
then: (schema) => schema.required('File is required'),
});
}
export function validation() {
return {
caCertFile: certValidation(),
certFile: certValidation(),
keyFile: certValidation(),
};
export function tlsConfigValidation({
optionalCert,
}: { optionalCert?: boolean } = {}): SchemaOf<TLSConfig> {
return object({
tls: boolean().default(false),
skipVerify: boolean().default(false),
caCertFile: certValidation(optionalCert),
certFile: certValidation(optionalCert),
keyFile: certValidation(optionalCert),
});
}

View file

@ -0,0 +1 @@
export { TLSFieldset, tlsConfigValidation } from './TLSFieldset';

View file

@ -0,0 +1,7 @@
export interface TLSConfig {
tls: boolean;
skipVerify?: boolean;
caCertFile?: File;
certFile?: File;
keyFile?: File;
}

View file

@ -67,6 +67,13 @@ export function isLocalEnvironment(environment: Environment) {
);
}
export function isDockerAPIEnvironment(environment: Environment) {
return (
environment.URL.startsWith('tcp://') &&
environment.Type === EnvironmentType.Docker
);
}
export function getDashboardRoute(environment: Environment) {
if (isEdgeEnvironment(environment.Type)) {
if (!environment.EdgeID) {

View file

@ -8,6 +8,7 @@ import {
Environment,
EnvironmentCreationTypes,
} from '@/react/portainer/environments/types';
import { TLSFieldset } from '@/react/components/TLSFieldset/TLSFieldset';
import { LoadingButton } from '@@/buttons/LoadingButton';
import { FormControl } from '@@/form-components/FormControl';
@ -19,7 +20,6 @@ import { MoreSettingsSection } from '../../shared/MoreSettingsSection';
import { useValidation } from './APIForm.validation';
import { FormValues } from './types';
import { TLSFieldset } from './TLSFieldset';
interface Props {
onCreate(environment: Environment): void;
@ -31,7 +31,10 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) {
const initialValues: FormValues = {
url: '',
name: '',
tls: false,
tlsConfig: {
tls: false,
skipVerify: false,
},
meta: {
groupId: 1,
tagIds: [],
@ -52,7 +55,7 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) {
validateOnMount
key={formKey}
>
{({ isValid, dirty }) => (
{({ values, errors, setFieldValue, isValid, dirty }) => (
<Form>
<NameField />
@ -70,7 +73,15 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) {
/>
</FormControl>
<TLSFieldset />
<TLSFieldset
values={values.tlsConfig}
onChange={(value) =>
Object.entries(value).forEach(([key, value]) =>
setFieldValue(`tlsConfig.${key}`, value)
)
}
errors={errors.tlsConfig}
/>
<MoreSettingsSection>
{isDockerStandalone && (
@ -141,24 +152,24 @@ export function APIForm({ onCreate, isDockerStandalone }: Props) {
}
);
function getTlsValues() {
if (!values.tls) {
if (!values.tlsConfig.tls) {
return undefined;
}
return {
skipVerify: values.skipVerify,
skipVerify: values.tlsConfig.skipVerify,
...getCertFiles(),
};
function getCertFiles() {
if (values.skipVerify) {
if (values.tlsConfig.skipVerify) {
return {};
}
return {
caCertFile: values.caCertFile,
certFile: values.certFile,
keyFile: values.keyFile,
caCertFile: values.tlsConfig.caCertFile,
certFile: values.tlsConfig.certFile,
keyFile: values.tlsConfig.keyFile,
};
}
}

View file

@ -1,18 +1,17 @@
import { boolean, object, SchemaOf, string } from 'yup';
import { object, SchemaOf, string } from 'yup';
import { tlsConfigValidation } from '@/react/components/TLSFieldset/TLSFieldset';
import { metadataValidation } from '../../shared/MetadataFieldset/validation';
import { useNameValidation } from '../../shared/NameField';
import { validation as certsValidation } from './TLSFieldset';
import { FormValues } from './types';
export function useValidation(): SchemaOf<FormValues> {
return object({
name: useNameValidation(),
url: string().required('This field is required.'),
tls: boolean().default(false),
skipVerify: boolean(),
tlsConfig: tlsConfigValidation(),
meta: metadataValidation(),
...certsValidation(),
});
}

View file

@ -1,12 +1,9 @@
import { TLSConfig } from '@/react/components/TLSFieldset/types';
import { EnvironmentMetadata } from '@/react/portainer/environments/environment.service/create';
export interface FormValues {
name: string;
url: string;
tls: boolean;
skipVerify?: boolean;
caCertFile?: File;
certFile?: File;
keyFile?: File;
tlsConfig: TLSConfig;
meta: EnvironmentMetadata;
}

View file

@ -126,6 +126,7 @@ function OverrideSocketFieldset() {
checked={values.overridePath}
onChange={(checked) => setFieldValue('overridePath', checked)}
label="Override default socket path"
labelClass="col-sm-3 col-lg-2"
/>
</div>
</div>