UI for Inspect (#1583)
This commit is contained in:
committed by
GitHub
parent
1f97f39864
commit
006b3c7da8
@@ -132,6 +132,7 @@ export const IAM_PAGES = {
|
||||
DASHBOARD: "/tools/metrics",
|
||||
TOOLS_HEAL: "/tools/heal",
|
||||
TOOLS_WATCH: "/tools/watch",
|
||||
TOOLS_INSPECT: "/tools/inspect",
|
||||
/* Health */
|
||||
HEALTH: "/health",
|
||||
|
||||
@@ -366,6 +367,7 @@ export const IAM_PAGES_PERMISSIONS = {
|
||||
[IAM_PAGES.CALL_HOME]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
|
||||
[IAM_PAGES.PROFILE]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
|
||||
[IAM_PAGES.HEALTH]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
|
||||
[IAM_PAGES.TOOLS_INSPECT]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
|
||||
[IAM_PAGES.LICENSE]: [
|
||||
IAM_SCOPES.ADMIN_SERVER_INFO,
|
||||
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
|
||||
|
||||
@@ -609,3 +609,20 @@ export const decodeFileName = (text: string) => {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
export const performDownload = (blob: Blob, fileName: string) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
export const getCookieValue = (cookieName: string) => {
|
||||
return (
|
||||
document.cookie
|
||||
.match("(^|;)\\s*" + cookieName + "\\s*=\\s*([^;]+)")
|
||||
?.pop() || ""
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import Grid from "@mui/material/Grid";
|
||||
import { actionsTray, fieldBasic } from "../common/styleLibrary";
|
||||
import HelpIcon from "../../../../../icons/HelpIcon";
|
||||
import clsx from "clsx";
|
||||
import { InputProps as StandardInputProps } from "@mui/material/Input/Input";
|
||||
|
||||
interface IFormSwitch {
|
||||
label?: string;
|
||||
@@ -38,6 +39,7 @@ interface IFormSwitch {
|
||||
checked: boolean;
|
||||
switchOnly?: boolean;
|
||||
indicatorLabels?: string[];
|
||||
extraInputProps?: StandardInputProps["inputProps"];
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -125,6 +127,7 @@ const FormSwitchWrapper = ({
|
||||
description = "",
|
||||
classes,
|
||||
indicatorLabels,
|
||||
extraInputProps = {},
|
||||
}: IFormSwitch) => {
|
||||
const switchComponent = (
|
||||
<React.Fragment>
|
||||
@@ -144,7 +147,7 @@ const FormSwitchWrapper = ({
|
||||
onChange={onChange}
|
||||
color="primary"
|
||||
name={name}
|
||||
inputProps={{ "aria-label": "primary checkbox" }}
|
||||
inputProps={{ "aria-label": "primary checkbox", ...extraInputProps }}
|
||||
disabled={disabled}
|
||||
disableRipple
|
||||
disableFocusRipple
|
||||
|
||||
@@ -414,7 +414,7 @@ export const selectorsCommon = {
|
||||
},
|
||||
};
|
||||
|
||||
export const settingsCommon = {
|
||||
export const settingsCommon:any = {
|
||||
customTitle: {
|
||||
fontSize: 18,
|
||||
color: "#000",
|
||||
@@ -1152,7 +1152,7 @@ export const spacingUtils: any = {
|
||||
},
|
||||
};
|
||||
|
||||
export const formFieldStyles = {
|
||||
export const formFieldStyles:any = {
|
||||
formFieldRow: {
|
||||
marginBottom: ".8rem",
|
||||
"& .MuiInputLabel-root": {
|
||||
|
||||
@@ -326,6 +326,10 @@ const Console = ({
|
||||
component: Tools,
|
||||
path: IAM_PAGES.PROFILE,
|
||||
},
|
||||
{
|
||||
component: Tools,
|
||||
path: IAM_PAGES.TOOLS_INSPECT,
|
||||
},
|
||||
{
|
||||
component: ConfigurationOptions,
|
||||
path: IAM_PAGES.SETTINGS,
|
||||
|
||||
501
portal-ui/src/screens/Console/Tools/Inspect.tsx
Normal file
501
portal-ui/src/screens/Console/Tools/Inspect.tsx
Normal file
@@ -0,0 +1,501 @@
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
import { Box, Button, DialogContentText } from "@mui/material";
|
||||
import PageHeader from "../Common/PageHeader/PageHeader";
|
||||
import PageLayout from "../Common/Layout/PageLayout";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
|
||||
import { CopyIcon, FileBookIcon, PasswordKeyIcon } from "../../../icons";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import {
|
||||
deleteDialogStyles,
|
||||
modalStyleUtils,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import { setErrorSnackMessage } from "../../../actions";
|
||||
import { connect } from "react-redux";
|
||||
import HelpBox from "../../../common/HelpBox";
|
||||
import {
|
||||
deleteCookie,
|
||||
getCookieValue,
|
||||
performDownload,
|
||||
} from "../../../common/utils";
|
||||
import DistributedOnly from "../Common/DistributedOnly/DistributedOnly";
|
||||
import { AppState } from "../../../store";
|
||||
import { InspectMenuIcon } from "../../../icons/SidebarMenus";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
switchLabel: {
|
||||
fontWeight: "normal",
|
||||
},
|
||||
...deleteDialogStyles,
|
||||
...modalStyleUtils,
|
||||
});
|
||||
|
||||
const KeyRevealer = ({ value }: { value: string }) => {
|
||||
const [shown, setShown] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexFlow: {
|
||||
sm: "row",
|
||||
xs: "column",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InputBoxWrapper
|
||||
id="inspect-dec-key"
|
||||
name="inspect-dec-key"
|
||||
placeholder=""
|
||||
label=""
|
||||
type={shown ? "text" : "password"}
|
||||
onChange={() => {}}
|
||||
value={value}
|
||||
overlayIcon={<CopyIcon />}
|
||||
extraInputProps={{
|
||||
readOnly: true,
|
||||
}}
|
||||
overlayAction={() => navigator.clipboard.writeText(value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
sx={{
|
||||
marginLeft: "10px",
|
||||
}}
|
||||
variant="contained"
|
||||
onClick={() => setShown(!shown)}
|
||||
>
|
||||
Show/Hide
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const mapState = (state: AppState) => ({
|
||||
distributedSetup: state.system.distributedSetup,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setErrorSnackMessage,
|
||||
};
|
||||
const connector = connect(mapState, mapDispatchToProps);
|
||||
|
||||
const Inspect = ({
|
||||
classes,
|
||||
setErrorSnackMessage,
|
||||
distributedSetup,
|
||||
}: {
|
||||
classes: any;
|
||||
setErrorSnackMessage: any;
|
||||
distributedSetup: boolean;
|
||||
}) => {
|
||||
const [volumeName, setVolumeName] = useState<string>("");
|
||||
const [inspectPath, setInspectPath] = useState<string>("");
|
||||
const [isEncrypt, setIsEncrypt] = useState<boolean>(true);
|
||||
|
||||
const [decryptionKey, setDecryptionKey] = useState<string>("");
|
||||
|
||||
const [insFileName, setInsFileName] = useState<string>("");
|
||||
|
||||
const [isFormValid, setIsFormValid] = useState<boolean>(false);
|
||||
const [volumeError, setVolumeError] = useState<string>("");
|
||||
const [pathError, setPathError] = useState<string>("");
|
||||
|
||||
/**
|
||||
* Validation Effect
|
||||
*/
|
||||
useEffect(() => {
|
||||
let isVolValid;
|
||||
let isPathValid;
|
||||
|
||||
isVolValid = volumeName.trim().length > 0;
|
||||
if (!isVolValid) {
|
||||
setVolumeError("This field is required");
|
||||
} else if (volumeName.slice(0, 1) === "/") {
|
||||
isVolValid = false;
|
||||
setVolumeError("Volume/Bucket name cannot start with /");
|
||||
}
|
||||
isPathValid = inspectPath.trim().length > 0;
|
||||
if (!inspectPath) {
|
||||
setPathError("This field is required");
|
||||
} else if (inspectPath.slice(0, 1) === "/") {
|
||||
isPathValid = false;
|
||||
setPathError("Path cannot start with /");
|
||||
}
|
||||
const isValid = isVolValid && isPathValid;
|
||||
|
||||
if (isVolValid) {
|
||||
setVolumeError("");
|
||||
}
|
||||
if (isPathValid) {
|
||||
setPathError("");
|
||||
}
|
||||
|
||||
setIsFormValid(isValid);
|
||||
}, [volumeName, inspectPath]);
|
||||
|
||||
const makeRequest = async (url: string) => {
|
||||
return await fetch(url, { method: "GET" });
|
||||
};
|
||||
|
||||
const performInspect = async () => {
|
||||
const file = encodeURIComponent(inspectPath);
|
||||
const volume = encodeURIComponent(volumeName);
|
||||
|
||||
const urlOfInspectApi = `/api/v1/admin/inspect?volume=${volume}&file=${file}&encrypt=${isEncrypt}`;
|
||||
|
||||
makeRequest(urlOfInspectApi)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const resErr: any = await res.json();
|
||||
|
||||
setErrorSnackMessage({
|
||||
errorMessage: resErr.message,
|
||||
detailedError: resErr.code,
|
||||
});
|
||||
}
|
||||
const blob: Blob = await res.blob();
|
||||
|
||||
//@ts-ignore
|
||||
const filename = res.headers.get("content-disposition").split('"')[1];
|
||||
const decryptKey = getCookieValue(filename) || "";
|
||||
|
||||
performDownload(blob, filename);
|
||||
setInsFileName(filename);
|
||||
setDecryptionKey(decryptKey);
|
||||
})
|
||||
.catch((err) => {
|
||||
setErrorSnackMessage(err);
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setVolumeName("");
|
||||
setInspectPath("");
|
||||
setIsEncrypt(true);
|
||||
};
|
||||
|
||||
const onCloseDecKeyModal = () => {
|
||||
deleteCookie(insFileName);
|
||||
setDecryptionKey("");
|
||||
resetForm();
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<PageHeader label={"Inspect"} />
|
||||
<PageLayout>
|
||||
{!distributedSetup ? (
|
||||
<DistributedOnly
|
||||
iconComponent={<InspectMenuIcon />}
|
||||
entity={"Inspect"}
|
||||
/>
|
||||
) : (
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
border: "1px solid #eaeaea",
|
||||
padding: "25px",
|
||||
}}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
performInspect();
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<InputBoxWrapper
|
||||
id="inspect_volume"
|
||||
name="inspect_volume"
|
||||
extraInputProps={{
|
||||
"data-test-id": "inspect_volume",
|
||||
}}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setVolumeName(e.target.value);
|
||||
}}
|
||||
label="Volume or Bucket Name"
|
||||
value={volumeName}
|
||||
error={volumeError}
|
||||
required
|
||||
placeholder={"test-bucket"}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
<InputBoxWrapper
|
||||
id="inspect_path"
|
||||
name="inspect_path"
|
||||
extraInputProps={{
|
||||
"data-test-id": "inspect_path",
|
||||
}}
|
||||
error={pathError}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInspectPath(e.target.value);
|
||||
}}
|
||||
label="File or Path to inspect"
|
||||
value={inspectPath}
|
||||
required
|
||||
placeholder={"test*/xl.meta"}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "25px",
|
||||
}}
|
||||
>
|
||||
<FormSwitchWrapper
|
||||
classes={{
|
||||
inputLabel: classes.switchLabel,
|
||||
}}
|
||||
extraInputProps={{
|
||||
"data-test-id": "inspect_encrypt",
|
||||
}}
|
||||
label="Encrypt"
|
||||
indicatorLabels={["True", "False"]}
|
||||
checked={isEncrypt}
|
||||
value={"true"}
|
||||
id="inspect_encrypt"
|
||||
name="inspect_encrypt"
|
||||
onChange={(e) => {
|
||||
setIsEncrypt(!isEncrypt);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
marginTop: "55px",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
sx={{
|
||||
marginRight: "15px",
|
||||
}}
|
||||
type="button"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
data-test-id="inspect-clear-button"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
data-test-id="inspect-submit-button"
|
||||
disabled={!isFormValid}
|
||||
>
|
||||
Inspect
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "55px",
|
||||
}}
|
||||
>
|
||||
<HelpBox
|
||||
title={"Inspect"}
|
||||
iconComponent={<FileBookIcon />}
|
||||
help={
|
||||
<Fragment>
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "15px",
|
||||
}}
|
||||
>
|
||||
Inspect files on MinIO server
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "15px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Examples
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
flex: "2",
|
||||
marginTop: "15px",
|
||||
"& .step-number": {
|
||||
color: "#ffffff",
|
||||
height: "25px",
|
||||
width: "25px",
|
||||
background: "#081C42",
|
||||
marginRight: "10px",
|
||||
textAlign: "center",
|
||||
fontWeight: 600,
|
||||
borderRadius: "50%",
|
||||
},
|
||||
|
||||
"& .step-row": {
|
||||
fontSize: "16px",
|
||||
display: "flex",
|
||||
marginTop: "15px",
|
||||
marginBottom: "15px",
|
||||
},
|
||||
|
||||
"& code": {
|
||||
paddingLeft: "10px",
|
||||
paddingRight: "10px",
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "3px",
|
||||
borderRadius: "2px",
|
||||
backgroundColor: "#eaeaea",
|
||||
color: "#082146",
|
||||
},
|
||||
"& .spacer": {
|
||||
marginBottom: "5px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box className="step-row">
|
||||
<div className="step-text">
|
||||
To Download 'xl.meta' for a specific object from all
|
||||
the drives in a zip file:
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
marginTop: "15px",
|
||||
marginLeft: "35px",
|
||||
"& input": {
|
||||
color: "#737373",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<label>Volume/bucket Name :</label>{" "}
|
||||
<code>test-bucket</code>
|
||||
<div className="spacer" />
|
||||
<label>Path : </label>
|
||||
<code>test*/xl.meta</code>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box className="step-row">
|
||||
<div className="step-text">
|
||||
To Download all constituent parts for a specific
|
||||
object, and optionally encrypt the downloaded zip:
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
marginTop: "15px",
|
||||
marginLeft: "35px",
|
||||
"& input": {
|
||||
color: "#737373",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<label>Volume/bucket Name : </label>
|
||||
<code>test-bucket</code>
|
||||
<div className="spacer" />
|
||||
<label>Path :</label> <code>test*/*/part.*</code>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box>
|
||||
<Box className="step-row">
|
||||
<div className="step-text">
|
||||
To Download recursively all objects at a prefix.
|
||||
<br />
|
||||
NOTE: This can be an expensive operation use it with
|
||||
caution.
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
flex: "1",
|
||||
marginTop: "15px",
|
||||
marginLeft: "35px",
|
||||
"& input": {
|
||||
color: "#737373",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<label>Volume/bucket Name : </label>
|
||||
<code>test-bucket</code>
|
||||
<div className="spacer" />
|
||||
<label>Path :</label> <code>test/**</code>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: "30px",
|
||||
}}
|
||||
>
|
||||
You can learn more at our{" "}
|
||||
<a
|
||||
href="https://github.com/minio/minio/tree/master/docs/debugging?ref=con"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
documentation
|
||||
</a>
|
||||
.
|
||||
</Box>
|
||||
</Fragment>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Fragment>
|
||||
)}
|
||||
{decryptionKey ? (
|
||||
<ModalWrapper
|
||||
modalOpen={true}
|
||||
title="Inspect Decryption Key"
|
||||
onClose={onCloseDecKeyModal}
|
||||
titleIcon={<PasswordKeyIcon />}
|
||||
>
|
||||
<DialogContentText>
|
||||
<Box>
|
||||
This will be displayed only once. It cannot be recovered.
|
||||
<br />
|
||||
Use secure medium to share this key.
|
||||
</Box>
|
||||
<Box>
|
||||
<KeyRevealer value={decryptionKey} />
|
||||
</Box>
|
||||
</DialogContentText>
|
||||
</ModalWrapper>
|
||||
) : null}
|
||||
</PageLayout>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(connector(Inspect));
|
||||
@@ -19,11 +19,15 @@ import { Route, Router, Switch } from "react-router-dom";
|
||||
import history from "../../../history";
|
||||
import NotFoundPage from "../../NotFoundPage";
|
||||
import ToolsList from "./ToolsPanel/ToolsList";
|
||||
import Register from "../Support/Register";
|
||||
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
|
||||
import FeatureNotAvailablePage from "../Common/Components/FeatureNotAvailablePage";
|
||||
import { SupportMenuIcon } from "../../../icons/SidebarMenus";
|
||||
|
||||
import withSuspense from "../Common/Components/withSuspense";
|
||||
|
||||
const Inspect = withSuspense(React.lazy(() => import("./Inspect")));
|
||||
const Register = withSuspense(React.lazy(() => import("../Support/Register")));
|
||||
|
||||
const Tools = () => {
|
||||
return (
|
||||
<Router history={history}>
|
||||
@@ -72,6 +76,7 @@ const Tools = () => {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Route path={IAM_PAGES.TOOLS_INSPECT} exact component={Inspect} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
SupportMenuIcon,
|
||||
TraceMenuIcon,
|
||||
UsersMenuIcon,
|
||||
InspectMenuIcon,
|
||||
} from "../../icons/SidebarMenus";
|
||||
import { hasPermission } from "../../common/SecureComponent";
|
||||
import WatchIcon from "../../icons/WatchIcon";
|
||||
@@ -159,6 +160,13 @@ export const validRoutes = (
|
||||
icon: DrivesMenuIcon,
|
||||
component: NavLink,
|
||||
},
|
||||
{
|
||||
name: "Inspect",
|
||||
id: "inspectObjects",
|
||||
to: IAM_PAGES.TOOLS_INSPECT,
|
||||
icon: InspectMenuIcon,
|
||||
component: NavLink,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
201
portal-ui/tests/permissions/inspect.ts
Normal file
201
portal-ui/tests/permissions/inspect.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { Role, Selector } from "testcafe";
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const data = readFileSync(__dirname + "/../constants/timestamp.txt", "utf-8");
|
||||
const $TIMESTAMP = data.trim();
|
||||
|
||||
let testDomainUrl = "http://localhost:9090";
|
||||
|
||||
let insAllowedAccKey = `inspect-allowed-${$TIMESTAMP}`;
|
||||
let insAllowedSeckey = "insallowed1234";
|
||||
let insNotAllowedAccKey = `inspect-not-allowed-${$TIMESTAMP}`;
|
||||
let insNotAllowedSeckey = "insnotallowed1234";
|
||||
|
||||
/* Begin Local Testing config block */
|
||||
|
||||
// For local Testing Create users and assign policies then update here.
|
||||
// Command to invoke the test locally: testcafe chrome tests/permissions/inspect.ts
|
||||
/*
|
||||
testDomainUrl = "http://localhost:5005";
|
||||
insAllowedAccKey = `all-actions`;
|
||||
insAllowedSeckey = "minio123";
|
||||
insNotAllowedAccKey = `deny-admin`;
|
||||
insNotAllowedSeckey = "minio123";
|
||||
*/
|
||||
|
||||
/* End Local Testing config block */
|
||||
|
||||
const loginUrl = `${testDomainUrl}/login`;
|
||||
const inspectScreenUrl = `${testDomainUrl}/tools/inspect`;
|
||||
|
||||
const loginSubmitBtn = Selector("form button");
|
||||
|
||||
export const supportSidebarEl = Selector(".MuiPaper-root")
|
||||
.find("ul")
|
||||
.child("#tools");
|
||||
|
||||
export const supportChildren = Selector("#tools-children");
|
||||
export const inspectEl = supportChildren
|
||||
.find("a")
|
||||
.withAttribute("href", "/tools/inspect");
|
||||
|
||||
export const inspect_volume_input = Selector('[data-test-id="inspect_volume"]');
|
||||
export const inspect_path_input = Selector('[data-test-id="inspect_path"]');
|
||||
|
||||
export const inspect_volume_input_err = Selector("#inspect_volume-helper-text");
|
||||
export const inspect_path_input_err = Selector("#inspect_path-helper-text");
|
||||
|
||||
export const inspect_encrypt_input = Selector(
|
||||
'[data-test-id="inspect_encrypt"]'
|
||||
);
|
||||
export const inspect_form_clear_btn = Selector(
|
||||
'[data-test-id="inspect-clear-button"]'
|
||||
);
|
||||
export const inspect_form_submit_btn = Selector(
|
||||
'[data-test-id="inspect-submit-button"]'
|
||||
);
|
||||
/** Begin Allowed Policy Test **/
|
||||
|
||||
export const inspectAllowedRole = Role(
|
||||
loginUrl,
|
||||
async (t) => {
|
||||
await t
|
||||
.typeText("#accessKey", insAllowedAccKey)
|
||||
.typeText("#secretKey", insAllowedSeckey)
|
||||
.click(loginSubmitBtn);
|
||||
},
|
||||
{ preserveUrl: true }
|
||||
);
|
||||
|
||||
fixture("For user with Inspect permissions")
|
||||
.page(testDomainUrl)
|
||||
.beforeEach(async (t) => {
|
||||
await t.useRole(inspectAllowedRole);
|
||||
});
|
||||
|
||||
test("Inspect page can be opened", async (t) => {
|
||||
await t.navigateTo(inspectScreenUrl);
|
||||
});
|
||||
test("Inspect sidebar item exists", async (t) => {
|
||||
await t.expect(supportSidebarEl.exists).ok();
|
||||
});
|
||||
|
||||
test("Inspect link exists in Menu list", async (t) => {
|
||||
await t
|
||||
.expect(supportSidebarEl.exists)
|
||||
.ok()
|
||||
.click(supportSidebarEl)
|
||||
.expect(inspectEl.exists)
|
||||
.ok();
|
||||
});
|
||||
|
||||
test("Form Input states verification", async (t) => {
|
||||
const volumeValue = "test";
|
||||
const pathValue = "test.txt/xl.meta";
|
||||
|
||||
await t.navigateTo(inspectScreenUrl);
|
||||
|
||||
//Initial state verification
|
||||
await t.expect(inspect_volume_input.value).eql("");
|
||||
await t.expect(inspect_path_input.value).eql("");
|
||||
await t.expect(inspect_form_submit_btn.hasAttribute("disabled")).ok();
|
||||
await t.expect(inspect_encrypt_input.hasAttribute("checked")).ok();
|
||||
await t
|
||||
.expect(inspect_volume_input_err.innerText)
|
||||
.eql("This field is required");
|
||||
await t
|
||||
.expect(inspect_path_input_err.innerText)
|
||||
.eql("This field is required");
|
||||
|
||||
//Enter form values
|
||||
await t.typeText(inspect_volume_input, "/").typeText(inspect_path_input, "/");
|
||||
|
||||
//verify post state of Invalid values
|
||||
await t.expect(inspect_volume_input.value).eql("/");
|
||||
await t.expect(inspect_path_input.value).eql("/");
|
||||
await t
|
||||
.expect(inspect_volume_input_err.innerText)
|
||||
.eql("Volume/Bucket name cannot start with /");
|
||||
await t
|
||||
.expect(inspect_path_input_err.innerText)
|
||||
.eql("Path cannot start with /");
|
||||
await t.expect(inspect_form_submit_btn.hasAttribute("disabled")).eql(true);
|
||||
await t.expect(inspect_form_clear_btn.hasAttribute("disabled")).eql(false);
|
||||
|
||||
//Important. Testcafe's way to clear input values.
|
||||
await t.selectText(inspect_volume_input).pressKey("delete");
|
||||
await t.selectText(inspect_path_input).pressKey("delete");
|
||||
|
||||
//Enter Valid form values
|
||||
await t
|
||||
.typeText(inspect_volume_input, volumeValue)
|
||||
.typeText(inspect_path_input, pathValue);
|
||||
|
||||
//verify post state of valid values
|
||||
await t.expect(inspect_volume_input.value).eql(volumeValue);
|
||||
await t.expect(inspect_path_input.value).eql(pathValue);
|
||||
await t.expect(inspect_volume_input_err.exists).notOk();
|
||||
await t.expect(inspect_path_input_err.exists).notOk();
|
||||
|
||||
await t.click(inspect_form_clear_btn);
|
||||
//reset state verification
|
||||
await t.expect(inspect_volume_input.value).eql("");
|
||||
await t.expect(inspect_path_input.value).eql("");
|
||||
await t.expect(inspect_form_submit_btn.hasAttribute("disabled")).eql(true);
|
||||
});
|
||||
/** End Allowed Policy Test **/
|
||||
|
||||
/** Begin Not Allowed Policy Test **/
|
||||
|
||||
export const inspectNotAllowedRole = Role(
|
||||
loginUrl,
|
||||
async (t) => {
|
||||
await t
|
||||
.typeText("#accessKey", insNotAllowedAccKey)
|
||||
.typeText("#secretKey", insNotAllowedSeckey)
|
||||
.click(loginSubmitBtn);
|
||||
},
|
||||
{ preserveUrl: true }
|
||||
);
|
||||
|
||||
fixture("For user with Denied Inspect permissions")
|
||||
.page(testDomainUrl)
|
||||
.beforeEach(async (t) => {
|
||||
await t.useRole(inspectNotAllowedRole);
|
||||
});
|
||||
|
||||
test("Inspect page can NOT be opened", async (t) => {
|
||||
try {
|
||||
await t.navigateTo(inspectScreenUrl);
|
||||
} catch (e) {
|
||||
await t.expect(e).ok();
|
||||
}
|
||||
});
|
||||
|
||||
test("Inspect link should NOT exists in Menu list", async (t) => {
|
||||
await t
|
||||
.expect(supportSidebarEl.exists)
|
||||
.ok()
|
||||
.click(supportSidebarEl)
|
||||
.expect(inspectEl.exists)
|
||||
.notOk(
|
||||
"Inspect Link should not exist in the menu list as per inspect not allowed policy"
|
||||
);
|
||||
});
|
||||
/** End Not Allowed Policy Test **/
|
||||
22
portal-ui/tests/policies/inspect-allowed.json
Normal file
22
portal-ui/tests/policies/inspect-allowed.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"admin:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Sid": "Allow_Admin_Actions"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": "Allow_S3_Actions"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
portal-ui/tests/policies/inspect-not-allowed.json
Normal file
22
portal-ui/tests/policies/inspect-not-allowed.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Action": [
|
||||
"admin:*"
|
||||
],
|
||||
"Effect": "Deny",
|
||||
"Sid": "Deny_Admin_Actions"
|
||||
},
|
||||
{
|
||||
"Action": [
|
||||
"s3:*"
|
||||
],
|
||||
"Effect": "Allow",
|
||||
"Resource": [
|
||||
"arn:aws:s3:::*"
|
||||
],
|
||||
"Sid": "Allow_S3_Actions"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23,6 +23,8 @@ remove_users() {
|
||||
mc admin user remove minio trace-$TIMESTAMP
|
||||
mc admin user remove minio users-$TIMESTAMP
|
||||
mc admin user remove minio watch-$TIMESTAMP
|
||||
mc admin user remove minio inspect-allowed-$TIMESTAMP
|
||||
mc admin user remove minio inspect-not-allowed-$TIMESTAMP
|
||||
}
|
||||
|
||||
remove_policies() {
|
||||
@@ -41,6 +43,8 @@ remove_policies() {
|
||||
mc admin policy remove minio trace-$TIMESTAMP
|
||||
mc admin policy remove minio users-$TIMESTAMP
|
||||
mc admin policy remove minio watch-$TIMESTAMP
|
||||
mc admin policy remove minio inspect-allowed-$TIMESTAMP
|
||||
mc admin policy remove minio inspect-not-allowed-$TIMESTAMP
|
||||
}
|
||||
|
||||
__init__() {
|
||||
|
||||
@@ -40,6 +40,8 @@ create_policies() {
|
||||
mc admin policy add minio users-$TIMESTAMP portal-ui/tests/policies/users.json
|
||||
mc admin policy add minio watch-$TIMESTAMP portal-ui/tests/policies/watch.json
|
||||
mc admin policy add minio bucketwriteprefixonlypolicy-$TIMESTAMP portal-ui/tests/policies/bucketWritePrefixOnlyPolicy.json
|
||||
mc admin policy add minio inspect-allowed-$TIMESTAMP portal-ui/tests/policies/inspect-allowed.json
|
||||
mc admin policy add minio inspect-not-allowed-$TIMESTAMP portal-ui/tests/policies/inspect-not-allowed.json
|
||||
}
|
||||
|
||||
create_users() {
|
||||
@@ -60,6 +62,8 @@ create_users() {
|
||||
mc admin user add minio users-$TIMESTAMP users1234
|
||||
mc admin user add minio watch-$TIMESTAMP watch1234
|
||||
mc admin user add minio bucketwriteprefixonlypolicy-$TIMESTAMP bucketwriteprefixonlypolicy
|
||||
mc admin user add minio inspect-allowed-$TIMESTAMP insallowed1234
|
||||
mc admin user add minio inspect-not-allowed-$TIMESTAMP insnotallowed1234
|
||||
}
|
||||
|
||||
create_buckets() {
|
||||
@@ -84,4 +88,6 @@ assign_policies() {
|
||||
mc admin policy set minio users-$TIMESTAMP user=users-$TIMESTAMP
|
||||
mc admin policy set minio watch-$TIMESTAMP user=watch-$TIMESTAMP
|
||||
mc admin policy set minio bucketwriteprefixonlypolicy-$TIMESTAMP user=bucketwriteprefixonlypolicy-$TIMESTAMP
|
||||
mc admin policy set minio inspect-allowed-$TIMESTAMP user=inspect-allowed-$TIMESTAMP
|
||||
mc admin policy set minio inspect-not-allowed-$TIMESTAMP user=inspect-not-allowed-$TIMESTAMP
|
||||
}
|
||||
@@ -32,6 +32,8 @@ remove_users() {
|
||||
mc admin user remove minio users-$TIMESTAMP
|
||||
mc admin user remove minio watch-$TIMESTAMP
|
||||
mc admin user remove minio bucketwriteprefixonlypolicy-$TIMESTAMP
|
||||
mc admin user remove minio inspect-allowed-$TIMESTAMP
|
||||
mc admin user remove minio inspect-not-allowed-$TIMESTAMP
|
||||
}
|
||||
|
||||
remove_policies() {
|
||||
@@ -51,6 +53,8 @@ remove_policies() {
|
||||
mc admin policy remove minio users-$TIMESTAMP
|
||||
mc admin policy remove minio watch-$TIMESTAMP
|
||||
mc admin policy remove minio bucketwriteprefixonlypolicy-$TIMESTAMP
|
||||
mc admin policy remove minio inspect-allowed-$TIMESTAMP
|
||||
mc admin policy remove minio inspect-not-allowed-$TIMESTAMP
|
||||
}
|
||||
|
||||
remove_buckets() {
|
||||
|
||||
Reference in New Issue
Block a user