Feature/import export encryption (#1533)

Add option to encrypt repository exports with a password and add possibility to decrypt them on repository import. Also make the repository export asynchronous. This implies that the repository export will be created on the server and can be downloaded multiple times. The repository export will be deleted automatically 10 days after creation.
This commit is contained in:
Eduard Heimbuch
2021-02-25 13:01:03 +01:00
committed by GitHub
parent 367d7294b8
commit db2ce98721
53 changed files with 2698 additions and 237 deletions

View File

@@ -23,7 +23,7 @@
*/
import React, { FC } from "react";
import { FileUpload, LabelWithHelpIcon, Checkbox } from "@scm-manager/ui-components";
import { FileUpload, LabelWithHelpIcon, Checkbox, InputField } from "@scm-manager/ui-components";
import { File } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
@@ -32,34 +32,57 @@ type Props = {
setValid: (valid: boolean) => void;
compressed: boolean;
setCompressed: (compressed: boolean) => void;
password: string;
setPassword: (password: string) => void;
disabled: boolean;
};
const ImportFromBundleForm: FC<Props> = ({ setFile, setValid, compressed, setCompressed, disabled }) => {
const ImportFromBundleForm: FC<Props> = ({
setFile,
setValid,
compressed,
setCompressed,
password,
setPassword,
disabled
}) => {
const [t] = useTranslation("repos");
return (
<div className="columns">
<div className="column is-half is-vcentered">
<LabelWithHelpIcon label={t("import.bundle.title")} helpText={t("import.bundle.helpText")} />
<FileUpload
handleFile={(file: File) => {
setFile(file);
setValid(!!file);
}}
/>
<>
<div className="columns">
<div className="column is-half is-vcentered">
<LabelWithHelpIcon label={t("import.bundle.title")} helpText={t("import.bundle.helpText")} />
<FileUpload
handleFile={(file: File) => {
setFile(file);
setValid(!!file);
}}
/>
</div>
<div className="column is-half is-vcentered">
<Checkbox
checked={compressed}
onChange={(value, name) => setCompressed(value)}
label={t("import.compressed.label")}
disabled={disabled}
helpText={t("import.compressed.helpText")}
title={t("import.compressed.label")}
/>
</div>
</div>
<div className="column is-half is-vcentered">
<Checkbox
checked={compressed}
onChange={(value, name) => setCompressed(value)}
label={t("import.compressed.label")}
disabled={disabled}
helpText={t("import.compressed.helpText")}
title={t("import.compressed.label")}
/>
<div className="columns">
<div className="column is-half is-vcentered">
<InputField
value={password}
onChange={value => setPassword(value)}
type="password"
label={t("import.bundle.password.title")}
helpText={t("import.bundle.password.helpText")}
/>
</div>
</div>
</div>
</>
);
};

View File

@@ -28,7 +28,6 @@ import RepositoryInformationForm from "./RepositoryInformationForm";
import { apiClient, ErrorNotification, Level, SubmitButton } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import { useHistory } from "react-router-dom";
import ImportFromBundleForm from "./ImportFromBundleForm";
import ImportFullRepositoryForm from "./ImportFullRepositoryForm";
type Props = {
@@ -44,9 +43,9 @@ const ImportFullRepository: FC<Props> = ({ url, repositoryType, setImportPending
type: repositoryType,
contact: "",
description: "",
_links: {},
_links: {}
});
const [password, setPassword] = useState("");
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | undefined>();
@@ -59,7 +58,7 @@ const ImportFullRepository: FC<Props> = ({ url, repositoryType, setImportPending
setLoading(loading);
};
const isValid = () => Object.values(valid).every((v) => v);
const isValid = () => Object.values(valid).every(v => v);
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
@@ -69,7 +68,7 @@ const ImportFullRepository: FC<Props> = ({ url, repositoryType, setImportPending
apiClient
.postBinary(url, formData => {
formData.append("bundle", file, file?.name);
formData.append("repository", JSON.stringify(repo));
formData.append("repository", JSON.stringify({ ...repo, password }));
})
.then(response => {
const location = response.headers.get("Location");
@@ -91,7 +90,12 @@ const ImportFullRepository: FC<Props> = ({ url, repositoryType, setImportPending
return (
<form onSubmit={submit}>
<ErrorNotification error={error} />
<ImportFullRepositoryForm setFile={setFile} setValid={(file: boolean) => setValid({ ...valid, file })}/>
<ImportFullRepositoryForm
setFile={setFile}
password={password}
setPassword={setPassword}
setValid={(file: boolean) => setValid({ ...valid, file })}
/>
<hr />
<NamespaceAndNameFields
repository={repo}

View File

@@ -23,21 +23,23 @@
*/
import React, { FC } from "react";
import { FileUpload, LabelWithHelpIcon, Checkbox } from "@scm-manager/ui-components";
import { FileUpload, InputField, LabelWithHelpIcon } from "@scm-manager/ui-components";
import { File } from "@scm-manager/ui-types";
import { useTranslation } from "react-i18next";
type Props = {
setFile: (file: File) => void;
setValid: (valid: boolean) => void;
password: string;
setPassword: (password: string) => void;
};
const ImportFullRepositoryForm: FC<Props> = ({ setFile, setValid}) => {
const ImportFullRepositoryForm: FC<Props> = ({ setFile, setValid, password, setPassword }) => {
const [t] = useTranslation("repos");
return (
<div className="columns">
<div className="column is-vcentered">
<div className="column is-half is-vcentered">
<LabelWithHelpIcon label={t("import.fullImport.title")} helpText={t("import.fullImport.helpText")} />
<FileUpload
handleFile={(file: File) => {
@@ -46,6 +48,15 @@ const ImportFullRepositoryForm: FC<Props> = ({ setFile, setValid}) => {
}}
/>
</div>
<div className="column is-half is-vcentered">
<InputField
value={password}
onChange={value => setPassword(value)}
type="password"
label={t("import.bundle.password.title")}
helpText={t("import.bundle.password.helpText")}
/>
</div>
</div>
);
};

View File

@@ -45,12 +45,12 @@ const ImportRepositoryFromBundle: FC<Props> = ({ url, repositoryType, setImportP
description: "",
_links: {}
});
const [password, setPassword] = useState("");
const [valid, setValid] = useState({ namespaceAndName: false, contact: true, file: false });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | undefined>();
const [file, setFile] = useState<File | null>(null);
const [compressed, setCompressed] = useState(false);
const [compressed, setCompressed] = useState(true);
const history = useHistory();
const [t] = useTranslation("repos");
@@ -69,7 +69,7 @@ const ImportRepositoryFromBundle: FC<Props> = ({ url, repositoryType, setImportP
apiClient
.postBinary(compressed ? url + "?compressed=true" : url, formData => {
formData.append("bundle", file, file?.name);
formData.append("repository", JSON.stringify(repo));
formData.append("repository", JSON.stringify({ ...repo, password }));
})
.then(response => {
const location = response.headers.get("Location");
@@ -96,6 +96,8 @@ const ImportRepositoryFromBundle: FC<Props> = ({ url, repositoryType, setImportP
setValid={(file: boolean) => setValid({ ...valid, file })}
compressed={compressed}
setCompressed={setCompressed}
password={password}
setPassword={setPassword}
disabled={loading}
/>
<hr />

View File

@@ -25,6 +25,7 @@ import React, { FC } from "react";
import { Link, RepositoryType } from "@scm-manager/ui-types";
import { LabelWithHelpIcon, Radio } from "@scm-manager/ui-components";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
type Props = {
repositoryType: RepositoryType;
@@ -33,6 +34,12 @@ type Props = {
disabled?: boolean;
};
const RadioGroup = styled.div`
label {
margin-right: 2rem;
}
`;
const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType, disabled }) => {
const [t] = useTranslation("repos");
@@ -45,18 +52,20 @@ const ImportTypeSelect: FC<Props> = ({ repositoryType, importType, setImportType
return (
<>
<LabelWithHelpIcon label={t("import.importTypes.label")} key="import.importTypes.label" />
{(repositoryType._links.import as Link[]).map((type, index) => (
<Radio
name={type.name}
checked={importType === type.name}
value={type.name}
label={t(`import.importTypes.${type.name}.label`)}
helpText={t(`import.importTypes.${type.name}.helpText`)}
onChange={changeImportType}
key={index}
disabled={disabled}
/>
))}
<RadioGroup>
{(repositoryType._links.import as Link[]).map((type, index) => (
<Radio
name={type.name}
checked={importType === type.name}
value={type.name}
label={t(`import.importTypes.${type.name}.label`)}
helpText={t(`import.importTypes.${type.name}.helpText`)}
onChange={changeImportType}
key={index}
disabled={disabled}
/>
))}
</RadioGroup>
</>
);
};