Disable Bucket/Replication (feature) if not enough permissions (#662)

* Bucket/Replication (feature) if not enough permissions

* Address comments

* Remove Consts fetchPerms
This commit is contained in:
Daniel Valdivia
2021-03-25 10:10:54 -07:00
committed by GitHub
parent e088431c62
commit 53eb59f5ad
23 changed files with 1430 additions and 107 deletions

View File

@@ -0,0 +1,97 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// HasPermissionRequest has permission request
//
// swagger:model hasPermissionRequest
type HasPermissionRequest struct {
// actions
Actions []*PolicyArgs `json:"actions"`
}
// Validate validates this has permission request
func (m *HasPermissionRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateActions(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *HasPermissionRequest) validateActions(formats strfmt.Registry) error {
if swag.IsZero(m.Actions) { // not required
return nil
}
for i := 0; i < len(m.Actions); i++ {
if swag.IsZero(m.Actions[i]) { // not required
continue
}
if m.Actions[i] != nil {
if err := m.Actions[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("actions" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *HasPermissionRequest) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *HasPermissionRequest) UnmarshalBinary(b []byte) error {
var res HasPermissionRequest
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,97 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// HasPermissionResponse has permission response
//
// swagger:model hasPermissionResponse
type HasPermissionResponse struct {
// permissions
Permissions []*PermissionAction `json:"permissions"`
}
// Validate validates this has permission response
func (m *HasPermissionResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validatePermissions(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *HasPermissionResponse) validatePermissions(formats strfmt.Registry) error {
if swag.IsZero(m.Permissions) { // not required
return nil
}
for i := 0; i < len(m.Permissions); i++ {
if swag.IsZero(m.Permissions[i]) { // not required
continue
}
if m.Permissions[i] != nil {
if err := m.Permissions[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("permissions" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *HasPermissionResponse) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *HasPermissionResponse) UnmarshalBinary(b []byte) error {
var res HasPermissionResponse
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,63 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PermissionAction permission action
//
// swagger:model permissionAction
type PermissionAction struct {
// can
Can bool `json:"can,omitempty"`
// id
ID string `json:"id,omitempty"`
}
// Validate validates this permission action
func (m *PermissionAction) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PermissionAction) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PermissionAction) UnmarshalBinary(b []byte) error {
var res PermissionAction
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

66
models/policy_args.go Normal file
View File

@@ -0,0 +1,66 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// PolicyArgs policy args
//
// swagger:model policyArgs
type PolicyArgs struct {
// action
Action string `json:"action,omitempty"`
// bucket name
BucketName string `json:"bucket_name,omitempty"`
// id
ID string `json:"id,omitempty"`
}
// Validate validates this policy args
func (m *PolicyArgs) Validate(formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *PolicyArgs) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *PolicyArgs) UnmarshalBinary(b []byte) error {
var res PolicyArgs
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -1,8 +1,8 @@
{
"files": {
"main.css": "/static/css/main.a19f3d53.chunk.css",
"main.js": "/static/js/main.1ecd68ff.chunk.js",
"main.js.map": "/static/js/main.1ecd68ff.chunk.js.map",
"main.js": "/static/js/main.e71727db.chunk.js",
"main.js.map": "/static/js/main.e71727db.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.76b14b73.chunk.css": "/static/css/2.76b14b73.chunk.css",
@@ -20,6 +20,6 @@
"static/css/2.76b14b73.chunk.css",
"static/js/2.41957633.chunk.js",
"static/css/main.a19f3d53.chunk.css",
"static/js/main.1ecd68ff.chunk.js"
"static/js/main.e71727db.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.76b14b73.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.41957633.chunk.js"></script><script src="/static/js/main.1ecd68ff.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.76b14b73.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.41957633.chunk.js"></script><script src="/static/js/main.e71727db.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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, { useEffect, useState, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button } from "@material-ui/core";
@@ -23,7 +23,7 @@ import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import Moment from "react-moment";
import { Bucket, BucketList } from "../types";
import { Bucket, BucketList, HasPermissionResponse } from "../types";
import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
import { AppState } from "../../../../store";
@@ -92,6 +92,43 @@ const ListBuckets = ({
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedBucket, setSelectedBucket] = useState<string>("");
const [filterBuckets, setFilterBuckets] = useState<string>("");
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canCreateBucket, setCanCreateBucket] = useState<boolean>(false);
// check the permissions for creating bucket
useEffect(() => {
if (loadingPerms) {
api
.invoke("POST", `/api/v1/has-permission`, {
actions: [
{
id: "createBucket",
action: "s3:CreateBucket",
},
],
})
.then((res: HasPermissionResponse) => {
setLoadingPerms(false);
if (!res.permissions) {
return;
}
const actions = res.permissions ? res.permissions : [];
let canCreate = actions.find((s) => s.id == "createBucket");
if (canCreate && canCreate.can) {
setCanCreateBucket(true);
} else {
setCanCreateBucket(false);
}
setLoadingPerms(false);
})
.catch((err: any) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [loadingPerms, setErrorSnackMessage]);
useEffect(() => {
if (loading) {
@@ -155,7 +192,7 @@ const ListBuckets = ({
});
return (
<React.Fragment>
<Fragment>
{addBucketModalOpen && (
<AddBucket
open={addBucketModalOpen}
@@ -192,16 +229,18 @@ const ListBuckets = ({
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
addBucketOpen(true);
}}
>
Create Bucket
</Button>
{canCreateBucket && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
addBucketOpen(true);
}}
>
Create Bucket
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
@@ -232,7 +271,7 @@ const ListBuckets = ({
</Grid>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};

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, { useEffect, useState, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, IconButton } from "@material-ui/core";
@@ -38,6 +38,7 @@ import {
BucketReplicationRule,
BucketReplicationRuleDeleteMarker,
BucketVersioning,
HasPermissionResponse,
} from "../types";
import { CreateIcon } from "../../../../icons";
import { niceBytes } from "../../../../common/utils";
@@ -196,7 +197,7 @@ function TabPanel(props: TabPanelProps) {
style={{ marginTop: "5px" }}
{...other}
>
{value === index && <React.Fragment>{children}</React.Fragment>}
{value === index && <Fragment>{children}</Fragment>}
</div>
);
}
@@ -247,6 +248,62 @@ const ViewBucket = ({
const [enableVersioningOpen, setEnableVersioningOpen] = useState<boolean>(
false
);
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canPutReplication, setCanPutReplication] = useState<boolean>(false);
const [canGetReplication, setCanGetReplication] = useState<boolean>(false);
// check the permissions for creating bucket
useEffect(() => {
if (loadingPerms) {
api
.invoke("POST", `/api/v1/has-permission`, {
actions: [
{
id: "PutReplicationConfiguration",
action: "s3:PutReplicationConfiguration",
bucket_name: bucketName,
},
{
id: "GetReplicationConfiguration",
action: "s3:GetReplicationConfiguration",
bucket_name: bucketName,
},
],
})
.then((res: HasPermissionResponse) => {
setLoadingPerms(false);
if (!res.permissions) {
return;
}
const actions = res.permissions ? res.permissions : [];
let canPutReplication = actions.find(
(s) => s.id == "PutReplicationConfiguration"
);
if (canPutReplication && canPutReplication.can) {
setCanPutReplication(true);
} else {
setCanPutReplication(false);
}
let canGetReplication = actions.find(
(s) => s.id == "GetReplicationConfiguration"
);
if (canGetReplication && canGetReplication.can) {
setCanGetReplication(true);
} else {
setCanGetReplication(false);
}
setLoadingPerms(false);
})
.catch((err: any) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [loadingPerms, setErrorSnackMessage]);
const bucketName = match.params["bucketName"];
@@ -430,19 +487,15 @@ const ViewBucket = ({
}
const eventsDisplay = (events: string[]) => {
return <React.Fragment>{events.join(", ")}</React.Fragment>;
return <Fragment>{events.join(", ")}</Fragment>;
};
const ruleDestDisplay = (events: BucketReplicationDestination) => {
return (
<React.Fragment>
{events.bucket.replace("arn:aws:s3:::", "")}
</React.Fragment>
);
return <Fragment>{events.bucket.replace("arn:aws:s3:::", "")}</Fragment>;
};
const ruleDelDisplay = (events: BucketReplicationRuleDeleteMarker) => {
return <React.Fragment>{events.status}</React.Fragment>;
return <Fragment>{events.status}</Fragment>;
};
const setOpenReplicationOpen = (open = false) => {
@@ -469,7 +522,7 @@ const ViewBucket = ({
const tableActions = [{ type: "delete", onClick: confirmDeleteEvent }];
return (
<React.Fragment>
<Fragment>
{addScreenOpen && (
<AddEvent
open={addScreenOpen}
@@ -573,10 +626,10 @@ const ViewBucket = ({
<span>{replicationRules.length ? "Yes" : "No"}</span>
</div>
{!hasObjectLocking && (
<React.Fragment>
<Fragment>
<div>Object Locking:</div>
<div>No</div>
</React.Fragment>
</Fragment>
)}
<div>Encryption:</div>
<div>
@@ -608,7 +661,7 @@ const ViewBucket = ({
variant="indeterminate"
/>
) : (
<React.Fragment>
<Fragment>
{isVersioned && !loadingVersioning ? "Yes" : "No"}
&nbsp;
<IconButton
@@ -622,7 +675,7 @@ const ViewBucket = ({
>
<PencilIcon active={true} />
</IconButton>
</React.Fragment>
</Fragment>
)}
</div>
<div>Retention:</div>
@@ -634,7 +687,7 @@ const ViewBucket = ({
variant="indeterminate"
/>
) : (
<React.Fragment>
<Fragment>
&nbsp;
<IconButton
color="primary"
@@ -647,7 +700,7 @@ const ViewBucket = ({
>
<PencilIcon active={true} />
</IconButton>
</React.Fragment>
</Fragment>
)}
</div>
</div>
@@ -671,7 +724,9 @@ const ViewBucket = ({
aria-label="cluster-tabs"
>
<Tab label="Events" {...a11yProps(0)} />
<Tab label="Replication" {...a11yProps(1)} />
{canGetReplication && (
<Tab label="Replication" {...a11yProps(1)} />
)}
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
@@ -689,17 +744,21 @@ const ViewBucket = ({
</Button>
)}
{curTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
<Fragment>
{canPutReplication && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
)}
</Fragment>
)}
</Grid>
</Grid>
@@ -723,37 +782,39 @@ const ViewBucket = ({
idField="id"
/>
</TabPanel>
<TabPanel index={1} value={curTab}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "ID", elementKey: "id" },
{
label: "Priority",
elementKey: "priority",
},
{
label: "Destination",
elementKey: "destination",
renderFunction: ruleDestDisplay,
},
{
label: "Delete Replication",
elementKey: "delete_marker_replication",
renderFunction: ruleDelDisplay,
},
{ label: "Status", elementKey: "status" },
]}
isLoading={loadingEvents}
records={replicationRules}
entityName="Replication Rules"
idField="id"
/>
</TabPanel>
{canGetReplication && (
<TabPanel index={1} value={curTab}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "ID", elementKey: "id" },
{
label: "Priority",
elementKey: "priority",
},
{
label: "Destination",
elementKey: "destination",
renderFunction: ruleDestDisplay,
},
{
label: "Delete Replication",
elementKey: "delete_marker_replication",
renderFunction: ruleDelDisplay,
},
{ label: "Status", elementKey: "status" },
]}
isLoading={loadingEvents}
records={replicationRules}
entityName="Replication Rules"
idField="id"
/>
</TabPanel>
)}
</Grid>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};

View File

@@ -118,3 +118,12 @@ export interface IRemoteBucket {
status: string;
service: string;
}
export interface PermissionAction {
id: string;
can: boolean;
}
export interface HasPermissionResponse {
permissions: PermissionAction[];
}

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, { useState, useEffect } from "react";
import React, { useState, useEffect, Fragment } from "react";
import { withRouter } from "react-router-dom";
import { connect } from "react-redux";
import get from "lodash/get";
@@ -26,7 +26,7 @@ import SearchIcon from "@material-ui/icons/Search";
import { Button } from "@material-ui/core";
import { CreateIcon } from "../../../icons";
import { niceBytes } from "../../../common/utils";
import { Bucket, BucketList } from "../Buckets/types";
import { Bucket, BucketList, HasPermissionResponse } from "../Buckets/types";
import {
actionsTray,
objectBrowserCommon,
@@ -118,6 +118,40 @@ const BrowseBuckets = ({
const [records, setRecords] = useState<Bucket[]>([]);
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
const [filterBuckets, setFilterBuckets] = useState<string>("");
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canCreateBucket, setCanCreateBucket] = useState<boolean>(false);
// check the permissions for creating bucket
useEffect(() => {
if (loadingPerms) {
api
.invoke("POST", `/api/v1/has-permission`, {
actions: [
{
id: "createBucket",
action: "s3:CreateBucket",
},
],
})
.then((res: HasPermissionResponse) => {
const canCreate = res.permissions
.filter((s) => s.id == "createBucket")
.pop();
if (canCreate && canCreate.can) {
setCanCreateBucket(true);
} else {
setCanCreateBucket(false);
}
setLoadingPerms(false);
// setRecords(res.buckets || []);
})
.catch((err: any) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [loadingPerms, setErrorSnackMessage]);
useEffect(() => {
resetRoutesList(true);
@@ -170,7 +204,7 @@ const BrowseBuckets = ({
};
return (
<React.Fragment>
<Fragment>
{addScreenOpen && (
<AddBucket
open={addScreenOpen}
@@ -182,18 +216,20 @@ const BrowseBuckets = ({
<div>
<BrowserBreadcrumbs />
</div>
<div>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Bucket
</Button>
</div>
{canCreateBucket && (
<div>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddScreenOpen(true);
}}
>
Create Bucket
</Button>
</div>
)}
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
@@ -251,7 +287,7 @@ const BrowseBuckets = ({
/>
</Grid>
</Grid>
</React.Fragment>
</Fragment>
);
};

View File

@@ -1724,6 +1724,39 @@ func init() {
}
}
},
"/has-permission": {
"post": {
"tags": [
"UserAPI"
],
"summary": "Checks whether the user can perform a series of actions",
"operationId": "HasPermissionTo",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/hasPermissionRequest"
}
}
],
"responses": {
"201": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/hasPermissionResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/login": {
"get": {
"security": [],
@@ -4094,6 +4127,28 @@ func init() {
}
}
},
"hasPermissionRequest": {
"type": "object",
"properties": {
"actions": {
"type": "array",
"items": {
"$ref": "#/definitions/policyArgs"
}
}
}
},
"hasPermissionResponse": {
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": {
"$ref": "#/definitions/permissionAction"
}
}
}
},
"iamEntity": {
"type": "string",
"pattern": "^[\\w+=,.@-]{1,64}$"
@@ -4704,6 +4759,17 @@ func init() {
"type": "string"
}
},
"permissionAction": {
"type": "object",
"properties": {
"can": {
"type": "boolean"
},
"id": {
"type": "string"
}
}
},
"podAffinityTerm": {
"description": "Required. A pod affinity term, associated with the corresponding weight.",
"type": "object",
@@ -4777,6 +4843,20 @@ func init() {
}
}
},
"policyArgs": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"bucket_name": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"policyEntity": {
"type": "string",
"default": "user",
@@ -7483,6 +7563,39 @@ func init() {
}
}
},
"/has-permission": {
"post": {
"tags": [
"UserAPI"
],
"summary": "Checks whether the user can perform a series of actions",
"operationId": "HasPermissionTo",
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/hasPermissionRequest"
}
}
],
"responses": {
"201": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/hasPermissionResponse"
}
},
"default": {
"description": "Generic error response.",
"schema": {
"$ref": "#/definitions/error"
}
}
}
}
},
"/login": {
"get": {
"security": [],
@@ -10462,6 +10575,28 @@ func init() {
}
}
},
"hasPermissionRequest": {
"type": "object",
"properties": {
"actions": {
"type": "array",
"items": {
"$ref": "#/definitions/policyArgs"
}
}
}
},
"hasPermissionResponse": {
"type": "object",
"properties": {
"permissions": {
"type": "array",
"items": {
"$ref": "#/definitions/permissionAction"
}
}
}
},
"iamEntity": {
"type": "string",
"pattern": "^[\\w+=,.@-]{1,64}$"
@@ -11028,6 +11163,17 @@ func init() {
"type": "string"
}
},
"permissionAction": {
"type": "object",
"properties": {
"can": {
"type": "boolean"
},
"id": {
"type": "string"
}
}
},
"podAffinityTerm": {
"description": "Required. A pod affinity term, associated with the corresponding weight.",
"type": "object",
@@ -11079,6 +11225,20 @@ func init() {
}
}
},
"policyArgs": {
"type": "object",
"properties": {
"action": {
"type": "string"
},
"bucket_name": {
"type": "string"
},
"id": {
"type": "string"
}
}
},
"policyEntity": {
"type": "string",
"default": "user",

View File

@@ -184,6 +184,9 @@ func NewConsoleAPI(spec *loads.Document) *ConsoleAPI {
AdminAPIGroupInfoHandler: admin_api.GroupInfoHandlerFunc(func(params admin_api.GroupInfoParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.GroupInfo has not yet been implemented")
}),
UserAPIHasPermissionToHandler: user_api.HasPermissionToHandlerFunc(func(params user_api.HasPermissionToParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation user_api.HasPermissionTo has not yet been implemented")
}),
AdminAPIListAllTenantsHandler: admin_api.ListAllTenantsHandlerFunc(func(params admin_api.ListAllTenantsParams, principal *models.Principal) middleware.Responder {
return middleware.NotImplemented("operation admin_api.ListAllTenants has not yet been implemented")
}),
@@ -475,6 +478,8 @@ type ConsoleAPI struct {
AdminAPIGetUserInfoHandler admin_api.GetUserInfoHandler
// AdminAPIGroupInfoHandler sets the operation handler for the group info operation
AdminAPIGroupInfoHandler admin_api.GroupInfoHandler
// UserAPIHasPermissionToHandler sets the operation handler for the has permission to operation
UserAPIHasPermissionToHandler user_api.HasPermissionToHandler
// AdminAPIListAllTenantsHandler sets the operation handler for the list all tenants operation
AdminAPIListAllTenantsHandler admin_api.ListAllTenantsHandler
// UserAPIListBucketEventsHandler sets the operation handler for the list bucket events operation
@@ -777,6 +782,9 @@ func (o *ConsoleAPI) Validate() error {
if o.AdminAPIGroupInfoHandler == nil {
unregistered = append(unregistered, "admin_api.GroupInfoHandler")
}
if o.UserAPIHasPermissionToHandler == nil {
unregistered = append(unregistered, "user_api.HasPermissionToHandler")
}
if o.AdminAPIListAllTenantsHandler == nil {
unregistered = append(unregistered, "admin_api.ListAllTenantsHandler")
}
@@ -1198,6 +1206,10 @@ func (o *ConsoleAPI) initHandlerCache() {
o.handlers["GET"] = make(map[string]http.Handler)
}
o.handlers["GET"]["/groups/{name}"] = admin_api.NewGroupInfo(o.context, o.AdminAPIGroupInfoHandler)
if o.handlers["POST"] == nil {
o.handlers["POST"] = make(map[string]http.Handler)
}
o.handlers["POST"]["/has-permission"] = user_api.NewHasPermissionTo(o.context, o.UserAPIHasPermissionToHandler)
if o.handlers["GET"] == nil {
o.handlers["GET"] = make(map[string]http.Handler)
}

View File

@@ -0,0 +1,90 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"net/http"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// HasPermissionToHandlerFunc turns a function with the right signature into a has permission to handler
type HasPermissionToHandlerFunc func(HasPermissionToParams, *models.Principal) middleware.Responder
// Handle executing the request and returning a response
func (fn HasPermissionToHandlerFunc) Handle(params HasPermissionToParams, principal *models.Principal) middleware.Responder {
return fn(params, principal)
}
// HasPermissionToHandler interface for that can handle valid has permission to params
type HasPermissionToHandler interface {
Handle(HasPermissionToParams, *models.Principal) middleware.Responder
}
// NewHasPermissionTo creates a new http.Handler for the has permission to operation
func NewHasPermissionTo(ctx *middleware.Context, handler HasPermissionToHandler) *HasPermissionTo {
return &HasPermissionTo{Context: ctx, Handler: handler}
}
/*HasPermissionTo swagger:route POST /has-permission UserAPI hasPermissionTo
Checks whether the user can perform a series of actions
*/
type HasPermissionTo struct {
Context *middleware.Context
Handler HasPermissionToHandler
}
func (o *HasPermissionTo) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
route, rCtx, _ := o.Context.RouteInfo(r)
if rCtx != nil {
r = rCtx
}
var Params = NewHasPermissionToParams()
uprinc, aCtx, err := o.Context.Authorize(r, route)
if err != nil {
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
if aCtx != nil {
r = aCtx
}
var principal *models.Principal
if uprinc != nil {
principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise
}
if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params
o.Context.Respond(rw, r, route.Produces, route, err)
return
}
res := o.Handler.Handle(Params, principal) // actually handle the request
o.Context.Respond(rw, r, route.Produces, route, res)
}

View File

@@ -0,0 +1,94 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"io"
"net/http"
"github.com/go-openapi/errors"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
)
// NewHasPermissionToParams creates a new HasPermissionToParams object
// no default values defined in spec.
func NewHasPermissionToParams() HasPermissionToParams {
return HasPermissionToParams{}
}
// HasPermissionToParams contains all the bound params for the has permission to operation
// typically these are obtained from a http.Request
//
// swagger:parameters HasPermissionTo
type HasPermissionToParams struct {
// HTTP Request Object
HTTPRequest *http.Request `json:"-"`
/*
Required: true
In: body
*/
Body *models.HasPermissionRequest
}
// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface
// for simple values it will use straight method calls.
//
// To ensure default values, the struct must have been initialized with NewHasPermissionToParams() beforehand.
func (o *HasPermissionToParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error {
var res []error
o.HTTPRequest = r
if runtime.HasBody(r) {
defer r.Body.Close()
var body models.HasPermissionRequest
if err := route.Consumer.Consume(r.Body, &body); err != nil {
if err == io.EOF {
res = append(res, errors.Required("body", "body", ""))
} else {
res = append(res, errors.NewParseError("body", "body", "", err))
}
} else {
// validate body object
if err := body.Validate(route.Formats); err != nil {
res = append(res, err)
}
if len(res) == 0 {
o.Body = &body
}
}
} else {
res = append(res, errors.Required("body", "body", ""))
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}

View File

@@ -0,0 +1,133 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"net/http"
"github.com/go-openapi/runtime"
"github.com/minio/console/models"
)
// HasPermissionToCreatedCode is the HTTP code returned for type HasPermissionToCreated
const HasPermissionToCreatedCode int = 201
/*HasPermissionToCreated A successful response.
swagger:response hasPermissionToCreated
*/
type HasPermissionToCreated struct {
/*
In: Body
*/
Payload *models.HasPermissionResponse `json:"body,omitempty"`
}
// NewHasPermissionToCreated creates HasPermissionToCreated with default headers values
func NewHasPermissionToCreated() *HasPermissionToCreated {
return &HasPermissionToCreated{}
}
// WithPayload adds the payload to the has permission to created response
func (o *HasPermissionToCreated) WithPayload(payload *models.HasPermissionResponse) *HasPermissionToCreated {
o.Payload = payload
return o
}
// SetPayload sets the payload to the has permission to created response
func (o *HasPermissionToCreated) SetPayload(payload *models.HasPermissionResponse) {
o.Payload = payload
}
// WriteResponse to the client
func (o *HasPermissionToCreated) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(201)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}
/*HasPermissionToDefault Generic error response.
swagger:response hasPermissionToDefault
*/
type HasPermissionToDefault struct {
_statusCode int
/*
In: Body
*/
Payload *models.Error `json:"body,omitempty"`
}
// NewHasPermissionToDefault creates HasPermissionToDefault with default headers values
func NewHasPermissionToDefault(code int) *HasPermissionToDefault {
if code <= 0 {
code = 500
}
return &HasPermissionToDefault{
_statusCode: code,
}
}
// WithStatusCode adds the status to the has permission to default response
func (o *HasPermissionToDefault) WithStatusCode(code int) *HasPermissionToDefault {
o._statusCode = code
return o
}
// SetStatusCode sets the status to the has permission to default response
func (o *HasPermissionToDefault) SetStatusCode(code int) {
o._statusCode = code
}
// WithPayload adds the payload to the has permission to default response
func (o *HasPermissionToDefault) WithPayload(payload *models.Error) *HasPermissionToDefault {
o.Payload = payload
return o
}
// SetPayload sets the payload to the has permission to default response
func (o *HasPermissionToDefault) SetPayload(payload *models.Error) {
o.Payload = payload
}
// WriteResponse to the client
func (o *HasPermissionToDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) {
rw.WriteHeader(o._statusCode)
if o.Payload != nil {
payload := o.Payload
if err := producer.Produce(rw, payload); err != nil {
panic(err) // let the recovery middleware deal with this
}
}
}

View File

@@ -0,0 +1,104 @@
// Code generated by go-swagger; DO NOT EDIT.
// 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/>.
//
package user_api
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the generate command
import (
"errors"
"net/url"
golangswaggerpaths "path"
)
// HasPermissionToURL generates an URL for the has permission to operation
type HasPermissionToURL struct {
_basePath string
}
// WithBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *HasPermissionToURL) WithBasePath(bp string) *HasPermissionToURL {
o.SetBasePath(bp)
return o
}
// SetBasePath sets the base path for this url builder, only required when it's different from the
// base path specified in the swagger spec.
// When the value of the base path is an empty string
func (o *HasPermissionToURL) SetBasePath(bp string) {
o._basePath = bp
}
// Build a url path and query string
func (o *HasPermissionToURL) Build() (*url.URL, error) {
var _result url.URL
var _path = "/has-permission"
_basePath := o._basePath
if _basePath == "" {
_basePath = "/api/v1"
}
_result.Path = golangswaggerpaths.Join(_basePath, _path)
return &_result, nil
}
// Must is a helper function to panic when the url builder returns an error
func (o *HasPermissionToURL) Must(u *url.URL, err error) *url.URL {
if err != nil {
panic(err)
}
if u == nil {
panic("url can't be nil")
}
return u
}
// String returns the string representation of the path with query string
func (o *HasPermissionToURL) String() string {
return o.Must(o.Build()).String()
}
// BuildFull builds a full url with scheme, host, path and query string
func (o *HasPermissionToURL) BuildFull(scheme, host string) (*url.URL, error) {
if scheme == "" {
return nil, errors.New("scheme is required for a full url on HasPermissionToURL")
}
if host == "" {
return nil, errors.New("host is required for a full url on HasPermissionToURL")
}
base, err := o.Build()
if err != nil {
return nil, err
}
base.Scheme = scheme
base.Host = host
return base, nil
}
// StringFull returns the string representation of a complete url
func (o *HasPermissionToURL) StringFull(scheme, host string) string {
return o.Must(o.BuildFull(scheme, host)).String()
}

View File

@@ -21,6 +21,8 @@ import (
"net/http"
"time"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
@@ -42,6 +44,15 @@ func registerAccountHandlers(api *operations.ConsoleAPI) {
user_api.NewLoginCreated().WithPayload(changePasswordResponse).WriteResponse(w, p)
})
})
// Checks if user can perform an action
api.UserAPIHasPermissionToHandler = user_api.HasPermissionToHandlerFunc(func(params user_api.HasPermissionToParams, session *models.Principal) middleware.Responder {
hasPermissionRespose, err := getUserHasPermissionsResponse(session, params)
if err != nil {
return user_api.NewHasPermissionToDefault(500).WithPayload(err)
}
// Custom response writer to update the session cookies
return user_api.NewHasPermissionToCreated().WithPayload(hasPermissionRespose)
})
}
// changePassword validate current current user password and if it's correct set the new password
@@ -92,3 +103,48 @@ func getChangePasswordResponse(session *models.Principal, params user_api.Accoun
}
return loginResponse, nil
}
func getUserHasPermissionsResponse(session *models.Principal, params user_api.HasPermissionToParams) (*models.HasPermissionResponse, *models.Error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
mAdmin, err := newMAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := adminClient{client: mAdmin}
userPolicy, err := getAccountPolicy(ctx, adminClient)
if err != nil {
return nil, prepareError(err)
}
var perms []*models.PermissionAction
for _, p := range params.Body.Actions {
canPerform := userCanDo(iampolicy.Args{
Action: iampolicy.Action(p.Action),
BucketName: p.BucketName,
}, userPolicy)
perms = append(perms, &models.PermissionAction{
Can: canPerform,
ID: p.ID,
})
}
return &models.HasPermissionResponse{
Permissions: perms,
}, nil
}
func userCanDo(arg iampolicy.Args, userPolicy *iampolicy.Policy) bool {
// check in all the statements if any allows the passed action
for _, stmt := range userPolicy.Statements {
if stmt.IsAllowed(arg) {
return true
}
}
return false
}

View File

@@ -18,9 +18,12 @@ package restapi
import (
"context"
"encoding/json"
"errors"
"testing"
iampolicy "github.com/minio/minio/pkg/iam/policy"
"github.com/minio/console/models"
)
@@ -106,3 +109,152 @@ func Test_changePassword(t *testing.T) {
})
}
}
func Test_useCanDo(t *testing.T) {
type args struct {
arg iampolicy.Args
userPolicy string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "Create Bucket",
args: args{
arg: iampolicy.Args{
Action: "s3:CreateBucket",
},
userPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"admin:*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`,
},
want: true,
},
{
name: "Create Bucket, No Admin",
args: args{
arg: iampolicy.Args{
Action: "s3:CreateBucket",
},
userPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::*"
]
}
]
}`,
},
want: true,
},
{
name: "Create Bucket, By Prefix",
args: args{
arg: iampolicy.Args{
Action: "s3:CreateBucket",
},
userPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::bucket1"
]
}
]
}`,
},
want: false,
},
{
name: "Create Bucket, With Bucket Name",
args: args{
arg: iampolicy.Args{
Action: "s3:CreateBucket",
BucketName: "bucket2",
},
userPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*"
],
"Resource": [
"arn:aws:s3:::bucket1"
]
}
]
}`,
},
want: false,
},
{
name: "Can't Create Bucket",
args: args{
arg: iampolicy.Args{
Action: "s3:CreateBucket",
BucketName: "bucket2",
},
userPolicy: `{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": [
"arn:aws:s3:::bucket1",
"arn:aws:s3:::bucket1/*",
"arn:aws:s3:::lkasdkljasd090901",
"arn:aws:s3:::lkasdkljasd090901/*"
]
}
]
}`,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var pol iampolicy.Policy
if err := json.Unmarshal([]byte(tt.args.userPolicy), &pol); err != nil {
t.Errorf("Policy can't be parsed: %s", err)
}
if got := userCanDo(tt.args.arg, &pol); got != tt.want {
t.Errorf("userCanDo() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -324,6 +324,7 @@ func getAccountInfo(ctx context.Context, client MinioAdmin) ([]*models.Bucket, e
if err != nil {
return []*models.Bucket{}, err
}
var bucketInfos []*models.Bucket
for _, bucket := range info.Buckets {
bucketElem := &models.Bucket{Name: swag.String(bucket.Name), CreationDate: bucket.Created.Format(time.RFC3339), Size: int64(bucket.Size)}

View File

@@ -946,6 +946,28 @@ paths:
tags:
- UserAPI
/has-permission:
post:
summary: Checks whether the user can perform a series of actions
operationId: HasPermissionTo
parameters:
- name: body
in: body
required: true
schema:
$ref: "#/definitions/hasPermissionRequest"
responses:
201:
description: A successful response.
schema:
$ref: "#/definitions/hasPermissionResponse"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- UserAPI
/users:
get:
summary: List Users
@@ -1557,14 +1579,14 @@ paths:
summary: Activate a particular tenant using the existing subscription license
operationId: SubscriptionActivate
parameters:
- name: namespace
in: path
required: true
type: string
- name: tenant
in: path
required: true
type: string
- name: namespace
in: path
required: true
type: string
- name: tenant
in: path
required: true
type: string
responses:
204:
description: A successful response.
@@ -2145,7 +2167,7 @@ paths:
$ref: "#/definitions/error"
tags:
- AdminAPI
/direct-csi/drives:
get:
summary: Get direct-csi drives list
@@ -2170,7 +2192,7 @@ paths:
$ref: "#/definitions/error"
tags:
- AdminAPI
/direct-csi/volumes:
get:
summary: Get direct-csi volumes list
@@ -2778,7 +2800,7 @@ definitions:
type: string
status:
type: string
enum: [ok]
enum: [ ok ]
operator:
type: boolean
widgetResult:
@@ -4050,7 +4072,7 @@ definitions:
restart:
description: Returns wheter server needs to restart to apply changes or not
type: boolean
subscriptionValidateRequest:
type: object
@@ -4105,7 +4127,7 @@ definitions:
type: string
message:
type: string
getDirectCSIVolumeListResponse:
type: object
properties:
@@ -4113,7 +4135,7 @@ definitions:
type: array
items:
$ref: "#/definitions/directCSIVolumeInfo"
directCSIVolumeInfo:
type: object
properties:
@@ -4126,3 +4148,34 @@ definitions:
type: string
drive:
type: string
policyArgs:
type: object
properties:
id:
type: string
action:
type: string
bucket_name:
type: string
hasPermissionRequest:
type: object
properties:
actions:
type: array
items:
$ref: "#/definitions/policyArgs"
permissionAction:
type: object
properties:
id:
type: string
can:
type: boolean
hasPermissionResponse:
type: object
properties:
permissions:
type: array
items:
$ref: "#/definitions/permissionAction"