Add KMS UI (#2377)

Adds components to interact with KMS server connected to minio
This commit is contained in:
Javier Adriel
2023-01-11 12:57:53 -06:00
committed by GitHub
parent 9edb579156
commit 1bad4c3da6
21 changed files with 1658 additions and 100 deletions

View File

@@ -36,6 +36,9 @@ type KmsLatencyHistogram struct {
// duration
Duration int64 `json:"duration,omitempty"`
// total
Total int64 `json:"total,omitempty"`
}
// Validate validates this kms latency histogram

View File

@@ -24,6 +24,7 @@ package models
import (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
@@ -57,7 +58,7 @@ type KmsMetricsResponse struct {
// latency histogram
// Required: true
LatencyHistogram *KmsLatencyHistogram `json:"latencyHistogram"`
LatencyHistogram []*KmsLatencyHistogram `json:"latencyHistogram"`
// request active
// Required: true
@@ -196,15 +197,22 @@ func (m *KmsMetricsResponse) validateLatencyHistogram(formats strfmt.Registry) e
return err
}
if m.LatencyHistogram != nil {
if err := m.LatencyHistogram.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("latencyHistogram")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("latencyHistogram")
}
return err
for i := 0; i < len(m.LatencyHistogram); i++ {
if swag.IsZero(m.LatencyHistogram[i]) { // not required
continue
}
if m.LatencyHistogram[i] != nil {
if err := m.LatencyHistogram[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("latencyHistogram" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("latencyHistogram" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
@@ -298,15 +306,19 @@ func (m *KmsMetricsResponse) ContextValidate(ctx context.Context, formats strfmt
func (m *KmsMetricsResponse) contextValidateLatencyHistogram(ctx context.Context, formats strfmt.Registry) error {
if m.LatencyHistogram != nil {
if err := m.LatencyHistogram.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("latencyHistogram")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("latencyHistogram")
for i := 0; i < len(m.LatencyHistogram); i++ {
if m.LatencyHistogram[i] != nil {
if err := m.LatencyHistogram[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("latencyHistogram" + "." + strconv.Itoa(i))
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("latencyHistogram" + "." + strconv.Itoa(i))
}
return err
}
return err
}
}
return nil

View File

@@ -113,6 +113,26 @@ export const IAM_SCOPES = {
ADMIN_INSPECT_DATA: "admin:InspectData",
S3_ALL_ACTIONS: "s3:*",
ADMIN_ALL_ACTIONS: "admin:*",
KMS_ALL_ACTIONS: "kms:*",
KMS_STATUS: "kms:Status",
KMS_METRICS: "kms:Metrics",
KMS_APIS: "kms:API",
KMS_Version: "kms:Version",
KMS_CREATE_KEY: "kms:CreateKey",
KMS_DELETE_KEY: "kms:DeleteKey",
KMS_LIST_KEYS: "kms:ListKeys",
KMS_IMPORT_KEY: "kms:ImportKey",
KMS_KEY_STATUS: "kms:KeyStatus",
KMS_DESCRIBE_POLICY: "kms:DescribePolicy",
KMS_ASSIGN_POLICY: "kms:AssignPolicy",
KMS_DELETE_POLICY: "kms:DeletePolicy",
KMS_SET_POLICY: "kms:SetPolicy",
KMS_GET_POLICY: "kms:GetPolicy",
KMS_LIST_POLICIES: "kms:ListPolicies",
KMS_DESCRIBE_IDENTITY: "kms:DescribeIdentity",
KMS_DESCRIBE_SELF_IDENTITY: "kms:DescribeSelfIdentity",
KMS_DELETE_IDENTITY: "kms:DeleteIdentity",
KMS_LIST_IDENTITIES: "kms:ListIdentities",
};
export const IAM_PAGES = {
@@ -160,6 +180,13 @@ export const IAM_PAGES = {
/* Health */
HEALTH: "/health",
/* KMS */
KMS: "/kms",
KMS_STATUS: "/kms/status",
KMS_KEYS: "/kms/keys",
KMS_KEYS_ADD: "/kms/add-key/",
KMS_KEYS_IMPORT: "/kms/import-key/",
/* Support */
TOOLS: "/support",
REGISTER_SUPPORT: "/support/register",
@@ -454,6 +481,24 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.KMS]: [IAM_SCOPES.KMS_ALL_ACTIONS],
[IAM_PAGES.KMS_STATUS]: [IAM_SCOPES.KMS_ALL_ACTIONS, IAM_SCOPES.KMS_STATUS],
[IAM_PAGES.KMS_KEYS]: [
IAM_SCOPES.KMS_ALL_ACTIONS,
IAM_SCOPES.KMS_CREATE_KEY,
IAM_SCOPES.KMS_DELETE_KEY,
IAM_SCOPES.KMS_LIST_KEYS,
IAM_SCOPES.KMS_IMPORT_KEY,
IAM_SCOPES.KMS_KEY_STATUS,
],
[IAM_PAGES.KMS_KEYS_ADD]: [
IAM_SCOPES.KMS_ALL_ACTIONS,
IAM_SCOPES.KMS_CREATE_KEY,
],
[IAM_PAGES.KMS_KEYS_IMPORT]: [
IAM_SCOPES.KMS_ALL_ACTIONS,
IAM_SCOPES.KMS_IMPORT_KEY,
],
[IAM_PAGES.IDP_LDAP_CONFIGURATIONS]: [
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,

View File

@@ -0,0 +1,37 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const EncryptionIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="255.209"
height="255.209"
viewBox="0 0 255.209 255.209"
className={`min-icon`}
fill={"currentcolor"}
{...props}
>
<path
id="KMS"
d="M175.664,255.209V228.695H79.546v26.515H46.4V228.695H3a3,3,0,0,1-3-3V3A3,3,0,0,1,3,0H252.21a3,3,0,0,1,3,3V225.694a3,3,0,0,1-3,3h-43.4v26.515ZM23.2,29.83V198.865a9.954,9.954,0,0,0,9.943,9.943H222.065a9.954,9.954,0,0,0,9.943-9.943V29.83a9.954,9.954,0,0,0-9.943-9.943H33.144A9.954,9.954,0,0,0,23.2,29.83ZM222.065,198.866h0Zm-188.921,0V29.83H222.065V198.865H33.144ZM69.224,88.258a26.52,26.52,0,1,0,34.909,34.375h33.071a2,2,0,0,0,2-2V104.747a2,2,0,0,0-2-2H104.134A26.545,26.545,0,0,0,69.224,88.258ZM59.659,112.69a19.886,19.886,0,1,1,19.886,19.886A19.887,19.887,0,0,1,59.659,112.69Z"
/>
</svg>
);
export default EncryptionIcon;

View File

@@ -0,0 +1,38 @@
// 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 * as React from "react";
import { SVGProps } from "react";
const EncryptionStatusIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="256"
height="162.281"
viewBox="0 0 256 162.281"
className={`min-icon`}
fill={"currentcolor"}
{...props}
>
<path
id="KMS-status"
d="M-13110.45-17976.135a8.3,8.3,0,0,1-7.6-4.979l-30.661-70.426h-41.776a8.3,8.3,0,0,1-8.292-8.3,8.3,8.3,0,0,1,8.292-8.3h47.211a8.289,8.289,0,0,1,7.6,4.98l23.306,53.533,32.412-122.619a8.3,8.3,0,0,1,8.017-6.178h.074a8.293,8.293,0,0,1,7.978,6.336l23.061,94.307,25.367-45.307a8.267,8.267,0,0,1,7.232-4.254c.136,0,.276,0,.416.01a8.315,8.315,0,0,1,7.189,4.979l20.733,47.732h28.818a8.292,8.292,0,0,1,8.293,8.287,8.294,8.294,0,0,1-8.293,8.3h-34.254a8.273,8.273,0,0,1-7.6-4.988l-16.239-37.379-27.48,49.107a8.274,8.274,0,0,1-7.233,4.244,9.94,9.94,0,0,1-1.12-.07,8.309,8.309,0,0,1-6.936-6.258l-20.317-83.1-30.171,114.166a8.3,8.3,0,0,1-7.387,6.152C-13110.021-17976.143-13110.24-17976.135-13110.45-17976.135Z"
transform="translate(13198.776 18138.416)"
/>
</svg>
);
export default EncryptionStatusIcon;

View File

@@ -0,0 +1,95 @@
// 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 { DialogContentText, Grid } from "@mui/material";
import React, { useState } from "react";
import { ErrorResponseHandler } from "../../../../common/types";
import { useAppDispatch } from "../../../../store";
import { setErrorSnackMessage } from "../../../../systemSlice";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import useApi from "../../Common/Hooks/useApi";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import KMSHelpBox from "../../KMS/KMSHelpbox";
interface IAddKeyModalProps {
closeAddModalAndRefresh: (refresh: boolean) => void;
addOpen: boolean;
}
const AddKeyModal = ({
closeAddModalAndRefresh,
addOpen,
}: IAddKeyModalProps) => {
const dispatch = useAppDispatch();
const onAddSuccess = () => closeAddModalAndRefresh(true);
const onAddError = (err: ErrorResponseHandler) => {
closeAddModalAndRefresh(false);
dispatch(setErrorSnackMessage(err));
};
const onClose = () => closeAddModalAndRefresh(false);
const [addLoading, invokeAddApi] = useApi(onAddSuccess, onAddError);
const [keyName, setKeyName] = useState<string>("");
const onConfirmAdd = () => {
invokeAddApi("POST", "/api/v1/kms/keys/", { key: keyName });
};
return (
<ConfirmDialog
title={""}
confirmText={"Create"}
isOpen={addOpen}
isLoading={addLoading}
onConfirm={onConfirmAdd}
onClose={onClose}
confirmButtonProps={{
disabled: keyName.indexOf(" ") !== -1 || keyName === "" || addLoading,
variant: "callAction",
}}
confirmationContent={
<DialogContentText>
<KMSHelpBox
helpText={"Create Key"}
contents={[
"Create a new cryptographic key in the Key Management Service server connected to MINIO.",
]}
/>
<Grid item xs={12} marginTop={3}>
<InputBoxWrapper
id="key-name"
name="key-name"
label="Key Name"
autoFocus={true}
value={keyName}
error={
keyName.indexOf(" ") !== -1
? "Key name cannot contain spaces"
: ""
}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyName(e.target.value);
}}
/>
</Grid>
</DialogContentText>
}
/>
);
};
export default AddKeyModal;

View File

@@ -14,10 +14,10 @@
// 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 React, { useEffect, useState } from "react";
import React, { Fragment, useEffect, useState } from "react";
import Grid from "@mui/material/Grid";
import { LinearProgress, SelectChangeEvent } from "@mui/material";
import { Button } from "mds";
import { AddIcon, Button } from "mds";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
@@ -29,12 +29,18 @@ import { BucketEncryptionInfo } from "../types";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { BucketEncryptionIcon } from "mds";
import { setModalErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../../common/SecureComponent";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import AddKeyModal from "./AddKeyModal";
const styles = (theme: Theme) =>
createStyles({
@@ -62,6 +68,9 @@ const EnableBucketEncryption = ({
const [loading, setLoading] = useState<boolean>(false);
const [kmsKeyID, setKmsKeyID] = useState<string>("");
const [encryptionType, setEncryptionType] = useState<string>("disabled");
const [keys, setKeys] = useState<[]>([]);
const [loadingKeys, setLoadingKeys] = useState<boolean>(false);
const [addOpen, setAddOpen] = useState<boolean>(false);
useEffect(() => {
if (encryptionCfg) {
@@ -74,6 +83,21 @@ const EnableBucketEncryption = ({
}
}, [encryptionCfg]);
useEffect(() => {
if (encryptionType === "sse-kms") {
api
.invoke("GET", `/api/v1/kms/keys`)
.then((res: any) => {
setKeys(res.results);
setLoadingKeys(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingKeys(false);
dispatch(setModalErrorSnackMessage(err));
});
}
}, [encryptionType, loadingKeys, dispatch]);
const enableBucketEncryption = (event: React.FormEvent) => {
event.preventDefault();
if (loading) {
@@ -108,90 +132,132 @@ const EnableBucketEncryption = ({
};
return (
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Enable Bucket Encryption"
titleIcon={<BucketEncryptionIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
enableBucketEncryption(e);
<Fragment>
{addOpen && (
<AddKeyModal
addOpen={addOpen}
closeAddModalAndRefresh={(refresh: boolean) => {
setAddOpen(false);
setLoadingKeys(true);
}}
/>
)}
<ModalWrapper
modalOpen={open}
onClose={() => {
closeModalAndRefresh();
}}
title="Enable Bucket Encryption"
titleIcon={<BucketEncryptionIcon />}
>
<Grid container>
<Grid item xs={12} className={classes.modalFormScrollable}>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
setEncryptionType(e.target.value as string);
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
enableBucketEncryption(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.modalFormScrollable}>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
setEncryptionType(e.target.value as string);
}}
id="select-encryption-type"
name="select-encryption-type"
label={"Encryption Type"}
value={encryptionType}
options={[
{
label: "Disabled",
value: "disabled",
},
{
label: "SSE-S3",
value: "sse-s3",
},
{
label: "SSE-KMS",
value: "sse-kms",
},
]}
/>
</Grid>
{encryptionType === "sse-kms" && (
<Grid
item
xs={12}
className={classes.formFieldRow}
display={"flex"}
>
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
setKmsKeyID(e.target.value);
}}
id="select-kms-key-id"
name="select-kms-key-id"
label={"KMS Key ID"}
value={kmsKeyID}
options={keys.map((key: any) => {
return {
label: key.name,
value: key.name,
};
})}
/>
<Grid marginLeft={1}>
<SecureComponent
scopes={[IAM_SCOPES.KMS_IMPORT_KEY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Add key"}>
<Button
id={"import-key"}
variant={"regular"}
icon={<AddIcon />}
onClick={(e) => {
setAddOpen(true);
e.preventDefault();
}}
/>
</TooltipWrapper>
</SecureComponent>
</Grid>
</Grid>
)}
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
id={"cancel"}
type="submit"
variant="regular"
onClick={() => {
closeModalAndRefresh();
}}
id="select-encryption-type"
name="select-encryption-type"
label={"Encryption Type"}
value={encryptionType}
options={[
{
label: "Disabled",
value: "disabled",
},
{
label: "SSE-S3",
value: "sse-s3",
},
{
label: "SSE-KMS",
value: "sse-kms",
},
]}
disabled={loading}
label={"Cancel"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={loading}
label={"Save"}
/>
</Grid>
{encryptionType === "sse-kms" && (
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="kms-key-id"
name="kms-key-id"
label="KMS Key ID"
value={kmsKeyID}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKmsKeyID(e.target.value);
}}
/>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
id={"cancel"}
type="submit"
variant="regular"
onClick={() => {
closeModalAndRefresh();
}}
disabled={loading}
label={"Cancel"}
/>
<Button
id={"save"}
type="submit"
variant="callAction"
disabled={loading}
label={"Save"}
/>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
</form>
</ModalWrapper>
</Fragment>
);
};

View File

@@ -13,7 +13,7 @@
//
// 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 React, { useState } from "react";
import React, { ClipboardEvent, useState } from "react";
import {
Grid,
IconButton,
@@ -44,6 +44,7 @@ interface InputBoxProps {
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyPress?: (e: any) => void;
onFocus?: () => void;
onPaste?: (e: ClipboardEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
@@ -137,6 +138,7 @@ const InputBoxWrapper = ({
className = "",
onKeyPress,
onFocus,
onPaste,
}: InputBoxProps) => {
let inputProps: any = { "data-index": index, ...extraInputProps };
const [toggleTextInput, setToggleTextInput] = useState<boolean>(false);
@@ -216,6 +218,7 @@ const InputBoxWrapper = ({
className={classes.inputRebase}
onKeyPress={onKeyPress}
onFocus={onFocus}
onPaste={onPaste}
/>
{inputBoxWrapperIcon && (
<div

View File

@@ -172,6 +172,8 @@ const DirectPVDrives = React.lazy(() => import("./DirectPV/DirectPVDrives"));
const DirectPVVolumes = React.lazy(() => import("./DirectPV/DirectPVVolumes"));
const KMSRoutes = React.lazy(() => import("./KMS/KMSRoutes"));
const styles = (theme: Theme) =>
createStyles({
root: {
@@ -236,6 +238,7 @@ const Console = ({ classes }: IConsoleProps) => {
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
const kmsIsEnabled = (features && features.includes("kms")) || false;
const obOnly = !!features?.includes("object-browser-only");
const restartServer = () => {
@@ -475,6 +478,11 @@ const Console = ({ classes }: IConsoleProps) => {
path: IAM_PAGES.LICENSE,
forceDisplay: true,
},
{
component: KMSRoutes,
path: IAM_PAGES.KMS,
fsHidden: !kmsIsEnabled,
},
];
const operatorConsoleRoutes: IRouteRule[] = [

View File

@@ -0,0 +1,50 @@
// 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 React, { Fragment } from "react";
import Grid from "@mui/material/Grid";
import PageHeader from "../Common/PageHeader/PageHeader";
import BackLink from "../../../common/BackLink";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { ErrorResponseHandler } from "../../../common/types";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useNavigate } from "react-router-dom";
import { useAppDispatch } from "../../../store";
import AddKeyForm from "./AddKeyForm";
const AddKey = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const onSuccess = () => navigate(`${IAM_PAGES.KMS_KEYS}`);
const onError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
return (
<Fragment>
<Grid item xs={12}>
<PageHeader
label={<BackLink to={IAM_PAGES.KMS_KEYS} label={"Keys"} />}
/>
<AddKeyForm onError={onError} onSuccess={onSuccess} />
</Grid>
</Fragment>
);
};
export default AddKey;

View File

@@ -0,0 +1,125 @@
// 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 React, { useState } from "react";
import { Box } from "@mui/material";
import Grid from "@mui/material/Grid";
import { AddAccessRuleIcon, Button } from "mds";
import PageLayout from "../Common/Layout/PageLayout";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ErrorResponseHandler } from "../../../common/types";
import FormLayout from "../Common/FormLayout";
import useApi from "../Common/Hooks/useApi";
import KMSHelpBox from "./KMSHelpbox";
interface IAddKeyFormProps {
onSuccess: () => void;
onError: (err: ErrorResponseHandler) => void;
}
const AddKeyForm = ({ onSuccess, onError }: IAddKeyFormProps) => {
const [loading, invokeApi] = useApi(onSuccess, onError);
const [keyName, setKeyName] = useState<string>("");
const addRecord = (event: React.FormEvent) => {
event.preventDefault();
invokeApi("POST", "/api/v1/kms/keys/", { key: keyName });
};
const resetForm = () => {
setKeyName("");
};
const validateKeyName = (keyName: string) => {
if (keyName.indexOf(" ") !== -1) {
return "Key name cannot contain spaces";
} else return "";
};
const validSave = keyName.trim() !== "" && keyName.indexOf(" ") === -1;
return (
<PageLayout>
<FormLayout
title={"Create Key"}
icon={<AddAccessRuleIcon />}
helpbox={
<KMSHelpBox
helpText={"Encryption Key"}
contents={[
"Create a new cryptographic key in the Key Management Service server connected to MINIO.",
]}
/>
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addRecord(e);
}}
>
<Grid container item spacing={1}>
<Grid item xs={12}>
<InputBoxWrapper
id="key-name"
name="key-name"
label="Key Name"
autoFocus={true}
value={keyName}
error={validateKeyName(keyName)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12} textAlign={"right"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"save-key"}
type="submit"
variant="callAction"
color="primary"
disabled={loading || !validSave}
label={"Save"}
/>
</Box>
</Grid>
</Grid>
</form>
</FormLayout>
</PageLayout>
);
};
export default AddKeyForm;

View File

@@ -0,0 +1,102 @@
// 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 React, { useState } from "react";
import { DialogContentText, Grid } from "@mui/material";
import { ErrorResponseHandler } from "../../../common/types";
import useApi from "../Common/Hooks/useApi";
import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import WarningMessage from "../Common/WarningMessage/WarningMessage";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ConfirmDeleteIcon } from "mds";
interface IDeleteKMSModalProps {
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedItem: string;
endpoint: string;
element: string;
}
const DeleteKMSModal = ({
closeDeleteModalAndRefresh,
deleteOpen,
selectedItem,
endpoint,
element,
}: IDeleteKMSModalProps) => {
const dispatch = useAppDispatch();
const onDelSuccess = () => closeDeleteModalAndRefresh(true);
const onDelError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const onClose = () => closeDeleteModalAndRefresh(false);
const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError);
const [retypeKey, setRetypeKey] = useState("");
if (!selectedItem) {
return null;
}
const onConfirmDelete = () => {
invokeDeleteApi("DELETE", `${endpoint}${selectedItem}`);
};
return (
<ConfirmDialog
title={`Delete ${element}`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmButtonProps={{
disabled: retypeKey !== selectedItem || deleteLoading,
}}
confirmationContent={
<DialogContentText>
<Grid item xs={12}>
<WarningMessage
title={"WARNING"}
label={
"Please note that this is a dangerous operation. Once a key has been deleted all data that has been encrypted with it cannot be decrypted anymore, and therefore, is lost."
}
/>
</Grid>
To continue please type <b>{selectedItem}</b> in the box.
<Grid item xs={12}>
<InputBoxWrapper
id="retype-key"
name="retype-key"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setRetypeKey(event.target.value);
}}
onPaste={(e) => e.preventDefault()}
label=""
value={retypeKey}
/>
</Grid>
</DialogContentText>
}
/>
);
};
export default DeleteKMSModal;

View File

@@ -0,0 +1,157 @@
// 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 React, { Fragment, useState } from "react";
import { Box } from "@mui/material";
import Grid from "@mui/material/Grid";
import { AddAccessRuleIcon, Button } from "mds";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import BackLink from "../../../common/BackLink";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { ErrorResponseHandler } from "../../../common/types";
import FormLayout from "../Common/FormLayout";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useNavigate } from "react-router-dom";
import { useAppDispatch } from "../../../store";
import useApi from "../Common/Hooks/useApi";
import KMSHelpBox from "./KMSHelpbox";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
export const emptyContent = '{\n "bytes": ""\n}';
const ImportKey = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const onSuccess = () => navigate(`${IAM_PAGES.KMS_KEYS}`);
const onError = (err: ErrorResponseHandler) =>
dispatch(setErrorSnackMessage(err));
const [loading, invokeApi] = useApi(onSuccess, onError);
const [keyName, setKeyName] = useState<string>("");
const [keyContent, setKeyContent] = useState<string>(emptyContent);
const importRecord = (event: React.FormEvent) => {
event.preventDefault();
let data = JSON.parse(keyContent);
invokeApi("POST", `/api/v1/kms/keys/${keyName}/import`, data);
};
const resetForm = () => {
setKeyName("");
setKeyContent("");
};
const validateKeyName = (keyName: string) => {
if (keyName.indexOf(" ") !== -1) {
return "Key name cannot contain spaces";
} else return "";
};
const validSave = keyName.trim() !== "" && keyName.indexOf(" ") === -1;
return (
<Fragment>
<Grid item xs={12}>
<PageHeader
label={<BackLink to={IAM_PAGES.KMS_KEYS} label={"Keys"} />}
/>
<PageLayout>
<FormLayout
title={"Import Key"}
icon={<AddAccessRuleIcon />}
helpbox={
<KMSHelpBox
helpText={"Encryption Key"}
contents={[
"Import a cryptographic key in the Key Management Service server connected to MINIO.",
]}
/>
}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
importRecord(e);
}}
>
<Grid container item spacing={1}>
<Grid item xs={12}>
<InputBoxWrapper
id="key-name"
name="key-name"
label="Key Name"
autoFocus={true}
value={keyName}
error={validateKeyName(keyName)}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setKeyName(e.target.value);
}}
/>
</Grid>
<Grid item xs={12}>
<CodeMirrorWrapper
label={"Set key Content"}
value={keyContent}
onBeforeChange={(editor, data, value) => {
setKeyContent(value);
}}
editorHeight={"350px"}
/>
</Grid>
<Grid item xs={12} textAlign={"right"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
id={"clear"}
type="button"
variant="regular"
onClick={resetForm}
label={"Clear"}
/>
<Button
id={"import-key"}
type="submit"
variant="callAction"
color="primary"
disabled={loading || !validSave}
label={"Import"}
/>
</Box>
</Grid>
</Grid>
</form>
</FormLayout>
</PageLayout>
</Grid>
</Fragment>
);
};
export default ImportKey;

View File

@@ -0,0 +1,51 @@
import React from "react";
import { Box } from "@mui/material";
import { HelpIconFilled } from "mds";
interface IKMSHelpBoxProps {
helpText: string;
contents: string[];
}
const KMSHelpBox = ({ helpText, contents }: IKMSHelpBoxProps) => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
paddingBottom: "20px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>{helpText}</div>
</Box>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
{contents.map((content) => (
<Box sx={{ paddingBottom: "20px" }}>{content}</Box>
))}
</Box>
</Box>
);
};
export default KMSHelpBox;

View File

@@ -0,0 +1,40 @@
// 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 React from "react";
import { Route, Routes } from "react-router-dom";
import withSuspense from "../Common/Components/withSuspense";
import NotFoundPage from "../../NotFoundPage";
const Status = withSuspense(React.lazy(() => import("./Status")));
const ListKeys = withSuspense(React.lazy(() => import("./ListKeys")));
const AddKey = withSuspense(React.lazy(() => import("./AddKey")));
const ImportKey = withSuspense(React.lazy(() => import("./ImportKey")));
const KMSRoutes = () => {
return (
<Routes>
<Route path={"status"} element={<Status />} />
<Route path={"keys"} element={<ListKeys />} />
<Route path={"add-key"} element={<AddKey />} />
<Route path={"import-key"} element={<ImportKey />} />
<Route path={"*"} element={<NotFoundPage />} />
</Routes>
);
};
export default KMSRoutes;

View File

@@ -0,0 +1,242 @@
// 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 { Grid, Theme } from "@mui/material";
import { createStyles, withStyles } from "@mui/styles";
import { AddIcon, Button, RefreshIcon, UploadIcon } from "mds";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import api from "../../../common/api";
import {
hasPermission,
SecureComponent,
} from "../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import { ErrorResponseHandler } from "../../../common/types";
import { useAppDispatch } from "../../../store";
import { setErrorSnackMessage } from "../../../systemSlice";
import withSuspense from "../Common/Components/withSuspense";
import {
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import PageLayout from "../Common/Layout/PageLayout";
import PageHeader from "../Common/PageHeader/PageHeader";
import SearchBox from "../Common/SearchBox";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper";
const DeleteKMSModal = withSuspense(
React.lazy(() => import("./DeleteKMSModal"))
);
const styles = (theme: Theme) =>
createStyles({
...searchField,
...containerForHeader(theme.spacing(4)),
});
interface IKeysProps {
classes: any;
}
const ListKeys = ({ classes }: IKeysProps) => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const [filter, setFilter] = useState<string>("");
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedKey, setSelectedKey] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [records, setRecords] = useState<[]>([]);
const deleteKey = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.KMS_DELETE_KEY,
]);
const displayKeys = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.KMS_LIST_KEYS,
]);
useEffect(() => {
fetchRecords();
}, []);
useEffect(() => {
setLoading(true);
}, [filter]);
useEffect(() => {
if (loading) {
if (displayKeys) {
let pattern = filter.trim() === "" ? "*" : filter.trim();
api
.invoke("GET", `/api/v1/kms/keys?pattern=${pattern}`)
.then((res) => {
setLoading(false);
setRecords(res.results);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
dispatch(setErrorSnackMessage(err));
});
} else {
setLoading(false);
}
}
}, [loading, setLoading, setRecords, dispatch, displayKeys, filter]);
const fetchRecords = () => {
setLoading(true);
};
const confirmDeleteKey = (key: string) => {
setDeleteOpen(true);
setSelectedKey(key);
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
fetchRecords();
}
};
const tableActions = [
{
type: "delete",
onClick: confirmDeleteKey,
sendOnlyId: true,
disableButtonFunction: () => !deleteKey,
},
];
return (
<React.Fragment>
{deleteOpen && (
<DeleteKMSModal
deleteOpen={deleteOpen}
selectedItem={selectedKey}
endpoint={"/api/v1/kms/keys/"}
element={"Key"}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<PageHeader label="Key Management Service Keys" />
<PageLayout className={classes.pageContainer}>
<Grid container spacing={1}>
<Grid
item
xs={12}
display={"flex"}
alignItems={"center"}
justifyContent={"flex-end"}
sx={{
"& button": {
marginLeft: "8px",
},
}}
>
<SecureComponent
scopes={[IAM_SCOPES.KMS_LIST_KEYS]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<SearchBox
onChange={setFilter}
placeholder="Search Keys with pattern"
value={filter}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.KMS_LIST_KEYS]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Refresh"}>
<Button
id={"refresh-keys"}
variant="regular"
icon={<RefreshIcon />}
onClick={() => setLoading(true)}
/>
</TooltipWrapper>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.KMS_IMPORT_KEY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Import Key"}>
<Button
id={"import-key"}
variant={"regular"}
icon={<UploadIcon />}
onClick={() => {
navigate(IAM_PAGES.KMS_KEYS_IMPORT);
}}
/>
</TooltipWrapper>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.KMS_CREATE_KEY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TooltipWrapper tooltip={"Create Key"}>
<Button
id={"create-key"}
label={"Create Key"}
variant={"callAction"}
icon={<AddIcon />}
onClick={() => navigate(IAM_PAGES.KMS_KEYS_ADD)}
/>
</TooltipWrapper>
</SecureComponent>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<SecureComponent
scopes={[IAM_SCOPES.KMS_LIST_KEYS]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Created By", elementKey: "createdBy" },
{ label: "Created At", elementKey: "createdAt" },
]}
isLoading={loading}
records={records}
entityName="Keys"
idField="name"
/>
</SecureComponent>
</Grid>
</Grid>
</PageLayout>
</React.Fragment>
);
};
export default withStyles(styles)(ListKeys);

View File

@@ -0,0 +1,435 @@
// 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 React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Box, Grid } from "@mui/material";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import api from "../../../common/api";
import { ErrorResponseHandler } from "../../../common/types";
import { hasPermission } from "../../../common/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import { setErrorSnackMessage } from "../../../systemSlice";
import { useAppDispatch } from "../../../store";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import { TabPanel } from "../../shared/tabs";
import LabelValuePair from "../Common/UsageBarWrapper/LabelValuePair";
import SectionTitle from "../Common/SectionTitle";
import LabelWithIcon from "../Buckets/BucketDetails/SummaryItems/LabelWithIcon";
import {
Bar,
BarChart,
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { DisabledIcon, EnabledIcon } from "mds";
const styles = (theme: Theme) => createStyles({});
const Status = () => {
const dispatch = useAppDispatch();
const [curTab, setCurTab] = useState<number>(0);
const [status, setStatus] = useState<any | null>(null);
const [loadingStatus, setLoadingStatus] = useState<boolean>(true);
const [metrics, setMetrics] = useState<any | null>(null);
const [loadingMetrics, setLoadingMetrics] = useState<boolean>(true);
const [apis, setAPIs] = useState<any | null>(null);
const [loadingAPIs, setLoadingAPIs] = useState<boolean>(true);
const [version, setVersion] = useState<any | null>(null);
const [loadingVersion, setLoadingVersion] = useState<boolean>(true);
const displayStatus = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.KMS_STATUS,
]);
const displayMetrics = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.KMS_METRICS,
]);
const displayAPIs = hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.KMS_APIS]);
const displayVersion = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.KMS_Version,
]);
useEffect(() => {
setLoadingStatus(true);
}, []);
useEffect(() => {
const loadMetrics = () => {
if (displayMetrics) {
api
.invoke("GET", `/api/v1/kms/metrics`)
.then((result: any) => {
if (result) {
setMetrics(result);
}
setLoadingMetrics(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingMetrics(false);
});
} else {
setLoadingMetrics(false);
}
};
const loadAPIs = () => {
if (displayAPIs) {
api
.invoke("GET", `/api/v1/kms/apis`)
.then((result: any) => {
if (result) {
setAPIs(result);
}
setLoadingAPIs(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingAPIs(false);
});
} else {
setLoadingAPIs(false);
}
};
const loadVersion = () => {
if (displayVersion) {
api
.invoke("GET", `/api/v1/kms/version`)
.then((result: any) => {
if (result) {
setVersion(result);
}
setLoadingVersion(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingVersion(false);
});
} else {
setLoadingVersion(false);
}
};
const loadStatus = () => {
if (displayStatus) {
api
.invoke("GET", `/api/v1/kms/status`)
.then((result: any) => {
if (result) {
setStatus(result);
}
setLoadingStatus(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingStatus(false);
});
} else {
setLoadingStatus(false);
}
};
if (loadingStatus) {
loadStatus();
}
if (loadingMetrics) {
loadMetrics();
}
if (loadingAPIs) {
loadAPIs();
}
if (loadingVersion) {
loadVersion();
}
}, [
dispatch,
displayStatus,
loadingStatus,
displayMetrics,
loadingMetrics,
displayAPIs,
loadingAPIs,
displayVersion,
loadingVersion,
]);
const statusPanel = (
<Fragment>
<SectionTitle>Status</SectionTitle>
<br />
{status && (
<Grid container spacing={1}>
<Grid item xs={12}>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
}}
>
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
}}
>
<LabelValuePair label={"Name:"} value={status.name} />
{version && (
<LabelValuePair label={"Version:"} value={version.version} />
)}
<LabelValuePair
label={"Default Key ID:"}
value={status.defaultKeyID}
/>
<LabelValuePair
label={"Key Management Service Endpoints:"}
value={
<Fragment>
{status.endpoints.map((e: any, i: number) => (
<LabelWithIcon
key={i}
icon={
e.status === "online" ? (
<EnabledIcon />
) : (
<DisabledIcon />
)
}
label={e.url}
/>
))}
</Fragment>
}
/>
</Box>
</Box>
</Grid>
</Grid>
)}
</Fragment>
);
const apisPanel = (
<Fragment>
<SectionTitle>Supported API endpoints</SectionTitle>
<br />
{apis && (
<Grid container spacing={1}>
<Grid item xs={12}>
<LabelValuePair
label={""}
value={
<Box
sx={{
display: "grid",
gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" },
gridAutoFlow: { xs: "dense", sm: "row" },
gap: 2,
}}
>
{apis.results.map((e: any, i: number) => (
<LabelWithIcon
key={i}
icon={<EnabledIcon />}
label={`${e.path} - ${e.method}`}
/>
))}
</Box>
}
/>
</Grid>
</Grid>
)}
</Fragment>
);
const getAPIRequestsData = () => {
return [
{ label: "Success", success: metrics.requestOK },
{ label: "Failures", failures: metrics.requestFail },
{ label: "Errors", errors: metrics.requestErr },
{ label: "Active", active: metrics.requestActive },
];
};
const getEventsData = () => {
return [
{ label: "Audit", audit: metrics.auditEvents },
{ label: "Errors", errors: metrics.errorEvents },
];
};
const getHistogramData = () => {
return metrics.latencyHistogram.map((h: any) => {
return {
...h,
duration: `${h.duration / 1000000}ms`,
};
});
};
const metricsPanel = (
<Fragment>
{metrics && (
<Fragment>
<h3>API Requests</h3>
<BarChart width={730} height={250} data={getAPIRequestsData()}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="label" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="success" fill="green" />
<Bar dataKey="failures" fill="red" />
<Bar dataKey="errors" fill="black" />
<Bar dataKey="active" fill="#8884d8" />
</BarChart>
<h3>Events</h3>
<BarChart width={730} height={250} data={getEventsData()}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="label" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="audit" fill="green" />
<Bar dataKey="errors" fill="black" />
</BarChart>
<h3>Latency Histogram</h3>
{metrics.latencyHistogram && (
<LineChart
width={730}
height={250}
data={getHistogramData()}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="duration" />
<YAxis />
<Tooltip />
<Legend />
<Line
type="monotone"
dataKey="total"
stroke="#8884d8"
name={"Requests that took T ms or less"}
/>
</LineChart>
)}
</Fragment>
)}
</Fragment>
);
return (
<Fragment>
<PageHeader label="Key Management Service" actions={<React.Fragment />} />
<PageLayout>
<Tabs
value={curTab}
onChange={(e: React.ChangeEvent<{}>, newValue: number) => {
setCurTab(newValue);
}}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
variant="scrollable"
scrollButtons="auto"
>
<Tab
label="Status"
id="simple-tab-0"
aria-controls="simple-tabpanel-0"
/>
<Tab
label="APIs"
id="simple-tab-1"
aria-controls="simple-tabpanel-1"
/>
<Tab
label="Metrics"
id="simple-tab-2"
aria-controls="simple-tabpanel-2"
onClick={() => {}}
/>
</Tabs>
<TabPanel index={0} value={curTab}>
<Box
sx={{
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{statusPanel}
</Box>
</TabPanel>
<TabPanel index={1} value={curTab}>
<Box
sx={{
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{apisPanel}
</Box>
</TabPanel>
<TabPanel index={2} value={curTab}>
<Box
sx={{
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "43px",
}}
>
{metricsPanel}
</Box>
</TabPanel>
</PageLayout>
</Fragment>
);
};
export default withStyles(styles)(Status);

View File

@@ -58,6 +58,8 @@ import {
import { hasPermission } from "../../common/SecureComponent";
import React from "react";
import LicenseBadge from "./Menu/LicenseBadge";
import EncryptionIcon from "../../icons/SidebarMenus/EncryptionIcon";
import EncryptionStatusIcon from "../../icons/SidebarMenus/EncryptionStatusIcon";
import { LockOpen, Login } from "@mui/icons-material";
export const validRoutes = (
@@ -66,6 +68,7 @@ export const validRoutes = (
directPVMode: boolean
) => {
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
const kmsIsEnabled = (features && features.includes("kms")) || false;
let consoleMenus: IMenuItem[] = [
{
group: "User",
@@ -216,6 +219,14 @@ export const validRoutes = (
icon: DrivesMenuIcon,
component: NavLink,
},
{
name: "Encryption",
id: "monitorEncryption",
to: IAM_PAGES.KMS_STATUS,
icon: EncryptionStatusIcon,
component: NavLink,
fsHidden: !kmsIsEnabled,
},
],
},
{
@@ -242,6 +253,15 @@ export const validRoutes = (
icon: RecoverIcon,
id: "sitereplication",
},
{
group: "Administrator",
component: NavLink,
to: IAM_PAGES.KMS_KEYS,
name: "Encryption",
icon: EncryptionIcon,
id: "encryption",
fsHidden: !kmsIsEnabled,
},
{
group: "Administrator",
component: NavLink,

View File

@@ -20,6 +20,7 @@ package restapi
import (
"context"
"encoding/json"
"sort"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
@@ -120,7 +121,7 @@ func kmsMetrics(ctx context.Context, minioClient MinioAdmin) (*models.KmsMetrics
RequestActive: &metrics.RequestActive,
AuditEvents: &metrics.AuditEvents,
ErrorEvents: &metrics.ErrorEvents,
LatencyHistogram: nil,
LatencyHistogram: parseHistogram(metrics.LatencyHistogram),
Uptime: &metrics.UpTime,
Cpus: &metrics.CPUs,
UsableCPUs: &metrics.UsableCPUs,
@@ -131,6 +132,17 @@ func kmsMetrics(ctx context.Context, minioClient MinioAdmin) (*models.KmsMetrics
}, nil
}
func parseHistogram(histogram map[int64]int64) (records []*models.KmsLatencyHistogram) {
for duration, total := range histogram {
records = append(records, &models.KmsLatencyHistogram{Duration: duration, Total: total})
}
cp := func(i, j int) bool {
return records[i].Duration < records[j].Duration
}
sort.Slice(records, cp)
return records
}
func GetKMSAPIsResponse(session *models.Principal, params kmsAPI.KMSAPIsParams) (*models.KmsAPIsResponse, *models.Error) {
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()

View File

@@ -6249,6 +6249,9 @@ func init() {
"properties": {
"duration": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
@@ -6319,7 +6322,10 @@ func init() {
"type": "integer"
},
"latencyHistogram": {
"$ref": "#/definitions/kmsLatencyHistogram"
"type": "array",
"items": {
"$ref": "#/definitions/kmsLatencyHistogram"
}
},
"requestActive": {
"type": "integer"
@@ -14817,6 +14823,9 @@ func init() {
"properties": {
"duration": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
},
@@ -14887,7 +14896,10 @@ func init() {
"type": "integer"
},
"latencyHistogram": {
"$ref": "#/definitions/kmsLatencyHistogram"
"type": "array",
"items": {
"$ref": "#/definitions/kmsLatencyHistogram"
}
},
"requestActive": {
"type": "integer"

View File

@@ -5677,7 +5677,9 @@ definitions:
errorEvents:
type: integer
latencyHistogram:
$ref: "#/definitions/kmsLatencyHistogram"
type: array
items:
$ref: "#/definitions/kmsLatencyHistogram"
uptime:
type: integer
cpus:
@@ -5697,6 +5699,9 @@ definitions:
properties:
duration:
type: integer
total:
type: integer
kmsAPIsResponse:
type: object
properties: