Make user details a page (#726)

This commit is contained in:
Daniel Valdivia
2021-05-08 00:00:29 -07:00
committed by GitHub
parent ffb3362f79
commit 24410e7c85
33 changed files with 1144 additions and 366 deletions

View File

@@ -1,38 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Make assets
run: make assets
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add portal-ui/build
git commit -m "Generated Assets for Change" -a
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}

View File

@@ -39,7 +39,7 @@ type User struct {
MemberOf []string `json:"memberOf"`
// policy
Policy string `json:"policy,omitempty"`
Policy []string `json:"policy"`
// status
Status string `json:"status,omitempty"`

View File

@@ -24,6 +24,7 @@ import (
var (
configuration = "/settings"
users = "/users"
usersDetail = "/users/:userName"
groups = "/groups"
iamPolicies = "/policies"
dashboard = "/dashboard"
@@ -268,6 +269,7 @@ var displayRules = map[string]func() bool{
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
users: usersActionSet,
usersDetail: usersActionSet,
groups: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
dashboard: dashboardActionSet,

View File

@@ -72,7 +72,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 19,
want: 20,
},
{
name: "all s3 endpoints",
@@ -91,7 +91,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 21,
want: 22,
},
{
name: "Console User - default endpoints",

View File

@@ -1,25 +1,25 @@
{
"files": {
"main.css": "/static/css/main.a19f3d53.chunk.css",
"main.js": "/static/js/main.33f485b0.chunk.js",
"main.js.map": "/static/js/main.33f485b0.chunk.js.map",
"main.js": "/static/js/main.7489cd3f.chunk.js",
"main.js.map": "/static/js/main.7489cd3f.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.f48e99e5.js",
"runtime-main.js.map": "/static/js/runtime-main.f48e99e5.js.map",
"static/css/2.f324abd6.chunk.css": "/static/css/2.f324abd6.chunk.css",
"static/js/2.37779a66.chunk.js": "/static/js/2.37779a66.chunk.js",
"static/js/2.37779a66.chunk.js.map": "/static/js/2.37779a66.chunk.js.map",
"static/css/2.2d55190c.chunk.css": "/static/css/2.2d55190c.chunk.css",
"static/js/2.42769b7f.chunk.js": "/static/js/2.42769b7f.chunk.js",
"static/js/2.42769b7f.chunk.js.map": "/static/js/2.42769b7f.chunk.js.map",
"index.html": "/index.html",
"static/css/2.f324abd6.chunk.css.map": "/static/css/2.f324abd6.chunk.css.map",
"static/css/2.2d55190c.chunk.css.map": "/static/css/2.2d55190c.chunk.css.map",
"static/css/main.a19f3d53.chunk.css.map": "/static/css/main.a19f3d53.chunk.css.map",
"static/js/2.37779a66.chunk.js.LICENSE.txt": "/static/js/2.37779a66.chunk.js.LICENSE.txt",
"static/js/2.42769b7f.chunk.js.LICENSE.txt": "/static/js/2.42769b7f.chunk.js.LICENSE.txt",
"static/media/minio_console_logo.0837460e.svg": "/static/media/minio_console_logo.0837460e.svg",
"static/media/minio_operator_logo.1312b7c9.svg": "/static/media/minio_operator_logo.1312b7c9.svg"
},
"entrypoints": [
"static/js/runtime-main.f48e99e5.js",
"static/css/2.f324abd6.chunk.css",
"static/js/2.37779a66.chunk.js",
"static/css/2.2d55190c.chunk.css",
"static/js/2.42769b7f.chunk.js",
"static/css/main.a19f3d53.chunk.css",
"static/js/main.33f485b0.chunk.js"
"static/js/main.7489cd3f.chunk.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.f324abd6.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.37779a66.chunk.js"></script><script src="/static/js/main.33f485b0.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="MinIO Console"/><link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet"/><link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png"/><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"/><link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png"/><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"/><link rel="manifest" href="/manifest.json"/><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#3a4e54"/><title>MinIO Console</title><link href="/static/css/2.2d55190c.chunk.css" rel="stylesheet"><link href="/static/css/main.a19f3d53.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>!function(e){function r(r){for(var n,l,i=r[0],a=r[1],p=r[2],c=0,s=[];c<i.length;c++)l=i[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n]);for(f&&f(r);s.length;)s.shift()();return u.push.apply(u,p||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,i=1;i<t.length;i++){var a=t[i];0!==o[a]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var i=this["webpackJsonpportal-ui"]=this["webpackJsonpportal-ui"]||[],a=i.push.bind(i);i.push=r,i=i.slice();for(var p=0;p<i.length;p++)r(i[p]);var f=a;t()}([])</script><script src="/static/js/2.42769b7f.chunk.js"></script><script src="/static/js/main.7489cd3f.chunk.js"></script></body></html>

View File

@@ -1,2 +1,2 @@
.ReactVirtualized__Table__headerRow{font-weight:700;text-transform:uppercase}.ReactVirtualized__Table__headerRow,.ReactVirtualized__Table__row{display:flex;flex-direction:row;align-items:center}.ReactVirtualized__Table__headerTruncatedText{display:inline-block;max-width:100%;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.ReactVirtualized__Table__headerColumn,.ReactVirtualized__Table__rowColumn{margin-right:10px;min-width:0}.ReactVirtualized__Table__rowColumn{text-overflow:ellipsis;white-space:nowrap}.ReactVirtualized__Table__headerColumn:first-of-type,.ReactVirtualized__Table__rowColumn:first-of-type{margin-left:10px}.ReactVirtualized__Table__sortableHeaderColumn{cursor:pointer}.ReactVirtualized__Table__sortableHeaderIconContainer{display:flex;align-items:center}.ReactVirtualized__Table__sortableHeaderIcon{flex:0 0 24px;height:1em;width:1em;fill:currentColor}.react-grid-layout{position:relative;transition:height .2s ease}.react-grid-item{transition:all .2s ease;transition-property:left,top}.react-grid-item img{pointer-events:none;-webkit-user-select:none;-ms-user-select:none;user-select:none}.react-grid-item.cssTransforms{transition-property:transform}.react-grid-item.resizing{z-index:1;will-change:width,height}.react-grid-item.react-draggable-dragging{transition:none;z-index:3;will-change:transform}.react-grid-item.dropping{visibility:hidden}.react-grid-item.react-grid-placeholder{background:red;opacity:.2;transition-duration:.1s;z-index:2;-webkit-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.react-grid-item>.react-resizable-handle{position:absolute;width:20px;height:20px}.react-grid-item>.react-resizable-handle:after{content:"";position:absolute;right:3px;bottom:3px;width:5px;height:5px;border-right:2px solid rgba(0,0,0,.4);border-bottom:2px solid rgba(0,0,0,.4)}.react-resizable-hide>.react-resizable-handle{display:none}.react-grid-item>.react-resizable-handle.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;transform:rotate(90deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;transform:rotate(180deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;transform:rotate(270deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e,.react-grid-item>.react-resizable-handle.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-n,.react-grid-item>.react-resizable-handle.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-grid-item>.react-resizable-handle.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-grid-item>.react-resizable-handle.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}.react-resizable{position:relative}.react-resizable-handle{position:absolute;width:20px;height:20px;background-repeat:no-repeat;background-origin:content-box;box-sizing:border-box;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOiNmZmZmZmYwMCIgd2lkdGg9IjYiIGhlaWdodD0iNiI+PHBhdGggZD0iTTYgNkgwVjQuMmg0LjJWMEg2djZ6IiBvcGFjaXR5PSIuMzAyIi8+PC9zdmc+");background-position:100% 100%;padding:0 3px 3px 0}.react-resizable-handle-sw{bottom:0;left:0;cursor:sw-resize;transform:rotate(90deg)}.react-resizable-handle-se{bottom:0;right:0;cursor:se-resize}.react-resizable-handle-nw{top:0;left:0;cursor:nw-resize;transform:rotate(180deg)}.react-resizable-handle-ne{top:0;right:0;cursor:ne-resize;transform:rotate(270deg)}.react-resizable-handle-e,.react-resizable-handle-w{top:50%;margin-top:-10px;cursor:ew-resize}.react-resizable-handle-w{left:0;transform:rotate(135deg)}.react-resizable-handle-e{right:0;transform:rotate(315deg)}.react-resizable-handle-n,.react-resizable-handle-s{left:50%;margin-left:-10px;cursor:ns-resize}.react-resizable-handle-n{top:0;transform:rotate(225deg)}.react-resizable-handle-s{bottom:0;transform:rotate(45deg)}
/*# sourceMappingURL=2.f324abd6.chunk.css.map */
/*# sourceMappingURL=2.2d55190c.chunk.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -66,6 +66,7 @@ import AddPolicy from "../../Policies/AddPolicy";
import DeleteReplicationRule from "../ViewBucket/DeleteReplicationRule";
import EditLifecycleConfiguration from "./EditLifecycleConfiguration";
import AddLifecycleModal from "./AddLifecycleModal";
import history from "../../../../history";
const styles = (theme: Theme) =>
createStyles({
@@ -710,6 +711,12 @@ const ViewBucket = ({
},
];
const userViewAction = (user: any) => {
history.push(`/users/${user}`);
};
const userTableActions = [{ type: "view", onClick: userViewAction }];
return (
<Fragment>
{addScreenOpen && (
@@ -1061,6 +1068,7 @@ const ViewBucket = ({
{usersEnabled && (
<TabPanel index={3} value={curTab}>
<TableWrapper
itemActions={userTableActions}
columns={[{ label: "User", elementKey: "accessKey" }]}
isLoading={loadingUsers}
records={bucketUsers}

View File

@@ -245,6 +245,10 @@ const Console = ({
component: Watch,
path: "/watch",
},
{
component: Users,
path: "/users/:userName",
},
{
component: Users,
path: "/users",

View File

@@ -114,9 +114,9 @@ const SetPolicy = ({
return;
}
const userPolicy: String = get(selectedUser, "policy", "");
setActualPolicy(userPolicy.split(","));
setSelectedPolicy(userPolicy.split(","));
const userPolicy: string[] = get(selectedUser, "policy", []);
setActualPolicy(userPolicy);
setSelectedPolicy(userPolicy);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, selectedGroup, selectedUser]);

View File

@@ -14,7 +14,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, { useEffect, useState } from "react";
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
@@ -29,16 +29,12 @@ import Tab from "@material-ui/core/Tab";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import Paper from "@material-ui/core/Paper";
import {
getTimeFromTimestamp,
niceBytes,
niceDays,
} from "../../../../common/utils";
import { niceBytes, niceDays } from "../../../../common/utils";
import AddPoolModal from "./AddPoolModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup";
import api from "../../../../common/api";
import { IPool, ITenant, IPodListElement } from "../ListTenants/types";
import { IPodListElement, IPool, ITenant } from "../ListTenants/types";
import PageHeader from "../../Common/PageHeader/PageHeader";
import UsageBarWrapper from "../../Common/UsageBarWrapper/UsageBarWrapper";
import UpdateTenantModal from "./UpdateTenantModal";
@@ -47,7 +43,6 @@ import { LicenseInfo } from "../../License/types";
import { Link } from "react-router-dom";
import { setErrorSnackMessage } from "../../../../actions";
import Moment from "react-moment";
import { Fragment } from "react";
interface ITenantDetailsProps {
classes: any;

View File

@@ -48,7 +48,7 @@ const styles = (theme: Theme) =>
...modalBasic,
});
const AddToGroup = ({
const BulkAddToGroup = ({
open,
checkedUsers,
closeModalAndRefresh,
@@ -183,4 +183,4 @@ const mapDispatchToProps = {
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(AddToGroup));
export default withStyles(styles)(connector(BulkAddToGroup));

View File

@@ -0,0 +1,210 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import { Button, LinearProgress } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../actions";
import api from "../../../common/api";
import GroupsSelectors from "./GroupsSelectors";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
const styles = (theme: Theme) =>
createStyles({
strongText: {
fontWeight: 700,
},
keyName: {
marginLeft: 5,
},
buttonContainer: {
textAlign: "right",
},
...modalBasic,
});
interface IChangeUserGroupsContentProps {
classes: any;
closeModalAndRefresh: () => void;
selectedUser: string;
open: boolean;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
}
const ChangeUserGroups = ({
classes,
closeModalAndRefresh,
selectedUser,
open,
setModalErrorSnackMessage,
}: IChangeUserGroupsContentProps) => {
const [addLoading, setAddLoading] = useState<boolean>(false);
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [enabled, setEnabled] = useState<boolean>(false);
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
const getUserInformation = useCallback(() => {
if (!selectedUser) {
return null;
}
api
.invoke("GET", `/api/v1/users/${selectedUser}`)
.then((res) => {
setAddLoading(false);
setAccessKey(res.accessKey);
setSelectedGroups(res.memberOf || []);
setEnabled(res.status === "enabled");
})
.catch((err) => {
setAddLoading(false);
setModalErrorSnackMessage(err);
});
}, [selectedUser, setModalErrorSnackMessage]);
useEffect(() => {
if (selectedUser == null) {
setAccessKey("");
setSecretKey("");
setSelectedGroups([]);
} else {
getUserInformation();
}
}, [selectedUser, getUserInformation]);
const saveRecord = (event: React.FormEvent) => {
event.preventDefault();
if (addLoading) {
return;
}
setAddLoading(true);
if (selectedUser !== null) {
api
.invoke("PUT", `/api/v1/users/${selectedUser}`, {
status: enabled ? "enabled" : "disabled",
groups: selectedGroups,
})
.then((_) => {
setAddLoading(false);
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setModalErrorSnackMessage(err);
});
} else {
api
.invoke("POST", "/api/v1/users", {
accessKey,
secretKey,
groups: selectedGroups,
})
.then((_) => {
setAddLoading(false);
closeModalAndRefresh();
})
.catch((err) => {
setAddLoading(false);
setModalErrorSnackMessage(err);
});
}
};
const resetForm = () => {
if (selectedUser !== null) {
setSelectedGroups([]);
return;
}
setAccessKey("");
setSecretKey("");
setSelectedGroups([]);
};
const sendEnabled =
accessKey.trim() !== "" &&
((secretKey.trim() !== "" && selectedUser === null) ||
selectedUser !== null);
return (
<ModalWrapper
onClose={() => {
closeModalAndRefresh();
}}
modalOpen={open}
title={"Set Groups"}
>
<React.Fragment>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
saveRecord(e);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={(elements: string[]) => {
setSelectedGroups(elements);
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={() => {
resetForm();
}}
>
Clear
</button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addLoading || !sendEnabled}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</React.Fragment>
</ModalWrapper>
);
};
const mapDispatchToProps = {
setModalErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(ChangeUserGroups));

View File

@@ -0,0 +1,291 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api";
import { Button, Grid, InputAdornment, TextField } from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import GroupIcon from "@material-ui/icons/Group";
import { User, UsersList } from "./types";
import { usersSort } from "../../../utils/sortFunctions";
import { CreateIcon } from "../../../icons";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../actions";
import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
import AddToGroup from "./BulkAddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
import PageHeader from "../Common/PageHeader/PageHeader";
import history from "../../../history";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
interface IUsersProps {
classes: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const ListUsers = ({ classes, setErrorSnackMessage }: IUsersProps) => {
const [records, setRecords] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const fetchRecords = useCallback(() => {
setLoading(true);
api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
const users = res.users === null ? [] : res.users;
setLoading(false);
setRecords(users.sort(usersSort));
})
.catch((err) => {
setLoading(false);
setErrorSnackMessage(err);
});
}, [setLoading, setRecords, setErrorSnackMessage]);
const closeAddModalAndRefresh = () => {
setAddScreenOpen(false);
fetchRecords();
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
fetchRecords();
}
};
const closeAddGroupBulk = (unCheckAll: boolean = false) => {
setAddGroupOpen(false);
if (unCheckAll) {
setCheckedUsers([]);
}
};
useEffect(() => {
fetchRecords();
}, [fetchRecords]);
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...checkedUsers]; // We clone the checkedUsers array
if (checked) {
// If the user has checked this field we need to push this to checkedUsersList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setCheckedUsers(elements);
return elements;
};
const viewAction = (selectionElement: any): void => {
history.push(`/users/${selectionElement.accessKey}`);
};
const deleteAction = (selectionElement: any): void => {
setDeleteOpen(true);
setSelectedUser(selectionElement);
};
const userLoggedIn = atob(localStorage.getItem("userLoggedIn") || "");
const tableActions = [
{ type: "view", onClick: viewAction },
{
type: "delete",
onClick: deleteAction,
disableButtonFunction: (topValue: any) => topValue === userLoggedIn,
},
];
return (
<React.Fragment>
{addScreenOpen && (
<AddUser
open={addScreenOpen}
selectedUser={selectedUser}
closeModalAndRefresh={() => {
closeAddModalAndRefresh();
}}
/>
)}
{policyOpen && (
<SetPolicy
open={policyOpen}
selectedUser={selectedUser}
selectedGroup={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
fetchRecords();
}}
/>
)}
{deleteOpen && (
<DeleteUser
deleteOpen={deleteOpen}
selectedUser={selectedUser}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{addGroupOpen && (
<AddToGroup
open={addGroupOpen}
checkedUsers={checkedUsers}
closeModalAndRefresh={(close: boolean) => {
closeAddGroupBulk(close);
}}
/>
)}
<PageHeader label={"Users"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Users"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<GroupIcon />}
disabled={checkedUsers.length <= 0}
onClick={() => {
if (checkedUsers.length > 0) {
setAddGroupOpen(true);
}
}}
>
Add to Group
</Button>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedUser(null);
}}
>
Create User
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
onSelect={selectionChanged}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
entityName="Users"
idField="accessKey"
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(ListUsers));

View File

@@ -0,0 +1,160 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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/>.
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { IPolicyItem } from "../Users/types";
import { setModalErrorSnackMessage } from "../../../actions";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import api from "../../../common/api";
import PolicySelectors from "../Policies/PolicySelectors";
interface ISetUserPoliciesProps {
classes: any;
closeModalAndRefresh: () => void;
selectedUser: string;
currentPolicies: IPolicyItem[];
open: boolean;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
buttonContainer: {
textAlign: "right",
},
});
const SetUserPolicies = ({
classes,
closeModalAndRefresh,
selectedUser,
currentPolicies,
setModalErrorSnackMessage,
open,
}: ISetUserPoliciesProps) => {
//Local States
const [loading, setLoading] = useState<boolean>(false);
const [actualPolicy, setActualPolicy] = useState<string[]>([]);
const [selectedPolicy, setSelectedPolicy] = useState<string[]>([]);
const SetUserPoliciesAction = () => {
let entity = "user";
let value = selectedUser;
setLoading(true);
api
.invoke("PUT", `/api/v1/set-policy/${selectedPolicy}`, {
entityName: value,
entityType: entity,
})
.then(() => {
setLoading(false);
closeModalAndRefresh();
})
.catch((err) => {
setLoading(false);
setModalErrorSnackMessage(err);
});
};
const resetSelection = () => {
setSelectedPolicy(actualPolicy);
};
useEffect(() => {
if (open) {
const userPolicy: string[] = [];
for (let pol of currentPolicies) {
userPolicy.push(pol.policy);
}
setActualPolicy(userPolicy);
setSelectedPolicy(userPolicy);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, selectedUser]);
return (
<ModalWrapper
onClose={() => {
closeModalAndRefresh();
}}
modalOpen={open}
title="Set Policies"
>
<PolicySelectors
selectedPolicy={selectedPolicy}
setSelectedPolicy={setSelectedPolicy}
/>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<button
type="button"
color="primary"
className={classes.clearButton}
onClick={resetSelection}
>
Clear
</button>
<Button
type="button"
variant="contained"
color="primary"
disabled={loading}
onClick={SetUserPoliciesAction}
>
Save
</Button>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</ModalWrapper>
);
};
const mapDispatchToProps = {
setModalErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(SetUserPolicies));

View File

@@ -14,284 +14,33 @@
// 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, { useCallback, useEffect, useState } from "react";
import React from "react";
import history from "../../../history";
import { Route, Router, Switch, withRouter } from "react-router-dom";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api";
import { Button, Grid, InputAdornment, TextField } from "@material-ui/core";
import SearchIcon from "@material-ui/icons/Search";
import GroupIcon from "@material-ui/icons/Group";
import { User, UsersList } from "./types";
import { usersSort } from "../../../utils/sortFunctions";
import { CreateIcon } from "../../../icons";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../actions";
import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
import PageHeader from "../Common/PageHeader/PageHeader";
import { AppState } from "../../../store";
import { setMenuOpen } from "../../../actions";
import NotFoundPage from "../../NotFoundPage";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
import ListUsers from "./ListUsers";
import ViewUser from "./ViewUser";
interface IUsersProps {
classes: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen,
});
const Users = ({ classes, setErrorSnackMessage }: IUsersProps) => {
const [records, setRecords] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const fetchRecords = useCallback(() => {
setLoading(true);
api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
const users = res.users === null ? [] : res.users;
setLoading(false);
setRecords(users.sort(usersSort));
})
.catch((err) => {
setLoading(false);
setErrorSnackMessage(err);
});
}, [setLoading, setRecords, setErrorSnackMessage]);
const closeAddModalAndRefresh = () => {
setAddScreenOpen(false);
fetchRecords();
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
fetchRecords();
}
};
const closeAddGroupBulk = (unCheckAll: boolean = false) => {
setAddGroupOpen(false);
if (unCheckAll) {
setCheckedUsers([]);
}
};
useEffect(() => {
fetchRecords();
}, [fetchRecords]);
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
);
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...checkedUsers]; // We clone the checkedUsers array
if (checked) {
// If the user has checked this field we need to push this to checkedUsersList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter((element) => element !== value);
}
setCheckedUsers(elements);
return elements;
};
const viewAction = (selectionElement: any): void => {
setAddScreenOpen(true);
setSelectedUser(selectionElement);
};
const setPolicyAction = (selectionElement: any): void => {
setPolicyOpen(true);
setSelectedUser(selectionElement);
};
const deleteAction = (selectionElement: any): void => {
setDeleteOpen(true);
setSelectedUser(selectionElement);
};
const userLoggedIn = atob(localStorage.getItem("userLoggedIn") || "");
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "description", onClick: setPolicyAction },
{
type: "delete",
onClick: deleteAction,
disableButtonFunction: (topValue: any) => topValue === userLoggedIn,
},
];
const connector = connect(mapState, { setMenuOpen });
const Users = () => {
return (
<React.Fragment>
{addScreenOpen && (
<AddUser
open={addScreenOpen}
selectedUser={selectedUser}
closeModalAndRefresh={() => {
closeAddModalAndRefresh();
}}
/>
)}
{policyOpen && (
<SetPolicy
open={policyOpen}
selectedUser={selectedUser}
selectedGroup={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
fetchRecords();
}}
/>
)}
{deleteOpen && (
<DeleteUser
deleteOpen={deleteOpen}
selectedUser={selectedUser}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
/>
)}
{addGroupOpen && (
<AddToGroup
open={addGroupOpen}
checkedUsers={checkedUsers}
closeModalAndRefresh={(close: boolean) => {
closeAddGroupBulk(close);
}}
/>
)}
<PageHeader label={"Users"} />
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Users"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<GroupIcon />}
disabled={checkedUsers.length <= 0}
onClick={() => {
if (checkedUsers.length > 0) {
setAddGroupOpen(true);
}
}}
>
Add to Group
</Button>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedUser(null);
}}
>
Create User
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
onSelect={selectionChanged}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
entityName="Users"
idField="accessKey"
/>
</Grid>
</Grid>
</Grid>
</React.Fragment>
<Router history={history}>
<Switch>
<Route path="/users/:userName" component={ViewUser} />
<Route path="/" component={ListUsers} />
<Route component={NotFoundPage} />
</Switch>
</Router>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(Users));
export default withRouter(connector(Users));

View File

@@ -0,0 +1,340 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, Grid } from "@material-ui/core";
import PageHeader from "../Common/PageHeader/PageHeader";
import { CreateIcon } from "../../../icons";
import {
setErrorSnackMessage,
setModalErrorSnackMessage,
} from "../../../actions";
import {
actionsTray,
containerForHeader,
searchField,
} from "../Common/FormComponents/common/styleLibrary";
import { IPolicyItem } from "./types";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { TabPanel } from "../../shared/tabs";
import Paper from "@material-ui/core/Paper";
import api from "../../../common/api";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import ChangeUserGroups from "./ChangeUserGroups";
import SetUserPolicies from "./SetUserPolicies";
import { Bookmark } from "@material-ui/icons";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word",
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
fixedHeight: {
height: 165,
minWidth: 247,
padding: "25px 28px",
"& svg": {
maxHeight: 18,
},
},
paperContainer: {
padding: 15,
paddingLeft: 50,
display: "flex",
},
gridContainer: {
display: "grid",
gridTemplateColumns: "auto auto",
gridGap: 8,
justifyContent: "flex-start",
alignItems: "center",
"& div:not(.MuiCircularProgress-root)": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
minWidth: 150,
},
},
...actionsTray,
...searchField,
...containerForHeader(theme.spacing(4)),
});
interface IViewUserProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
function a11yProps(index: any) {
return {
id: `simple-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`,
};
}
interface IGroupItem {
group: string;
}
const ViewUser = ({ classes, match }: IViewUserProps) => {
const [curTab, setCurTab] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false);
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const [addLoading, setAddLoading] = useState<boolean>(false);
const [enabled, setEnabled] = useState<boolean>(false);
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
const [currentGroups, setCurrentGroups] = useState<IGroupItem[]>([]);
const [currentPolicies, setCurrentPolicies] = useState<IPolicyItem[]>([]);
const userName = match.params["userName"];
const getUserInformation = useCallback(() => {
if (userName === "") {
return null;
}
setLoading(true);
api
.invoke("GET", `/api/v1/users/${userName}`)
.then((res) => {
setAddLoading(false);
const memberOf = res.memberOf || [];
setSelectedGroups(memberOf);
let currentGroups: IGroupItem[] = [];
for (let group of memberOf) {
currentGroups.push({
group: group,
});
}
setCurrentGroups(currentGroups);
let currentPolicies: IPolicyItem[] = [];
for (let policy of res.policy) {
currentPolicies.push({
policy: policy,
});
}
setCurrentPolicies(currentPolicies);
setEnabled(res.status === "enabled");
setLoading(false);
})
.catch((err) => {
setAddLoading(false);
setLoading(false);
setModalErrorSnackMessage(err);
});
}, [userName]);
const saveRecord = (isEnabled: boolean) => {
if (addLoading) {
return;
}
setAddLoading(true);
api
.invoke("PUT", `/api/v1/users/${userName}`, {
status: isEnabled ? "enabled" : "disabled",
groups: selectedGroups,
})
.then((_) => {
setAddLoading(false);
})
.catch((err) => {
setAddLoading(false);
setModalErrorSnackMessage(err);
});
};
useEffect(() => {
getUserInformation();
}, [getUserInformation]);
const userLoggedIn = atob(localStorage.getItem("userLoggedIn") || "");
return (
<React.Fragment>
<PageHeader label={`User: ${userName}`} />
{addGroupOpen && (
<ChangeUserGroups
open={addGroupOpen}
selectedUser={userName}
closeModalAndRefresh={() => {
setAddGroupOpen(false);
getUserInformation();
}}
/>
)}
{policyOpen && (
<SetUserPolicies
open={policyOpen}
selectedUser={userName}
currentPolicies={currentPolicies}
closeModalAndRefresh={() => {
setPolicyOpen(false);
getUserInformation();
}}
/>
)}
<Grid container>
<Grid item xs={12} className={classes.container}>
<Grid item xs={12}>
<Grid container spacing={2}>
<Grid item>
<Paper className={classes.paperContainer}>
<div className={classes.gridContainer}>
<div>Enabled:</div>
<div className={classes.capitalizeFirst}>
<FormSwitchWrapper
checked={enabled}
value={"user_enabled"}
id="user-status"
name="user-status"
disabled={userLoggedIn === userName}
onChange={(e) => {
setEnabled(e.target.checked);
saveRecord(e.target.checked);
}}
switchOnly
/>
</div>
</div>
</Paper>
</Grid>
</Grid>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid container item xs={12}>
<Grid item xs={9}>
<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="Groups" {...a11yProps(0)} />
<Tab label="Policies" {...a11yProps(0)} />
</Tabs>
</Grid>
<Grid item xs={3} className={classes.actionsTray}>
{curTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setAddGroupOpen(true);
}}
>
Add to Groups
</Button>
)}
{curTab === 1 && (
<Fragment>
<Button
variant="contained"
color="primary"
startIcon={<Bookmark />}
size="medium"
onClick={() => {
setPolicyOpen(true);
}}
>
Assign Policies
</Button>
</Fragment>
)}
</Grid>
</Grid>
<Grid item xs={12}>
<TabPanel index={0} value={curTab}>
<TableWrapper
// itemActions={userTableActions}
columns={[{ label: "Name", elementKey: "group" }]}
isLoading={loading}
records={currentGroups}
entityName="Groups"
idField="group"
/>
</TabPanel>
<TabPanel index={1} value={curTab}>
<TableWrapper
// itemActions={userTableActions}
columns={[{ label: "Name", elementKey: "policy" }]}
isLoading={loading}
records={currentPolicies}
entityName="Policies"
idField="policy"
/>
</TabPanel>
</Grid>
</Grid>
</Grid>
</React.Fragment>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(ViewUser));

View File

@@ -22,10 +22,14 @@ export interface User {
enabled: boolean;
accessKey: string;
secretKey: string;
policy?: string;
policy?: string[];
}
export interface UsersList {
users: User[];
total_users: number;
}
export interface IPolicyItem {
policy: string;
}

View File

@@ -0,0 +1,40 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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";
interface TabPanelProps {
children?: React.ReactNode;
index: any;
value: any;
}
export const TabPanel = (props: TabPanelProps) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
style={{ marginTop: "5px" }}
{...other}
>
{value === index && <Fragment>{children}</Fragment>}
</div>
);
};

View File

@@ -115,7 +115,7 @@ func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
userElem := &models.User{
AccessKey: accessKey,
Status: string(user.Status),
Policy: user.PolicyName,
Policy: strings.Split(user.PolicyName, ","),
MemberOf: user.MemberOf,
}
users = append(users, userElem)
@@ -165,7 +165,7 @@ func addUser(ctx context.Context, client MinioAdmin, accessKey, secretKey *strin
userRet := &models.User{
AccessKey: *accessKey,
MemberOf: nil,
Policy: "",
Policy: []string{},
Status: "",
}
return userRet, nil
@@ -250,7 +250,7 @@ func getUserInfoResponse(session *models.Principal, params admin_api.GetUserInfo
userInformation := &models.User{
AccessKey: params.Name,
MemberOf: user.MemberOf,
Policy: user.PolicyName,
Policy: strings.Split(user.PolicyName, ","),
Status: string(user.Status),
}
@@ -333,10 +333,12 @@ func updateUserGroups(ctx context.Context, client MinioAdmin, user string, group
return nil, err
}
policies := strings.Split(userInfo.PolicyName, ",")
userReturn := &models.User{
AccessKey: user,
MemberOf: userInfo.MemberOf,
Policy: userInfo.PolicyName,
Policy: policies,
Status: string(userInfo.Status),
}
@@ -492,18 +494,20 @@ func getListUsersWithAccessToBucketResponse(session *models.Principal, bucket st
var retval []string
seen := make(map[string]bool)
for i := 0; i < len(users); i++ {
policy, err := adminClient.getPolicy(ctx, users[i].Policy)
if err == nil {
parsedPolicy, err2 := parsePolicy(users[i].Policy, policy)
if err2 == nil && policyMatchesBucket(parsedPolicy, bucket) {
retval = append(retval, users[i].AccessKey)
seen[users[i].AccessKey] = true
for _, policyName := range users[i].Policy {
policy, err := adminClient.getPolicy(ctx, policyName)
if err == nil {
parsedPolicy, err2 := parsePolicy(policyName, policy)
if err2 == nil && policyMatchesBucket(parsedPolicy, bucket) {
retval = append(retval, users[i].AccessKey)
seen[users[i].AccessKey] = true
}
if err2 != nil {
log.Println(err2)
}
} else {
log.Println(err)
}
if err2 != nil {
log.Println(err2)
}
} else {
log.Println(err)
}
}

View File

@@ -19,6 +19,7 @@ package restapi
import (
"context"
"fmt"
"strings"
"testing"
"github.com/minio/minio/pkg/madmin"
@@ -99,7 +100,7 @@ func TestListUsers(t *testing.T) {
for _, b := range userMap {
assert.Contains(mockUserMap, b.AccessKey)
assert.Equal(string(mockUserMap[b.AccessKey].Status), b.Status)
assert.Equal(mockUserMap[b.AccessKey].PolicyName, b.Policy)
assert.Equal(mockUserMap[b.AccessKey].PolicyName, strings.Join(b.Policy, ","))
assert.ElementsMatch(mockUserMap[b.AccessKey].MemberOf, []string{"group1", "group2"})
}

View File

@@ -6684,7 +6684,10 @@ func init() {
}
},
"policy": {
"type": "string"
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string"
@@ -13936,7 +13939,10 @@ func init() {
}
},
"policy": {
"type": "string"
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string"

View File

@@ -2665,7 +2665,9 @@ definitions:
accessKey:
type: string
policy:
type: string
type: array
items:
type: string
memberOf:
type: array
items: