Dynamic UI components (#1162)

Hide/Show UI components based on the IAM policy of the current user

- Buckets lists: hide/show manage button
- Bucket admin page: left menu items enable/disable
- Bucket admin page: bucket configuration buttons are enabled/disabled
- Bucket admin page: hide/show create buttons
- Bucket admin page: enable/disable requests to backend service
- Object browser: hide/show bucket buttons for upload, delete, etc
- Object browser: hide/show bucket configuration button
- Object details: hide/show object buttons, ie: delete
- Object details: hide/show object attributes, ie: legal hold,
  retention, tags, etc

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>

Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Lenin Alevski
2021-11-02 17:34:39 -07:00
committed by GitHub
parent e1a3164cd9
commit 184f864873
32 changed files with 1905 additions and 709 deletions

View File

@@ -40,12 +40,18 @@ type Bucket struct {
// access
Access *BucketAccess `json:"access,omitempty"`
// allowed actions
AllowedActions []string `json:"allowedActions"`
// creation date
CreationDate string `json:"creation_date,omitempty"`
// details
Details *BucketDetails `json:"details,omitempty"`
// manage
Manage bool `json:"manage,omitempty"`
// name
// Required: true
// Min Length: 3

132
models/iam_policy.go Normal file
View File

@@ -0,0 +1,132 @@
// 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 (
"context"
"strconv"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// IamPolicy iam policy
//
// swagger:model iamPolicy
type IamPolicy struct {
// statement
Statement []*IamPolicyStatement `json:"statement"`
// version
Version string `json:"version,omitempty"`
}
// Validate validates this iam policy
func (m *IamPolicy) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateStatement(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *IamPolicy) validateStatement(formats strfmt.Registry) error {
if swag.IsZero(m.Statement) { // not required
return nil
}
for i := 0; i < len(m.Statement); i++ {
if swag.IsZero(m.Statement[i]) { // not required
continue
}
if m.Statement[i] != nil {
if err := m.Statement[i].Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("statement" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// ContextValidate validate this iam policy based on the context it is used
func (m *IamPolicy) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateStatement(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *IamPolicy) contextValidateStatement(ctx context.Context, formats strfmt.Registry) error {
for i := 0; i < len(m.Statement); i++ {
if m.Statement[i] != nil {
if err := m.Statement[i].ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("statement" + "." + strconv.Itoa(i))
}
return err
}
}
}
return nil
}
// MarshalBinary interface implementation
func (m *IamPolicy) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *IamPolicy) UnmarshalBinary(b []byte) error {
var res IamPolicy
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -0,0 +1,76 @@
// 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 (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// IamPolicyStatement iam policy statement
//
// swagger:model iamPolicyStatement
type IamPolicyStatement struct {
// action
Action []string `json:"action"`
// condition
Condition map[string]interface{} `json:"condition,omitempty"`
// effect
Effect string `json:"effect,omitempty"`
// resource
Resource []string `json:"resource"`
}
// Validate validates this iam policy statement
func (m *IamPolicyStatement) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this iam policy statement based on context it is used
func (m *IamPolicyStatement) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *IamPolicyStatement) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *IamPolicyStatement) UnmarshalBinary(b []byte) error {
var res IamPolicyStatement
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -49,6 +49,9 @@ type SessionResponse struct {
// pages
Pages []string `json:"pages"`
// policy
Policy *IamPolicy `json:"policy,omitempty"`
// status
// Enum: [ok]
Status string `json:"status,omitempty"`
@@ -58,6 +61,10 @@ type SessionResponse struct {
func (m *SessionResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validatePolicy(formats); err != nil {
res = append(res, err)
}
if err := m.validateStatus(formats); err != nil {
res = append(res, err)
}
@@ -68,6 +75,23 @@ func (m *SessionResponse) Validate(formats strfmt.Registry) error {
return nil
}
func (m *SessionResponse) validatePolicy(formats strfmt.Registry) error {
if swag.IsZero(m.Policy) { // not required
return nil
}
if m.Policy != nil {
if err := m.Policy.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("policy")
}
return err
}
}
return nil
}
var sessionResponseTypeStatusPropEnum []interface{}
func init() {
@@ -107,8 +131,31 @@ func (m *SessionResponse) validateStatus(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this session response based on context it is used
// ContextValidate validate this session response based on the context it is used
func (m *SessionResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidatePolicy(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *SessionResponse) contextValidatePolicy(ctx context.Context, formats strfmt.Registry) error {
if m.Policy != nil {
if err := m.Policy.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("policy")
}
return err
}
}
return nil
}

81
pkg/acl/permissions.go Normal file
View File

@@ -0,0 +1,81 @@
// This file is part of MinIO Orchestrator
// 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 acl
import iampolicy "github.com/minio/pkg/iam/policy"
var BucketViewerRole = iampolicy.NewActionSet(
iampolicy.ListBucketAction,
iampolicy.GetObjectAction,
)
var BucketEditorRole = iampolicy.NewActionSet(
iampolicy.ListBucketAction,
iampolicy.GetObjectAction,
iampolicy.DeleteObjectAction,
iampolicy.PutObjectAction,
)
var BucketAdminRole = iampolicy.NewActionSet(
iampolicy.AbortMultipartUploadAction,
iampolicy.CreateBucketAction,
iampolicy.DeleteBucketAction,
iampolicy.ForceDeleteBucketAction,
iampolicy.DeleteBucketPolicyAction,
iampolicy.GetBucketLocationAction,
iampolicy.GetBucketNotificationAction,
iampolicy.GetBucketPolicyAction,
iampolicy.HeadBucketAction,
iampolicy.ListAllMyBucketsAction,
iampolicy.GetBucketPolicyStatusAction,
iampolicy.ListBucketVersionsAction,
iampolicy.ListBucketMultipartUploadsAction,
iampolicy.ListenNotificationAction,
iampolicy.ListenBucketNotificationAction,
iampolicy.ListMultipartUploadPartsAction,
iampolicy.PutBucketLifecycleAction,
iampolicy.GetBucketLifecycleAction,
iampolicy.PutBucketNotificationAction,
iampolicy.PutBucketPolicyAction,
iampolicy.BypassGovernanceRetentionAction,
iampolicy.PutObjectRetentionAction,
iampolicy.GetObjectRetentionAction,
iampolicy.GetObjectLegalHoldAction,
iampolicy.PutObjectLegalHoldAction,
iampolicy.GetBucketObjectLockConfigurationAction,
iampolicy.PutBucketObjectLockConfigurationAction,
iampolicy.GetBucketTaggingAction,
iampolicy.PutBucketTaggingAction,
iampolicy.GetObjectVersionAction,
iampolicy.GetObjectVersionTaggingAction,
iampolicy.DeleteObjectVersionAction,
iampolicy.DeleteObjectVersionTaggingAction,
iampolicy.PutObjectVersionTaggingAction,
iampolicy.GetObjectTaggingAction,
iampolicy.PutObjectTaggingAction,
iampolicy.DeleteObjectTaggingAction,
iampolicy.PutBucketEncryptionAction,
iampolicy.GetBucketEncryptionAction,
iampolicy.PutBucketVersioningAction,
iampolicy.GetBucketVersioningAction,
iampolicy.GetReplicationConfigurationAction,
iampolicy.PutReplicationConfigurationAction,
iampolicy.ReplicateObjectAction,
iampolicy.ReplicateDeleteAction,
iampolicy.ReplicateTagsAction,
iampolicy.GetObjectVersionForReplicationAction,
iampolicy.AllActions,
)

View File

@@ -32,12 +32,21 @@ import { ErrorResponseHandler } from "../../../../common/types";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import api from "../../../../common/api";
import history from "../../../../history";
import { BucketInfo } from "../types";
import { displayComponent } from "../../../../utils/permissions";
import {
ADMIN_GET_POLICY,
ADMIN_LIST_GROUPS,
ADMIN_LIST_USER_POLICIES,
ADMIN_LIST_USERS,
} from "../../../../types";
const styles = (theme: Theme) => createStyles({});
const mapState = (state: AppState) => ({
session: state.console.session,
loadingBucket: state.buckets.bucketDetails.loadingBucket,
bucketInfo: state.buckets.bucketDetails.bucketInfo,
});
const connector = connect(mapState, { setErrorSnackMessage });
@@ -55,6 +64,7 @@ interface IAccessDetailsProps {
classes: any;
match: any;
loadingBucket: boolean;
bucketInfo: BucketInfo | null;
}
const AccessDetails = ({
@@ -63,6 +73,7 @@ const AccessDetails = ({
setErrorSnackMessage,
session,
loadingBucket,
bucketInfo,
}: IAccessDetailsProps) => {
const [curTab, setCurTab] = useState<number>(0);
const [loadingPolicies, setLoadingPolicies] = useState<boolean>(true);
@@ -72,7 +83,15 @@ const AccessDetails = ({
const bucketName = match.params["bucketName"];
const usersEnabled = session.pages?.indexOf("/users") > -1;
const displayPoliciesList = displayComponent(bucketInfo?.allowedActions, [
ADMIN_LIST_USER_POLICIES,
]);
const displayUsersList = displayComponent(
bucketInfo?.allowedActions,
[ADMIN_GET_POLICY, ADMIN_LIST_USERS, ADMIN_LIST_GROUPS],
true
);
useEffect(() => {
if (loadingBucket) {
@@ -100,32 +119,41 @@ const AccessDetails = ({
];
useEffect(() => {
if (loadingUsers && usersEnabled) {
api
.invoke("GET", `/api/v1/bucket-users/${bucketName}`)
.then((res: any) => {
setBucketUsers(res);
setLoadingUsers(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingUsers(false);
});
if (loadingUsers) {
if (displayUsersList) {
api
.invoke("GET", `/api/v1/bucket-users/${bucketName}`)
.then((res: any) => {
setBucketUsers(res);
setLoadingUsers(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingUsers(false);
});
} else {
setLoadingUsers(false);
}
}
}, [loadingUsers, setErrorSnackMessage, bucketName, usersEnabled]);
}, [loadingUsers, setErrorSnackMessage, bucketName]);
useEffect(() => {
if (loadingPolicies) {
api
.invoke("GET", `/api/v1/bucket-policy/${bucketName}`)
.then((res: any) => {
setBucketPolicy(res.policies);
setLoadingPolicies(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingPolicies(false);
});
console.log("displayPoliciesList", displayPoliciesList);
if (displayPoliciesList) {
api
.invoke("GET", `/api/v1/bucket-policy/${bucketName}`)
.then((res: any) => {
setBucketPolicy(res.policies);
setLoadingPolicies(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingPolicies(false);
});
} else {
setLoadingPolicies(false);
}
}
}, [loadingPolicies, setErrorSnackMessage, bucketName]);
@@ -144,11 +172,12 @@ const AccessDetails = ({
scrollButtons="auto"
>
<Tab label="Policies" {...a11yProps(0)} />
{usersEnabled && <Tab label="Users" {...a11yProps(1)} />}
{displayUsersList && <Tab label="Users" {...a11yProps(1)} />}
</Tabs>
<Paper>
<TabPanel index={0} value={curTab}>
<TableWrapper
disabled={!displayPoliciesList}
noBackground={true}
itemActions={PolicyActions}
columns={[{ label: "Name", elementKey: "name" }]}
@@ -158,7 +187,7 @@ const AccessDetails = ({
idField="name"
/>
</TabPanel>
{usersEnabled && (
{displayUsersList && (
<TabPanel index={1} value={curTab}>
<TableWrapper
noBackground={true}

View File

@@ -37,6 +37,9 @@ import {
objectBrowserCommon,
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import { BucketInfo } from "../types";
import { displayComponent } from "../../../../utils/permissions";
import { S3_GET_BUCKET_POLICY, S3_PUT_BUCKET_POLICY } from "../../../../types";
const styles = (theme: Theme) =>
createStyles({
@@ -109,6 +112,7 @@ const styles = (theme: Theme) =>
const mapState = (state: AppState) => ({
session: state.console.session,
loadingBucket: state.buckets.bucketDetails.loadingBucket,
bucketInfo: state.buckets.bucketDetails.bucketInfo,
});
const connector = connect(mapState, { setErrorSnackMessage });
@@ -119,6 +123,7 @@ interface IAccessRuleProps {
classes: any;
match: any;
loadingBucket: boolean;
bucketInfo: BucketInfo | null;
}
const AccessRule = ({
@@ -126,6 +131,7 @@ const AccessRule = ({
match,
setErrorSnackMessage,
loadingBucket,
bucketInfo,
}: IAccessRuleProps) => {
const [loadingAccessRules, setLoadingAccessRules] = useState<boolean>(true);
const [accessRules, setAccessRules] = useState([]);
@@ -139,6 +145,16 @@ const AccessRule = ({
const bucketName = match.params["bucketName"];
const displayAccessRules = displayComponent(bucketInfo?.allowedActions, [
S3_GET_BUCKET_POLICY,
]);
const displayAddAccessRules = displayComponent(
bucketInfo?.allowedActions,
[S3_GET_BUCKET_POLICY, S3_PUT_BUCKET_POLICY],
true
);
useEffect(() => {
if (loadingBucket) {
setLoadingAccessRules(true);
@@ -165,16 +181,20 @@ const AccessRule = ({
useEffect(() => {
if (loadingAccessRules) {
api
.invoke("GET", `/api/v1/bucket/${bucketName}/access-rules`)
.then((res: any) => {
setAccessRules(res.accessRules);
setLoadingAccessRules(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingAccessRules(false);
});
if (displayAccessRules) {
api
.invoke("GET", `/api/v1/bucket/${bucketName}/access-rules`)
.then((res: any) => {
setAccessRules(res.accessRules);
setLoadingAccessRules(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingAccessRules(false);
});
} else {
setLoadingAccessRules(false);
}
}
}, [loadingAccessRules, setErrorSnackMessage, bucketName]);
@@ -221,24 +241,27 @@ const AccessRule = ({
)}
<Grid item xs={12} className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Access Rules</h1>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
component="label"
onClick={() => {
setAddAccessRuleOpen(true);
}}
className={classes.listButton}
>
Add Access Rule
</Button>
{displayAddAccessRules && (
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
component="label"
onClick={() => {
setAddAccessRuleOpen(true);
}}
className={classes.listButton}
>
Add Access Rule
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Paper>
<TableWrapper
disabled={!displayAccessRules}
noBackground={true}
itemActions={AccessRuleActions}
columns={[

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, { Fragment, useEffect } from "react";
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
@@ -29,6 +29,8 @@ import ObjectDetails from "../ListBuckets/Objects/ObjectDetails/ObjectDetails";
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
import PageHeader from "../../Common/PageHeader/PageHeader";
import { SettingsIcon } from "../../../../icons";
import { BucketInfo } from "../types";
import { setErrorSnackMessage } from "../../../../actions";
interface IBrowserHandlerProps {
fileMode: boolean;
@@ -36,6 +38,8 @@ interface IBrowserHandlerProps {
history: any;
classes: any;
setFileModeEnabled: typeof setFileModeEnabled;
setErrorSnackMessage: typeof setErrorSnackMessage;
bucketInfo: BucketInfo | null;
}
const styles = (theme: Theme) =>
@@ -53,6 +57,7 @@ const BrowserHandler = ({
history,
classes,
setFileModeEnabled,
bucketInfo,
}: IBrowserHandlerProps) => {
const bucketName = match.params["bucketName"];
const internalPaths = get(match.params, "subpaths", "");
@@ -77,19 +82,21 @@ const BrowserHandler = ({
</Fragment>
}
actions={
<Fragment>
<Tooltip title={"Configure Bucket"}>
<IconButton
color="primary"
aria-label="Configure Bucket"
component="span"
onClick={openBucketConfiguration}
size="large"
>
<SettingsIcon />
</IconButton>
</Tooltip>
</Fragment>
bucketInfo?.manage && (
<Fragment>
<Tooltip title={"Configure Bucket"}>
<IconButton
color="primary"
aria-label="Configure Bucket"
component="span"
onClick={openBucketConfiguration}
size="large"
>
<SettingsIcon />
</IconButton>
</Tooltip>
</Fragment>
)
}
/>
<Grid container className={classes.container}>
@@ -99,13 +106,15 @@ const BrowserHandler = ({
);
};
const mapStateToProps = ({ objectBrowser }: AppState) => ({
const mapStateToProps = ({ objectBrowser, buckets }: AppState) => ({
fileMode: get(objectBrowser, "fileMode", false),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
bucketInfo: buckets.bucketDetails.bucketInfo,
});
const mapDispatchToProps = {
setFileModeEnabled,
setErrorSnackMessage,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -22,7 +22,7 @@ import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Grid from "@mui/material/Grid";
import api from "../../../../common/api";
import { BucketInfo, HasPermissionResponse } from "../types";
import { BucketInfo } from "../types";
import {
actionsTray,
buttonsStyles,
@@ -54,6 +54,23 @@ import DeleteBucket from "../ListBuckets/DeleteBucket";
import AccessRulePanel from "./AccessRulePanel";
import RefreshIcon from "../../../../icons/RefreshIcon";
import BoxIconButton from "../../Common/BoxIconButton";
import {
ADMIN_GET_POLICY,
ADMIN_LIST_USER_POLICIES,
ADMIN_LIST_USERS,
S3_DELETE_BUCKET,
S3_FORCE_DELETE_BUCKET,
S3_GET_BUCKET_NOTIFICATIONS,
S3_GET_BUCKET_POLICY,
S3_GET_LIFECYCLE_CONFIGURATION,
S3_GET_REPLICATION_CONFIGURATION,
S3_LISTEN_BUCKET_NOTIFICATIONS,
S3_PUT_BUCKET_NOTIFICATIONS,
S3_PUT_LIFECYCLE_CONFIGURATION,
S3_PUT_REPLICATION_CONFIGURATION,
} from "../../../../types";
import { displayComponent } from "../../../../utils/permissions";
import { ISessionResponse } from "../../types";
const styles = (theme: Theme) =>
createStyles({
@@ -162,6 +179,9 @@ const styles = (theme: Theme) =>
...actionsTray.actionsTray,
padding: "15px 0 0",
},
capitalize: {
textTransform: "capitalize",
},
...hrClass,
...buttonsStyles,
...containerForHeader(theme.spacing(4)),
@@ -195,8 +215,6 @@ const BucketDetails = ({
bucketInfo,
}: IBucketDetailsProps) => {
const [iniLoad, setIniLoad] = useState<boolean>(false);
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [canGetReplication, setCanGetReplication] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const bucketName = match.params["bucketName"];
@@ -242,45 +260,6 @@ const BucketDetails = ({
}
}, [match, bucketName, setBucketDetailsTab, selectedTab]);
// check the permissions for creating bucket
useEffect(() => {
if (loadingPerms) {
api
.invoke("POST", `/api/v1/has-permission`, {
actions: [
{
id: "GetReplicationConfiguration",
action: "s3:GetReplicationConfiguration",
bucket_name: bucketName,
},
],
})
.then((res: HasPermissionResponse) => {
setLoadingPerms(false);
if (!res.permissions) {
return;
}
const actions = res.permissions ? res.permissions : [];
let canGetReplicationVal = actions.find(
(s) => s.id === "GetReplicationConfiguration"
);
if (canGetReplicationVal && canGetReplicationVal.can) {
setCanGetReplication(true);
} else {
setCanGetReplication(false);
}
setLoadingPerms(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [bucketName, loadingPerms, setErrorSnackMessage]);
const changeRoute = (newTab: string) => {
let mainRoute = `/buckets/${bucketName}`;
@@ -364,27 +343,38 @@ const BucketDetails = ({
}
title={bucketName}
subTitle={
<Fragment>
Access:{" "}
{bucketInfo &&
bucketInfo?.access[0].toUpperCase() +
bucketInfo?.access.substr(1).toLowerCase()}
</Fragment>
displayComponent(
bucketInfo?.allowedActions,
[S3_GET_BUCKET_POLICY],
false
) && (
<Fragment>
Access:{" "}
<span className={classes.capitalize}>
{bucketInfo?.access.toLowerCase()}
</span>
</Fragment>
)
}
actions={
<Fragment>
<Tooltip title={"Delete"}>
<BoxIconButton
color="primary"
aria-label="Delete"
onClick={() => {
setDeleteOpen(true);
}}
size="large"
>
<DeleteIcon />
</BoxIconButton>
</Tooltip>
{displayComponent(bucketInfo?.allowedActions, [
S3_DELETE_BUCKET,
S3_FORCE_DELETE_BUCKET,
]) && (
<Tooltip title={"Delete"}>
<BoxIconButton
color="primary"
aria-label="Delete"
onClick={() => {
setDeleteOpen(true);
}}
size="large"
>
<DeleteIcon />
</BoxIconButton>
</Tooltip>
)}
<Tooltip title={"Refresh"}>
<BoxIconButton
color="primary"
@@ -413,6 +403,12 @@ const BucketDetails = ({
<ListItemText primary="Summary" />
</ListItem>
<ListItem
disabled={
!displayComponent(bucketInfo?.allowedActions, [
S3_GET_BUCKET_NOTIFICATIONS,
S3_PUT_BUCKET_NOTIFICATIONS,
])
}
button
selected={selectedTab === "events"}
onClick={() => {
@@ -423,7 +419,13 @@ const BucketDetails = ({
</ListItem>
<ListItem
button
disabled={!canGetReplication}
disabled={
!distributedSetup ||
!displayComponent(bucketInfo?.allowedActions, [
S3_GET_REPLICATION_CONFIGURATION,
S3_PUT_REPLICATION_CONFIGURATION,
])
}
selected={selectedTab === "replication"}
onClick={() => {
changeRoute("replication");
@@ -431,9 +433,15 @@ const BucketDetails = ({
>
<ListItemText primary="Replication" />
</ListItem>
<ListItem
button
disabled={
!distributedSetup ||
!displayComponent(bucketInfo?.allowedActions, [
S3_GET_LIFECYCLE_CONFIGURATION,
S3_PUT_LIFECYCLE_CONFIGURATION,
])
}
selected={selectedTab === "lifecycle"}
onClick={() => {
changeRoute("lifecycle");
@@ -443,6 +451,13 @@ const BucketDetails = ({
</ListItem>
<ListItem
button
disabled={
!displayComponent(bucketInfo?.allowedActions, [
ADMIN_GET_POLICY,
ADMIN_LIST_USER_POLICIES,
ADMIN_LIST_USERS,
])
}
selected={selectedTab === "access"}
onClick={() => {
changeRoute("access");
@@ -452,6 +467,11 @@ const BucketDetails = ({
</ListItem>
<ListItem
button
disabled={
!displayComponent(bucketInfo?.allowedActions, [
S3_GET_BUCKET_POLICY,
])
}
selected={selectedTab === "prefix"}
onClick={() => {
changeRoute("prefix");

View File

@@ -23,7 +23,7 @@ import { Button } from "@mui/material";
import get from "lodash/get";
import Grid from "@mui/material/Grid";
import { AddIcon, LambdaIcon } from "../../../../icons";
import { BucketEvent, BucketEventList } from "../types";
import { BucketEvent, BucketEventList, BucketInfo } from "../types";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import {
@@ -36,6 +36,12 @@ import api from "../../../../common/api";
import DeleteEvent from "./DeleteEvent";
import AddEvent from "./AddEvent";
import HelpBox from "../../../../common/HelpBox";
import { displayComponent } from "../../../../utils/permissions";
import {
ADMIN_SERVER_INFO,
S3_GET_BUCKET_NOTIFICATIONS,
S3_PUT_BUCKET_NOTIFICATIONS,
} from "../../../../types";
const styles = (theme: Theme) =>
createStyles({
@@ -54,6 +60,7 @@ interface IBucketEventsProps {
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
loadingBucket: boolean;
bucketInfo: BucketInfo | null;
}
const BucketEventsPanel = ({
@@ -61,6 +68,7 @@ const BucketEventsPanel = ({
match,
setErrorSnackMessage,
loadingBucket,
bucketInfo,
}: IBucketEventsProps) => {
const [addEventScreenOpen, setAddEventScreenOpen] = useState<boolean>(false);
const [loadingEvents, setLoadingEvents] = useState<boolean>(true);
@@ -70,6 +78,16 @@ const BucketEventsPanel = ({
const bucketName = match.params["bucketName"];
const displayEvents = displayComponent(bucketInfo?.allowedActions, [
S3_GET_BUCKET_NOTIFICATIONS,
]);
const displaySubscribeToEvents = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_BUCKET_NOTIFICATIONS, ADMIN_SERVER_INFO],
true
);
useEffect(() => {
if (loadingBucket) {
setLoadingEvents(true);
@@ -78,17 +96,21 @@ const BucketEventsPanel = ({
useEffect(() => {
if (loadingEvents) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/events`)
.then((res: BucketEventList) => {
const events = get(res, "events", []);
setLoadingEvents(false);
setRecords(events || []);
})
.catch((err: ErrorResponseHandler) => {
setLoadingEvents(false);
setErrorSnackMessage(err);
});
if (displayEvents) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/events`)
.then((res: BucketEventList) => {
const events = get(res, "events", []);
setLoadingEvents(false);
setRecords(events || []);
})
.catch((err: ErrorResponseHandler) => {
setLoadingEvents(false);
setErrorSnackMessage(err);
});
} else {
setLoadingEvents(false);
}
}
}, [loadingEvents, setErrorSnackMessage, bucketName]);
@@ -136,20 +158,23 @@ const BucketEventsPanel = ({
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Events</h1>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
size="medium"
onClick={() => {
setAddEventScreenOpen(true);
}}
>
Subscribe to Event
</Button>
{displaySubscribeToEvents && (
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
size="medium"
onClick={() => {
setAddEventScreenOpen(true);
}}
>
Subscribe to Event
</Button>
)}
</Grid>
<Grid item xs={12}>
<TableWrapper
disabled={!displayEvents}
itemActions={tableActions}
columns={[
{ label: "SQS", elementKey: "arn" },
@@ -203,6 +228,7 @@ const BucketEventsPanel = ({
const mapState = (state: AppState) => ({
session: state.console.session,
loadingBucket: state.buckets.bucketDetails.loadingBucket,
bucketInfo: state.buckets.bucketDetails.bucketInfo,
});
const connector = connect(mapState, {

View File

@@ -23,7 +23,7 @@ import { Button } from "@mui/material";
import get from "lodash/get";
import * as reactMoment from "react-moment";
import Grid from "@mui/material/Grid";
import { LifeCycleItem } from "../types";
import { BucketInfo, LifeCycleItem } from "../types";
import { AddIcon, TiersIcon } from "../../../../icons";
import {
actionsTray,
@@ -37,6 +37,14 @@ import EditLifecycleConfiguration from "./EditLifecycleConfiguration";
import AddLifecycleModal from "./AddLifecycleModal";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import HelpBox from "../../../../common/HelpBox";
import { displayComponent } from "../../../../utils/permissions";
import {
ADMIN_LIST_TIERS,
S3_GET_LIFECYCLE_CONFIGURATION,
S3_GET_REPLICATION_CONFIGURATION,
S3_PUT_LIFECYCLE_CONFIGURATION,
S3_PUT_REPLICATION_CONFIGURATION,
} from "../../../../types";
const styles = (theme: Theme) =>
createStyles({
@@ -52,6 +60,7 @@ interface IBucketLifecyclePanelProps {
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
loadingBucket: boolean;
bucketInfo: BucketInfo | null;
}
const BucketLifecyclePanel = ({
@@ -59,6 +68,7 @@ const BucketLifecyclePanel = ({
match,
setErrorSnackMessage,
loadingBucket,
bucketInfo,
}: IBucketLifecyclePanelProps) => {
const [loadingLifecycle, setLoadingLifecycle] = useState<boolean>(true);
const [lifecycleRecords, setLifecycleRecords] = useState<LifeCycleItem[]>([]);
@@ -67,6 +77,16 @@ const BucketLifecyclePanel = ({
const bucketName = match.params["bucketName"];
const displayLifeCycleRules = displayComponent(bucketInfo?.allowedActions, [
S3_GET_LIFECYCLE_CONFIGURATION,
]);
const displayAddLifeCycleRules = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_LIFECYCLE_CONFIGURATION, ADMIN_LIST_TIERS],
true
);
useEffect(() => {
if (loadingBucket) {
setLoadingLifecycle(true);
@@ -75,18 +95,22 @@ const BucketLifecyclePanel = ({
useEffect(() => {
if (loadingLifecycle) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/lifecycle`)
.then((res: any) => {
const records = get(res, "lifecycle", []);
if (displayLifeCycleRules) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/lifecycle`)
.then((res: any) => {
const records = get(res, "lifecycle", []);
setLifecycleRecords(records || []);
setLoadingLifecycle(false);
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
setLoadingLifecycle(false);
});
setLifecycleRecords(records || []);
setLoadingLifecycle(false);
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
setLoadingLifecycle(false);
});
} else {
setLoadingLifecycle(false);
}
}
}, [loadingLifecycle, setLoadingLifecycle, bucketName]);
@@ -184,17 +208,19 @@ const BucketLifecyclePanel = ({
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Lifecycle Rules</h1>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
size="medium"
onClick={() => {
setAddLifecycleOpen(true);
}}
>
Add Lifecycle Rule
</Button>
{displayAddLifeCycleRules && (
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
size="medium"
onClick={() => {
setAddLifecycleOpen(true);
}}
>
Add Lifecycle Rule
</Button>
)}
</Grid>
<Grid item xs={12}>
<TableWrapper
@@ -243,6 +269,7 @@ const BucketLifecyclePanel = ({
const mapState = (state: AppState) => ({
session: state.console.session,
loadingBucket: state.buckets.bucketDetails.loadingBucket,
bucketInfo: state.buckets.bucketDetails.bucketInfo,
});
const connector = connect(mapState, {

View File

@@ -28,6 +28,7 @@ import {
searchField,
} from "../../Common/FormComponents/common/styleLibrary";
import {
BucketInfo,
BucketReplication,
BucketReplicationDestination,
BucketReplicationRule,
@@ -40,12 +41,21 @@ import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import AddReplicationModal from "./AddReplicationModal";
import DeleteReplicationRule from "./DeleteReplicationRule";
import HelpBox from "../../../../common/HelpBox";
import { displayComponent } from "../../../../utils/permissions";
import {
ADMIN_SERVER_INFO,
S3_GET_BUCKET_NOTIFICATIONS,
S3_GET_REPLICATION_CONFIGURATION,
S3_PUT_BUCKET_NOTIFICATIONS,
S3_PUT_REPLICATION_CONFIGURATION,
} from "../../../../types";
interface IBucketReplicationProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
loadingBucket: boolean;
bucketInfo: BucketInfo | null;
}
const styles = (theme: Theme) =>
@@ -62,13 +72,12 @@ const BucketReplicationPanel = ({
match,
setErrorSnackMessage,
loadingBucket,
bucketInfo,
}: IBucketReplicationProps) => {
const [canPutReplication, setCanPutReplication] = useState<boolean>(false);
const [loadingReplication, setLoadingReplication] = useState<boolean>(true);
const [replicationRules, setReplicationRules] = useState<
BucketReplicationRule[]
>([]);
const [loadingPerms, setLoadingPerms] = useState<boolean>(true);
const [deleteReplicationModal, setDeleteReplicationModal] =
useState<boolean>(false);
const [openSetReplication, setOpenSetReplication] = useState<boolean>(false);
@@ -76,68 +85,39 @@ const BucketReplicationPanel = ({
const bucketName = match.params["bucketName"];
const displayReplicationRules = displayComponent(bucketInfo?.allowedActions, [
S3_GET_REPLICATION_CONFIGURATION,
]);
const displayAddReplicationRules = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_REPLICATION_CONFIGURATION],
true
);
useEffect(() => {
if (loadingBucket) {
setLoadingReplication(true);
}
}, [loadingBucket, setLoadingReplication]);
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 userCanPutReplication = actions.find(
(s) => s.id === "PutReplicationConfiguration"
);
if (userCanPutReplication && userCanPutReplication.can) {
setCanPutReplication(true);
} else {
setCanPutReplication(false);
}
setLoadingPerms(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingPerms(false);
setErrorSnackMessage(err);
});
}
}, [bucketName, loadingPerms, setErrorSnackMessage]);
useEffect(() => {
if (loadingReplication) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/replication`)
.then((res: BucketReplication) => {
const r = res.rules ? res.rules : [];
setReplicationRules(r);
setLoadingReplication(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingReplication(false);
});
if (displayReplicationRules) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/replication`)
.then((res: BucketReplication) => {
const r = res.rules ? res.rules : [];
setReplicationRules(r);
setLoadingReplication(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingReplication(false);
});
} else {
setLoadingReplication(false);
}
}
}, [loadingReplication, setErrorSnackMessage, bucketName]);
@@ -200,21 +180,23 @@ const BucketReplicationPanel = ({
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Replication</h1>
<Button
variant="contained"
color="primary"
disabled={!canPutReplication}
startIcon={<AddIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
{displayAddReplicationRules && (
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
size="medium"
onClick={() => {
setOpenReplicationOpen(true);
}}
>
Add Replication Rule
</Button>
)}
</Grid>
<Grid item xs={12}>
<TableWrapper
disabled={!displayReplicationRules}
itemActions={replicationTableActions}
columns={[
{
@@ -275,6 +257,7 @@ const BucketReplicationPanel = ({
const mapState = (state: AppState) => ({
session: state.console.session,
loadingBucket: state.buckets.bucketDetails.loadingBucket,
bucketInfo: state.buckets.bucketDetails.bucketInfo,
});
const connector = connect(mapState, {

View File

@@ -53,6 +53,21 @@ import GavelIcon from "@mui/icons-material/Gavel";
import EnableQuota from "./EnableQuota";
import { setBucketDetailsLoad } from "../actions";
import ReportedUsageIcon from "../../../../icons/ReportedUsageIcon";
import { displayComponent } from "../../../../utils/permissions";
import {
ADMIN_GET_BUCKET_QUOTA,
ADMIN_SET_BUCKET_QUOTA,
S3_GET_BUCKET_ENCRYPTION_CONFIGURATION,
S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION,
S3_GET_BUCKET_POLICY,
S3_GET_BUCKET_VERSIONING,
S3_GET_OBJECT_RETENTION,
S3_GET_REPLICATION_CONFIGURATION,
S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION,
S3_PUT_BUCKET_POLICY,
S3_PUT_BUCKET_VERSIONING,
S3_PUT_OBJECT_RETENTION,
} from "../../../../types";
interface IBucketSummaryProps {
classes: any;
@@ -153,6 +168,62 @@ const BucketSummary = ({
accessPolicy = bucketInfo.access;
}
const displayGetBucketPolicy = displayComponent(bucketInfo?.allowedActions, [
S3_GET_BUCKET_POLICY,
]);
const displayPutBucketPolicy = displayComponent(bucketInfo?.allowedActions, [
S3_PUT_BUCKET_POLICY,
]);
const displayGetReplicationConfiguration = displayComponent(
bucketInfo?.allowedActions,
[S3_GET_REPLICATION_CONFIGURATION]
);
const displayGetBucketObjectLockConfiguration = displayComponent(
bucketInfo?.allowedActions,
[S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION]
);
const displayGetBucketEncryptionConfiguration = displayComponent(
bucketInfo?.allowedActions,
[S3_GET_BUCKET_ENCRYPTION_CONFIGURATION]
);
const displayPutBucketEncryptionConfiguration = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION]
);
const displayGetBucketVersioning = displayComponent(
bucketInfo?.allowedActions,
[S3_GET_BUCKET_VERSIONING]
);
const displayPutBucketVersioning = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_BUCKET_VERSIONING]
);
const displayGetBucketQuota = displayComponent(bucketInfo?.allowedActions, [
ADMIN_GET_BUCKET_QUOTA,
]);
const displaySetBucketQuota = displayComponent(bucketInfo?.allowedActions, [
ADMIN_SET_BUCKET_QUOTA,
]);
const displayGetObjectRetention = displayComponent(
bucketInfo?.allowedActions,
[S3_GET_OBJECT_RETENTION]
);
const displayPutObjectRetention = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_OBJECT_RETENTION]
);
useEffect(() => {
if (loadingBucket) {
setBucketLoading(true);
@@ -163,25 +234,31 @@ const BucketSummary = ({
useEffect(() => {
if (loadingEncryption) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/encryption/info`)
.then((res: BucketEncryptionInfo) => {
if (res.algorithm) {
setEncryptionEnabled(true);
setEncryptionCfg(res);
}
setLoadingEncryption(false);
})
.catch((err: ErrorResponseHandler) => {
if (
err.errorMessage ===
"The server side encryption configuration was not found"
) {
setEncryptionEnabled(false);
setEncryptionCfg(null);
}
setLoadingEncryption(false);
});
if (displayGetBucketEncryptionConfiguration) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/encryption/info`)
.then((res: BucketEncryptionInfo) => {
if (res.algorithm) {
setEncryptionEnabled(true);
setEncryptionCfg(res);
}
setLoadingEncryption(false);
})
.catch((err: ErrorResponseHandler) => {
if (
err.errorMessage ===
"The server side encryption configuration was not found"
) {
setEncryptionEnabled(false);
setEncryptionCfg(null);
}
setLoadingEncryption(false);
});
} else {
setEncryptionEnabled(false);
setEncryptionCfg(null);
setLoadingEncryption(false);
}
}
}, [loadingEncryption, bucketName]);
@@ -202,22 +279,27 @@ const BucketSummary = ({
useEffect(() => {
if (loadingQuota && distributedSetup) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/quota`)
.then((res: BucketQuota) => {
setQuota(res);
if (res.quota) {
setQuotaEnabled(true);
} else {
if (displayGetBucketQuota) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/quota`)
.then((res: BucketQuota) => {
setQuota(res);
if (res.quota) {
setQuotaEnabled(true);
} else {
setQuotaEnabled(false);
}
setLoadingQuota(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setQuotaEnabled(false);
}
setLoadingQuota(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setQuotaEnabled(false);
setLoadingQuota(false);
});
setLoadingQuota(false);
});
} else {
setQuotaEnabled(false);
setLoadingQuota(false);
}
}
}, [
loadingQuota,
@@ -229,16 +311,20 @@ const BucketSummary = ({
useEffect(() => {
if (loadingVersioning && distributedSetup) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
.then((res: BucketObjectLocking) => {
setHasObjectLocking(res.object_locking_enabled);
setLoadingLocking(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingLocking(false);
});
if (displayGetBucketObjectLockConfiguration) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/object-locking`)
.then((res: BucketObjectLocking) => {
setHasObjectLocking(res.object_locking_enabled);
setLoadingLocking(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingLocking(false);
});
} else {
setLoadingLocking(false);
}
}
}, [
loadingObjectLocking,
@@ -405,64 +491,76 @@ const BucketSummary = ({
<Grid item xs={8}>
<table width={"100%"}>
<tbody>
<tr>
<td className={classes.titleCol}>Access Policy:</td>
<td className={classes.capitalizeFirst}>
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setAccessPolicyScreenOpen(true);
}}
>
{bucketLoading ? (
{displayGetBucketPolicy && (
<tr>
<td className={classes.titleCol}>Access Policy:</td>
<td className={classes.capitalizeFirst}>
<Button
disabled={!displayPutBucketPolicy}
color="primary"
className={classes.anchorButton}
onClick={() => {
setAccessPolicyScreenOpen(true);
}}
>
{bucketLoading ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</Button>
</td>
</tr>
)}
{distributedSetup && (
<Fragment>
{displayGetReplicationConfiguration && (
<tr>
<td className={classes.titleCol}>Replication:</td>
<td className={classes.doubleElement}>
<span>
{replicationRules ? "Enabled" : "Disabled"}
</span>
</td>
</tr>
)}
{displayGetBucketObjectLockConfiguration && (
<tr>
<td className={classes.titleCol}>Object Locking:</td>
<td>{!hasObjectLocking ? "Disabled" : "Enabled"}</td>
</tr>
)}
</Fragment>
)}
{displayGetBucketEncryptionConfiguration && (
<tr>
<td className={classes.titleCol}>Encryption:</td>
<td>
{loadingEncryption ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
<Button
disabled={!displayPutBucketEncryptionConfiguration}
color="primary"
className={classes.anchorButton}
onClick={() => {
setEnableEncryptionScreenOpen(true);
}}
>
{encryptionEnabled ? "Enabled" : "Disabled"}
</Button>
)}
</Button>
</td>
</tr>
{distributedSetup && (
<Fragment>
<tr>
<td className={classes.titleCol}>Replication:</td>
<td className={classes.doubleElement}>
<span>{replicationRules ? "Enabled" : "Disabled"}</span>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Object Locking:</td>
<td>{!hasObjectLocking ? "Disabled" : "Enabled"}</td>
</tr>
</Fragment>
</td>
</tr>
)}
<tr>
<td className={classes.titleCol}>Encryption:</td>
<td>
{loadingEncryption ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Button
color="primary"
className={classes.anchorButton}
onClick={() => {
setEnableEncryptionScreenOpen(true);
}}
>
{encryptionEnabled ? "Enabled" : "Disabled"}
</Button>
)}
</td>
</tr>
</tbody>
</table>
</Grid>
@@ -485,7 +583,7 @@ const BucketSummary = ({
</Paper>
<br />
<br />
{distributedSetup && (
{distributedSetup && displayGetBucketVersioning && (
<Fragment>
<Paper className={classes.paperContainer} elevation={1}>
<Grid container>
@@ -506,6 +604,7 @@ const BucketSummary = ({
) : (
<Fragment>
<Button
disabled={!displayPutBucketVersioning}
color="primary"
className={classes.anchorButton}
onClick={setBucketVersioning}
@@ -515,26 +614,31 @@ const BucketSummary = ({
</Fragment>
)}
</td>
<td className={classes.titleCol}>Quota:</td>
<td>
{loadingQuota ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Fragment>
<Button
color="primary"
className={classes.anchorButton}
onClick={setBucketQuota}
>
{quotaEnabled ? "Enabled" : "Disabled"}
</Button>
</Fragment>
)}
</td>
{displayGetBucketQuota && (
<Fragment>
<td className={classes.titleCol}>Quota:</td>
<td>
{loadingQuota ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
<Fragment>
<Button
disabled={!displaySetBucketQuota}
color="primary"
className={classes.anchorButton}
onClick={setBucketQuota}
>
{quotaEnabled ? "Enabled" : "Disabled"}
</Button>
</Fragment>
)}
</td>
</Fragment>
)}
</tr>
</tbody>
</table>
@@ -563,7 +667,7 @@ const BucketSummary = ({
</Fragment>
)}
{hasObjectLocking && (
{hasObjectLocking && displayGetObjectRetention && (
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={12}>
@@ -583,6 +687,7 @@ const BucketSummary = ({
) : (
<Fragment>
<Button
disabled={!displayPutObjectRetention}
color="primary"
className={classes.anchorButton}
onClick={() => {

View File

@@ -243,20 +243,22 @@ const BucketListItem = ({
<div style={{ marginBottom: 10 }} />
</Box>
</Grid>
<Grid item xs={6} sm={12} md={12}>
<Link
to={`/buckets/${bucket.name}/admin`}
style={{ textDecoration: "none" }}
>
<Button
variant={"outlined"}
endIcon={<SettingsIcon />}
className={classes.manageButton}
{bucket.manage && (
<Grid item xs={6} sm={12} md={12}>
<Link
to={`/buckets/${bucket.name}/admin`}
style={{ textDecoration: "none" }}
>
Manage
</Button>
</Link>
</Grid>
<Button
variant={"outlined"}
endIcon={<SettingsIcon />}
className={classes.manageButton}
>
Manage
</Button>
</Link>
</Grid>
)}
</Grid>
</Grid>
</Grid>

View File

@@ -42,6 +42,7 @@ import BucketListItem from "./BucketListItem";
import BulkReplicationModal from "./BulkReplicationModal";
import SearchIcon from "../../../../icons/SearchIcon";
import HelpBox from "../../../../common/HelpBox";
import { ISessionResponse } from "../../types";
const styles = (theme: Theme) =>
createStyles({
@@ -135,6 +136,7 @@ interface IListBucketsProps {
addBucketModalOpen: boolean;
addBucketReset: typeof addBucketReset;
setErrorSnackMessage: typeof setErrorSnackMessage;
session: ISessionResponse;
}
const ListBuckets = ({
@@ -144,6 +146,7 @@ const ListBuckets = ({
addBucketModalOpen,
addBucketReset,
setErrorSnackMessage,
session,
}: IListBucketsProps) => {
const [records, setRecords] = useState<Bucket[]>([]);
const [loading, setLoading] = useState<boolean>(true);
@@ -327,7 +330,7 @@ const ListBuckets = ({
}}
inputProps={{
disableUnderline: true,
endAdornment: (
endadornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
@@ -345,7 +348,7 @@ const ListBuckets = ({
}}
inputProps={{
disableUnderline: true,
endAdornment: (
endadornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
@@ -462,6 +465,7 @@ const ListBuckets = ({
const mapState = (state: AppState) => ({
addBucketModalOpen: state.buckets.open,
session: state.console.session,
});
const connector = connect(mapState, {

View File

@@ -63,7 +63,7 @@ import {
setLoadingProgress,
setSnackBarMessage,
} from "../../../../../../actions";
import { BucketVersioning } from "../../../types";
import { BucketInfo, BucketVersioning } from "../../../types";
import { ErrorResponseHandler } from "../../../../../../common/types";
import RewindEnable from "./RewindEnable";
import DeleteIcon from "@mui/icons-material/Delete";
@@ -97,6 +97,17 @@ import {
FileZipIcon,
} from "../../../../../../icons";
import ShareFile from "../ObjectDetails/ShareFile";
import { displayComponent } from "../../../../../../utils/permissions";
import {
S3_DELETE_BUCKET,
S3_DELETE_OBJECT,
S3_FORCE_DELETE_BUCKET,
S3_GET_OBJECT,
S3_LIST_BUCKET,
S3_PUT_OBJECT,
} from "../../../../../../types";
import { setBucketDetailsLoad, setBucketInfo } from "../../../actions";
import { AppState } from "../../../../../../store";
const commonIcon = {
backgroundRepeat: "no-repeat",
@@ -203,6 +214,10 @@ interface IListObjectsProps {
setErrorSnackMessage: typeof setErrorSnackMessage;
resetRewind: typeof resetRewind;
setFileModeEnabled: typeof setFileModeEnabled;
loadingBucket: boolean;
setBucketInfo: typeof setBucketInfo;
bucketInfo: BucketInfo | null;
setBucketDetailsLoad: typeof setBucketDetailsLoad;
}
function useInterval(callback: any, delay: number) {
@@ -243,6 +258,10 @@ const ListObjects = ({
setErrorSnackMessage,
resetRewind,
setFileModeEnabled,
setBucketDetailsLoad,
loadingBucket,
setBucketInfo,
bucketInfo,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [loading, setLoading] = useState<boolean>(true);
@@ -269,12 +288,29 @@ const ListObjects = ({
"ASC" | "DESC" | undefined
>("ASC");
const [currentSortField, setCurrentSortField] = useState<string>("name");
const [iniLoad, setIniLoad] = useState<boolean>(false);
const internalPaths = get(match.params, "subpaths", "");
const bucketName = match.params["bucketName"];
const fileUpload = useRef<HTMLInputElement>(null);
const displayPutObject = displayComponent(bucketInfo?.allowedActions, [
S3_PUT_OBJECT,
]);
const displayGetObject = displayComponent(bucketInfo?.allowedActions, [
S3_GET_OBJECT,
]);
const displayDeleteObject = displayComponent(bucketInfo?.allowedActions, [
S3_DELETE_OBJECT,
]);
const displayListObjects = displayComponent(bucketInfo?.allowedActions, [
S3_LIST_BUCKET,
]);
const updateMessage = () => {
let timeDelta = Date.now() - loadingStartTime;
@@ -296,6 +332,13 @@ const ListObjects = ({
}
};
useEffect(() => {
if (!iniLoad) {
setBucketDetailsLoad(true);
setIniLoad(true);
}
}, [iniLoad, setBucketDetailsLoad, setIniLoad]);
useInterval(() => {
// Your custom logic here
if (loading) {
@@ -304,17 +347,25 @@ const ListObjects = ({
}, 1000);
useEffect(() => {
if (loadingVersioning) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
setIsVersioned(res.is_versioned);
setLoadingVersioning(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingVersioning(false);
});
if (
loadingVersioning &&
bucketInfo?.allowedActions &&
bucketInfo?.name == bucketName
) {
if (displayListObjects) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}/versioning`)
.then((res: BucketVersioning) => {
setIsVersioned(res.is_versioned);
setLoadingVersioning(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setLoadingVersioning(false);
});
} else {
setLoadingVersioning(false);
}
}
}, [bucketName, loadingVersioning, setErrorSnackMessage]);
@@ -373,121 +424,130 @@ const ListObjects = ({
}, [internalPaths]);
useEffect(() => {
if (loading) {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeFileName(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
if (
loading &&
bucketInfo?.allowedActions &&
bucketInfo?.name == bucketName
) {
if (displayListObjects) {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeFileName(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
let currentTimestamp = Date.now();
setLoadingStartTime(currentTimestamp);
setLoadingMessage(defLoading);
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects${
pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : ``
}`
)
.then((res: BucketObjectsList) => {
const records: BucketObject[] = res.objects || [];
const folders: BucketObject[] = [];
const files: BucketObject[] = [];
let currentTimestamp = Date.now();
setLoadingStartTime(currentTimestamp);
setLoadingMessage(defLoading);
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects${
pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : ``
}`
)
.then((res: BucketObjectsList) => {
const records: BucketObject[] = res.objects || [];
const folders: BucketObject[] = [];
const files: BucketObject[] = [];
records.forEach((record) => {
// this is a folder
if (record.name.endsWith("/")) {
folders.push(record);
} else {
// this is a file
files.push(record);
}
});
const recordsInElement = [...folders, ...files];
setRecords(recordsInElement);
// In case no objects were retrieved, We check if item is a file
if (!res.objects && pathPrefix !== "") {
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeFileName(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
records.forEach((record) => {
// this is a folder
if (record.name.endsWith("/")) {
folders.push(record);
} else {
// this is a file
files.push(record);
}
});
const recordsInElement = [...folders, ...files];
setRecords(recordsInElement);
// In case no objects were retrieved, We check if item is a file
if (!res.objects && pathPrefix !== "") {
if (rewindEnabled) {
const rewindParsed = rewindDate.toISOString();
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${
pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : ``
}`
)
.then((res: RewindObjectList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects === null) {
setFileModeEnabled(true);
setLoadingRewind(false);
setLoading(false);
} else {
// It is a folder, we remove loader
setLoadingRewind(false);
setLoading(false);
setFileModeEnabled(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoadingRewind(false);
setLoading(false);
setErrorSnackMessage(err);
});
} else {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects${
internalPaths ? `?prefix=${internalPaths}` : ``
}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (!res.objects) {
// It is a folder, we remove loader
setFileModeEnabled(false);
setLoading(false);
} else {
// This is an empty folder.
if (
res.objects.length === 1 &&
res.objects[0].name.endsWith("/")
) {
setFileModeEnabled(false);
} else {
let pathPrefix = "";
if (internalPaths) {
const decodedPath = decodeFileName(internalPaths);
pathPrefix = decodedPath.endsWith("/")
? decodedPath
: decodedPath + "/";
}
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/rewind/${rewindParsed}${
pathPrefix ? `?prefix=${encodeFileName(pathPrefix)}` : ``
}`
)
.then((res: RewindObjectList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects === null) {
setFileModeEnabled(true);
setLoadingRewind(false);
setLoading(false);
} else {
// It is a folder, we remove loader
setLoadingRewind(false);
setLoading(false);
setFileModeEnabled(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoadingRewind(false);
setLoading(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
setErrorSnackMessage(err);
});
} else {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects${
internalPaths ? `?prefix=${internalPaths}` : ``
}`
)
.then((res: BucketObjectsList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (!res.objects) {
// It is a folder, we remove loader
setFileModeEnabled(false);
setLoading(false);
} else {
// This is an empty folder.
if (
res.objects.length === 1 &&
res.objects[0].name.endsWith("/")
) {
setFileModeEnabled(false);
} else {
setFileModeEnabled(true);
}
setLoading(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
}
} else {
setFileModeEnabled(false);
setLoading(false);
}
} else {
setFileModeEnabled(false);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
}
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
setErrorSnackMessage(err);
});
} else {
setLoadingRewind(false);
setLoading(false);
}
}
}, [
loading,
@@ -498,6 +558,29 @@ const ListObjects = ({
rewindDate,
internalPaths,
setFileModeEnabled,
bucketInfo,
]);
// bucket info
useEffect(() => {
if (loadingBucket) {
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
setBucketDetailsLoad(false);
setBucketInfo(res);
})
.catch((err: ErrorResponseHandler) => {
setBucketDetailsLoad(false);
setErrorSnackMessage(err);
});
}
}, [
bucketName,
loadingBucket,
setBucketDetailsLoad,
setBucketInfo,
setErrorSnackMessage,
]);
const closeDeleteModalAndRefresh = (refresh: boolean) => {
@@ -606,9 +689,9 @@ const ListObjects = ({
return niceBytes(String(object.size));
};
const confirmDeleteObject = (object: string) => {
const confirmDeleteObject = (object: BucketObject) => {
setDeleteOpen(true);
setSelectedObject(object);
setSelectedObject(object.name);
};
const displayDeleteFlag = (state: boolean) => {
@@ -688,15 +771,17 @@ const ListObjects = ({
},
sendOnlyId: false,
},
{
];
if (displayDeleteObject) {
tableActions.push({
type: "delete",
onClick: confirmDeleteObject,
sendOnlyId: true,
disableButtonFunction: () => {
return rewindEnabled;
},
},
];
});
}
const displayName = (element: string) => {
let elementString = element;
@@ -1000,46 +1085,50 @@ const ListObjects = ({
}
actions={
<Fragment>
<Tooltip title={"Choose or create a new path"}>
<IconButton
color="primary"
aria-label="Add a new folder"
component="span"
onClick={() => {
setCreateFolderOpen(true);
}}
disabled={rewindEnabled}
size="large"
>
<AddFolderIcon />
</IconButton>
</Tooltip>
{displayPutObject && (
<Fragment>
<Tooltip title={"Choose or create a new path"}>
<IconButton
color="primary"
aria-label="Add a new folder"
component="span"
onClick={() => {
setCreateFolderOpen(true);
}}
disabled={rewindEnabled}
size="large"
>
<AddFolderIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Upload file"}>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
if (fileUpload && fileUpload.current) {
fileUpload.current.click();
}
}}
disabled={rewindEnabled}
size="large"
>
<UploadIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Upload file"}>
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
if (fileUpload && fileUpload.current) {
fileUpload.current.click();
}
}}
disabled={rewindEnabled}
size="large"
>
<UploadIcon />
</IconButton>
</Tooltip>
<input
type="file"
multiple={true}
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
ref={fileUpload}
/>
</Fragment>
)}
<input
type="file"
multiple={true}
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
ref={fileUpload}
/>
<Tooltip title={"Rewind"}>
<Badge
badgeContent=" "
@@ -1081,42 +1170,47 @@ const ListObjects = ({
/>
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterObjects(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
variant="standard"
/>
{displayListObjects && (
<TextField
placeholder="Search Objects"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterObjects(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
variant="standard"
/>
)}
<Button
variant="contained"
color="primary"
startIcon={<DeleteIcon />}
onClick={() => {
setDeleteMultipleOpen(true);
}}
disabled={selectedObjects.length === 0}
>
Delete Selected
</Button>
{displayDeleteObject && (
<Button
variant="contained"
color="primary"
startIcon={<DeleteIcon />}
onClick={() => {
setDeleteMultipleOpen(true);
}}
disabled={selectedObjects.length === 0}
>
Delete Selected
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
disabled={!displayListObjects}
itemActions={tableActions}
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={rewindEnabled ? loadingRewind : loading}
@@ -1142,12 +1236,14 @@ const ListObjects = ({
);
};
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
const mapStateToProps = ({ objectBrowser, buckets }: AppState) => ({
routesList: get(objectBrowser, "routesList", []),
downloadingFiles: get(objectBrowser, "downloadingFiles", []),
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
loadingBucket: buckets.bucketDetails.loadingBucket,
bucketInfo: buckets.bucketDetails.bucketInfo,
});
const mapDispatchToProps = {
@@ -1156,6 +1252,8 @@ const mapDispatchToProps = {
setErrorSnackMessage,
setFileModeEnabled,
resetRewind,
setBucketDetailsLoad,
setBucketInfo,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -80,6 +80,19 @@ import SearchIcon from "../../../../../../icons/SearchIcon";
import ObjectBrowserIcon from "../../../../../../icons/ObjectBrowserIcon";
import PreviewFileContent from "../Preview/PreviewFileContent";
import { decodeFileName, encodeFileName } from "../../../../../../common/utils";
import { BucketInfo } from "../../../types";
import { displayComponent } from "../../../../../../utils/permissions";
import {
S3_DELETE_OBJECT,
S3_DELETE_OBJECT_TAGGING,
S3_GET_OBJECT_LEGAL_HOLD,
S3_GET_OBJECT_RETENTION,
S3_GET_OBJECT_TAGGING,
S3_GET_REPLICATION_CONFIGURATION,
S3_PUT_OBJECT_LEGAL_HOLD,
S3_PUT_OBJECT_RETENTION,
S3_PUT_OBJECT_TAGGING,
} from "../../../../../../types";
const styles = (theme: Theme) =>
createStyles({
@@ -257,6 +270,8 @@ const ObjectDetails = ({
const [metadataLoad, setMetadataLoad] = useState<boolean>(true);
const [metadata, setMetadata] = useState<any>({});
const [selectedTab, setSelectedTab] = useState<number>(0);
const [loadingBucket, setLoadingBucket] = useState<boolean>(false);
const [bucketInfo, setBucketInfo] = useState<any>(null);
const internalPaths = get(match.params, "subpaths", "");
const internalPathsDecoded = decodeFileName(internalPaths) || "";
@@ -270,6 +285,64 @@ const ObjectDetails = ({
objectNameArray = actualInfo.name.split("/");
}
const displayObjectLegalHold = displayComponent(bucketInfo?.allowedActions, [
S3_GET_OBJECT_LEGAL_HOLD,
]);
const displayEditObjectLegalHold = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_OBJECT_LEGAL_HOLD],
true
);
const displayObjectRetention = displayComponent(bucketInfo?.allowedActions, [
S3_GET_OBJECT_RETENTION,
]);
const displayEditObjectRetention = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_OBJECT_RETENTION],
true
);
const displayObjectTag = displayComponent(bucketInfo?.allowedActions, [
S3_GET_OBJECT_TAGGING,
]);
const displayEditObjectTagging = displayComponent(
bucketInfo?.allowedActions,
[S3_PUT_OBJECT_TAGGING],
true
);
const displayRemoveObjectTagging = displayComponent(
bucketInfo?.allowedActions,
[S3_DELETE_OBJECT_TAGGING],
true
);
const displayDeleteObject = displayComponent(
bucketInfo?.allowedActions,
[S3_DELETE_OBJECT],
true
);
// bucket info
useEffect(() => {
if (!loadingBucket) {
setLoadingBucket(true);
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
setBucketInfo(res);
})
.catch((err: ErrorResponseHandler) => {
setLoadingBucket(false);
setErrorSnackMessage(err);
});
}
}, [bucketName, loadingBucket, setBucketInfo, setErrorSnackMessage]);
useEffect(() => {
if (loadObjectData) {
api
@@ -504,7 +577,6 @@ const ObjectDetails = ({
actualInfo={actualInfo}
/>
)}
<Grid container>
{!actualInfo && (
<Grid item xs={12}>
@@ -576,19 +648,21 @@ const ObjectDetails = ({
</Tooltip>
)}
<Tooltip title="Delete Object">
<IconButton
color="primary"
aria-label="delete"
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
size="large"
>
<DeleteIcon />
</IconButton>
</Tooltip>
{displayDeleteObject && (
<Tooltip title="Delete Object">
<IconButton
color="primary"
aria-label="delete"
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
size="large"
>
<DeleteIcon />
</IconButton>
</Tooltip>
)}
</Fragment>
}
/>
@@ -635,109 +709,138 @@ const ObjectDetails = ({
<h1 className={classes.sectionTitle}>Details</h1>
</div>
<br />
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={10}>
<table width={"100%"}>
<tbody>
<tr>
<td className={classes.titleCol}>Legal Hold:</td>
<td className={classes.capitalizeFirst}>
{actualInfo.version_id &&
actualInfo.version_id !== "null" ? (
<Fragment>
{actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"}
<IconButton
color="primary"
aria-label="legal-hold"
size="small"
className={classes.propertiesIcon}
onClick={() => {
setLegalholdOpen(true);
}}
>
<EditIcon />
</IconButton>
</Fragment>
) : (
"Disabled"
{(displayObjectLegalHold ||
displayObjectRetention ||
displayObjectTag) && (
<Fragment>
<Paper className={classes.paperContainer}>
<Grid container>
<Grid item xs={10}>
<table width={"100%"}>
<tbody>
{displayObjectLegalHold && (
<tr>
<td className={classes.titleCol}>
Legal Hold:
</td>
<td className={classes.capitalizeFirst}>
{actualInfo.version_id &&
actualInfo.version_id !== "null" ? (
<Fragment>
{actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"}
{displayEditObjectLegalHold && (
<IconButton
color="primary"
aria-label="legal-hold"
size="small"
className={classes.propertiesIcon}
onClick={() => {
setLegalholdOpen(true);
}}
>
<EditIcon />
</IconButton>
)}
</Fragment>
) : (
"Disabled"
)}
</td>
</tr>
)}
</td>
</tr>
<tr>
<td className={classes.titleCol}>Retention:</td>
<td className={classes.capitalizeFirst}>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"}
<IconButton
color="primary"
aria-label="retention"
size="small"
className={classes.propertiesIcon}
onClick={() => {
openRetentionModal();
}}
>
<EditIcon />
</IconButton>
</td>
</tr>
<tr>
<td className={classes.titleCol}>Tags:</td>
<td>
{tagKeys &&
tagKeys.map((tagKey, index) => {
const tag = get(
actualInfo,
`tags.${tagKey}`,
""
);
if (tag !== "") {
return (
<Chip
key={`chip-${index}`}
className={classes.tag}
size="small"
label={`${tagKey} : ${tag}`}
{displayObjectRetention && (
<tr>
<td className={classes.titleCol}>
Retention:
</td>
<td className={classes.capitalizeFirst}>
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"}
{displayEditObjectRetention && (
<IconButton
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
deleteTag(tagKey, tag);
aria-label="retention"
size="small"
className={classes.propertiesIcon}
onClick={() => {
openRetentionModal();
}}
>
<EditIcon />
</IconButton>
)}
</td>
</tr>
)}
{displayObjectTag && (
<tr>
<td className={classes.titleCol}>Tags:</td>
<td>
{tagKeys &&
tagKeys.map((tagKey, index) => {
const tag = get(
actualInfo,
`tags.${tagKey}`,
""
);
if (tag !== "") {
return displayRemoveObjectTagging ? (
<Chip
key={`chip-${index}`}
className={classes.tag}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
deleteIcon={<CloseIcon />}
onDelete={() => {
deleteTag(tagKey, tag);
}}
/>
) : (
<Chip
key={`chip-${index}`}
className={classes.tag}
size="small"
label={`${tagKey} : ${tag}`}
color="primary"
/>
);
}
return null;
})}
{displayEditObjectTagging && (
<Chip
className={classes.tag}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
onClick={() => {
setTagModalOpen(true);
}}
/>
);
}
return null;
})}
<Chip
className={classes.tag}
icon={<AddIcon />}
clickable
size="small"
label="Add tag"
color="primary"
variant="outlined"
onClick={() => {
setTagModalOpen(true);
}}
/>
</td>
</tr>
</tbody>
</table>
</Grid>
</Grid>
</Paper>
<br />
<br />
)}
</td>
</tr>
)}
</tbody>
</table>
</Grid>
</Grid>
</Paper>
<br />
</Fragment>
)}
<Paper className={classes.paperContainer}>
<Grid item xs={12}>
<Grid item xs={12}>
<h2>Object Metadata</h2>
<hr className={classes.hr}></hr>
<hr className={classes.hr} />
</Grid>
<Grid item xs={12}>

View File

@@ -25,6 +25,8 @@ export interface Bucket {
size?: number;
objects?: number;
rw_access?: RwAccess;
allowedActions?: string[];
manage: boolean;
}
export interface BucketEncryptionInfo {
@@ -35,6 +37,8 @@ export interface BucketEncryptionInfo {
export interface BucketInfo {
name: string;
access: string;
allowedActions?: string[];
manage?: boolean;
}
export interface BucketList {

View File

@@ -39,7 +39,7 @@ const styles = (theme: Theme) =>
...actionsTray,
...widgetContainerCommon,
syncButton: {
"&.MuiButton-root .MuiButton-iconSizeMedium > *:first-child": {
"&.MuiButton-root .MuiButton-iconSizeMedium > *:first-of-type": {
fontSize: 18,
},
},

View File

@@ -101,6 +101,7 @@ interface TableWrapperProps {
autoScrollToBottom?: boolean;
infiniteScrollConfig?: IInfiniteScrollConfig;
sortConfig?: ISortConfig;
disabled?: boolean;
}
const borderColor = "#9c9c9c80";
@@ -142,6 +143,10 @@ const styles = () =>
backgroundColor: "transparent",
border: 0,
},
disabled: {
backgroundColor: "#fbfafa",
color: "#cccccc",
},
defaultPaperHeight: {
height: "calc(100vh - 205px)",
},
@@ -514,6 +519,7 @@ const TableWrapper = ({
infiniteScrollConfig,
sortConfig,
autoScrollToBottom = false,
disabled = false,
}: TableWrapperProps) => {
const [columnSelectorOpen, setColumnSelectorOpen] = useState<boolean>(false);
const [anchorEl, setAnchorEl] = React.useState<any>(null);
@@ -597,9 +603,9 @@ const TableWrapper = ({
return (
<Grid item xs={12}>
<Paper
className={`${classes.paper} ${
noBackground ? classes.noBackground : ""
} ${
className={`${classes.paper} ${noBackground ? classes.noBackground : ""}
${disabled ? classes.disabled : ""}
${
customPaperHeight !== ""
? customPaperHeight
: classes.defaultPaperHeight

View File

@@ -53,7 +53,7 @@ const styles = (theme: Theme) =>
...actionsTray,
...widgetContainerCommon,
syncButton: {
"&.MuiButton-root .MuiButton-iconSizeMedium > *:first-child": {
"&.MuiButton-root .MuiButton-iconSizeMedium > *:first-of-type": {
fontSize: 18,
},
},

View File

@@ -28,6 +28,10 @@ const initialState: ConsoleState = {
pages: [],
features: [],
distributedMode: false,
policy: {
version: "",
statement: [],
},
},
};

View File

@@ -14,10 +14,22 @@
// 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/>.
export interface ISessionPolicyStatement {
action: string[];
condition: any;
effect: string;
resource: string[];
}
export interface ISessionPolicy {
version: string;
statement: ISessionPolicyStatement[];
}
export interface ISessionResponse {
status: string;
pages: string[];
features: string[];
operator: boolean;
distributedMode: boolean;
policy: ISessionPolicy,
}

View File

@@ -85,7 +85,7 @@ const theme = createTheme({
fontWeight: 600,
color: "#767676",
},
"& .MuiButton-iconSizeMedium > *:first-child": {
"& .MuiButton-iconSizeMedium > *:first-of-type": {
fontSize: 12,
},
},

View File

@@ -122,3 +122,55 @@ export type SystemActionTypes =
| SetModalSnackMessage
| SetModalErrorMessage
| SetDistributedSetup;
// S3 Actions
export const S3_LIST_BUCKET = "s3:ListBucket";
export const S3_GET_BUCKET_POLICY = "s3:GetBucketPolicy";
export const S3_PUT_BUCKET_POLICY = "s3:PutBucketPolicy";
export const S3_GET_OBJECT = "s3:GetObject";
export const S3_PUT_OBJECT = "s3:PutObject";
export const S3_GET_OBJECT_LEGAL_HOLD = "s3:GetObjectLegalHold";
export const S3_PUT_OBJECT_LEGAL_HOLD = "s3:PutObjectLegalHold";
export const S3_DELETE_OBJECT = "s3:DeleteObject";
export const S3_GET_BUCKET_VERSIONING = "s3:GetBucketVersioning";
export const S3_PUT_BUCKET_VERSIONING = "s3:PutBucketVersioning";
export const S3_GET_OBJECT_RETENTION = "s3:GetObjectRetention";
export const S3_PUT_OBJECT_RETENTION = "s3:PutObjectRetention";
export const S3_GET_OBJECT_TAGGING = "s3:GetObjectTagging";
export const S3_PUT_OBJECT_TAGGING = "s3:PutObjectTagging";
export const S3_DELETE_OBJECT_TAGGING = "s3:DeleteObjectTagging";
export const S3_GET_BUCKET_ENCRYPTION_CONFIGURATION =
"s3:GetEncryptionConfiguration";
export const S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION =
"s3:PutEncryptionConfiguration";
export const S3_DELETE_BUCKET = "s3:DeleteBucket";
export const S3_FORCE_DELETE_BUCKET = "s3:ForceDeleteBucket";
export const S3_GET_BUCKET_NOTIFICATIONS = "s3:GetBucketNotification";
export const S3_LISTEN_BUCKET_NOTIFICATIONS = "s3:ListenBucketNotification";
export const S3_PUT_BUCKET_NOTIFICATIONS = "s3:PutBucketNotification";
export const S3_GET_REPLICATION_CONFIGURATION =
"s3:GetReplicationConfiguration";
export const S3_PUT_REPLICATION_CONFIGURATION =
"s3:PutReplicationConfiguration";
export const S3_GET_LIFECYCLE_CONFIGURATION = "s3:GetLifecycleConfiguration";
export const S3_PUT_LIFECYCLE_CONFIGURATION = "s3:PutLifecycleConfiguration";
export const S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION =
"s3:GetBucketObjectLockConfiguration";
export const S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION =
"s3:PutBucketObjectLockConfiguration";
// Admin Actions
export const ADMIN_GET_POLICY = "admin:GetPolicy";
export const ADMIN_LIST_USERS = "admin:ListUsers";
export const ADMIN_LIST_USER_POLICIES = "admin:ListUserPolicies";
export const ADMIN_SERVER_INFO = "admin:ServerInfo";
export const ADMIN_GET_BUCKET_QUOTA = "admin:GetBucketQuota";
export const ADMIN_SET_BUCKET_QUOTA = "admin:SetBucketQuota";
export const ADMIN_LIST_TIERS = "admin:ListTier";
export const ADMIN_LIST_GROUPS = "admin:ListGroups";
export const S3_ALL_ACTIONS = "s3:*";
export const ADMIN_ALL_ACTIONS = "admin:*";

View File

@@ -0,0 +1,49 @@
// 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 {
ADMIN_ALL_ACTIONS,
S3_ALL_ACTIONS,
S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION,
S3_PUT_OBJECT_TAGGING,
} from "../types";
// displayComponent receives a list of user permissions to perform on a specific resource, then compares those permissions against
// a list of required permissions and return true or false depending of the level of required access (match all permissions,
// match some of the permissions)
export const displayComponent = (
userPermissionsOnBucket: string[] | null | undefined,
requiredPermissions: string[],
matchAll?: boolean
) => {
if (!userPermissionsOnBucket) {
return false;
}
const s3All = userPermissionsOnBucket.includes(S3_ALL_ACTIONS);
const AdminAll = userPermissionsOnBucket.includes(ADMIN_ALL_ACTIONS);
const permissions = requiredPermissions.filter(function (n) {
return (
userPermissionsOnBucket.indexOf(n) !== -1 ||
(n.indexOf("s3:") !== -1 && s3All) ||
(n.indexOf("admin:") !== -1 && AdminAll)
);
});
return matchAll
? permissions.length == requiredPermissions.length
: permissions.length > 0;
};

View File

@@ -2372,7 +2372,7 @@ func init() {
"tags": [
"UserAPI"
],
"summary": "Logout from Console.",
"summary": "Logout from Operator.",
"operationId": "Logout",
"responses": {
"200": {
@@ -3620,6 +3620,12 @@ func init() {
"access": {
"$ref": "#/definitions/bucketAccess"
},
"allowedActions": {
"type": "array",
"items": {
"type": "string"
}
},
"creation_date": {
"type": "string"
},
@@ -3662,6 +3668,9 @@ func init() {
}
}
},
"manage": {
"type": "boolean"
},
"name": {
"type": "string",
"minLength": 3
@@ -4146,6 +4155,46 @@ func init() {
"type": "string",
"pattern": "^[\\w+=,.@-]{1,64}$"
},
"iamPolicy": {
"type": "object",
"properties": {
"statement": {
"type": "array",
"items": {
"$ref": "#/definitions/iamPolicyStatement"
}
},
"version": {
"type": "string"
}
}
},
"iamPolicyStatement": {
"type": "object",
"properties": {
"action": {
"type": "array",
"items": {
"type": "string"
}
},
"condition": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"effect": {
"type": "string"
},
"resource": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"license": {
"type": "object",
"properties": {
@@ -5115,6 +5164,10 @@ func init() {
"type": "string"
}
},
"policy": {
"type": "object",
"$ref": "#/definitions/iamPolicy"
},
"status": {
"type": "string",
"enum": [
@@ -7980,7 +8033,7 @@ func init() {
"tags": [
"UserAPI"
],
"summary": "Logout from Console.",
"summary": "Logout from Operator.",
"operationId": "Logout",
"responses": {
"200": {
@@ -9348,6 +9401,12 @@ func init() {
"access": {
"$ref": "#/definitions/bucketAccess"
},
"allowedActions": {
"type": "array",
"items": {
"type": "string"
}
},
"creation_date": {
"type": "string"
},
@@ -9390,6 +9449,9 @@ func init() {
}
}
},
"manage": {
"type": "boolean"
},
"name": {
"type": "string",
"minLength": 3
@@ -9874,6 +9936,46 @@ func init() {
"type": "string",
"pattern": "^[\\w+=,.@-]{1,64}$"
},
"iamPolicy": {
"type": "object",
"properties": {
"statement": {
"type": "array",
"items": {
"$ref": "#/definitions/iamPolicyStatement"
}
},
"version": {
"type": "string"
}
}
},
"iamPolicyStatement": {
"type": "object",
"properties": {
"action": {
"type": "array",
"items": {
"type": "string"
}
},
"condition": {
"type": "object",
"additionalProperties": {
"type": "object"
}
},
"effect": {
"type": "string"
},
"resource": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"license": {
"type": "object",
"properties": {
@@ -10843,6 +10945,10 @@ func init() {
"type": "string"
}
},
"policy": {
"type": "object",
"$ref": "#/definitions/iamPolicy"
},
"status": {
"type": "string",
"enum": [

View File

@@ -50,7 +50,7 @@ func NewLogout(ctx *middleware.Context, handler LogoutHandler) *Logout {
/* Logout swagger:route POST /logout UserAPI logout
Logout from Console.
Logout from Operator.
*/
type Logout struct {

View File

@@ -24,6 +24,8 @@ import (
"strings"
"time"
"github.com/minio/console/pkg/acl"
"github.com/minio/mc/cmd"
"github.com/minio/mc/pkg/probe"
"github.com/minio/minio-go/v7"
@@ -283,9 +285,37 @@ func getAccountInfo(ctx context.Context, client MinioAdmin) ([]*models.Bucket, e
return []*models.Bucket{}, err
}
policyInfo, err := getAccountPolicy(ctx, client)
if err != nil {
return nil, err
}
bucketsPolicies := map[string]minioIAMPolicy.ActionSet{}
for _, statement := range policyInfo.Statements {
if statement.Effect == "Allow" {
for _, resource := range statement.Resources.ToSlice() {
resourceName := resource.String()
if actions, ok := bucketsPolicies[resourceName]; ok {
mergedActions := append(actions.ToSlice(), statement.Actions.ToSlice()...)
bucketsPolicies[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...)
} else {
bucketsPolicies[resourceName] = statement.Actions
}
}
}
}
var bucketInfos []*models.Bucket
for _, bucket := range info.Buckets {
var bucketAdminRole bool
bucketNameARN := fmt.Sprintf("arn:aws:s3:::%s/*", bucket.Name)
// match bucket name against policy that allows admin actions
if bucketPolicyActions, ok := bucketsPolicies[bucketNameARN]; ok {
bucketAdminRoleActions := bucketPolicyActions.Intersection(acl.BucketAdminRole)
bucketAdminRole = len(bucketAdminRoleActions) > 0
} else if bucketPolicyActions, ok := bucketsPolicies["arn:aws:s3:::*"]; ok {
bucketAdminRoleActions := bucketPolicyActions.Intersection(acl.BucketAdminRole)
bucketAdminRole = len(bucketAdminRoleActions) > 0
}
bucketElem := &models.Bucket{
CreationDate: bucket.Created.Format(time.RFC3339),
Details: &models.BucketDetails{
@@ -298,6 +328,7 @@ func getAccountInfo(ctx context.Context, client MinioAdmin) ([]*models.Bucket, e
Name: swag.String(bucket.Name),
Objects: int64(bucket.Objects),
Size: int64(bucket.Size),
Manage: bucketAdminRole,
}
if bucket.Details != nil {
@@ -470,12 +501,20 @@ func getBucketSetPolicyResponse(session *models.Principal, bucketName string, re
// defining the client to be used
minioClient := minioClient{client: mClient}
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
// set bucket access policy
if err := setBucketAccessPolicy(ctx, minioClient, bucketName, *req.Access); err != nil {
return nil, prepareError(err)
}
// get updated bucket details and return it
bucket, err := getBucketInfo(minioClient, bucketName)
bucket, err := getBucketInfo(ctx, minioClient, adminClient, bucketName)
if err != nil {
return nil, prepareError(err)
}
@@ -507,12 +546,53 @@ func getDeleteBucketResponse(session *models.Principal, params user_api.DeleteBu
return nil
}
func getPolicyActionSetForBucket(bucketName string, statement []minioIAMPolicy.Statement) minioIAMPolicy.ActionSet {
bucketActions := minioIAMPolicy.ActionSet{}
bucketNameARN := fmt.Sprintf("arn:aws:s3:::%s/*", bucketName)
for _, st := range statement {
if st.Effect == "Allow" {
if len(st.Resources.ToSlice()) == 0 {
mergedActions := append(bucketActions.ToSlice(), st.Actions.ToSlice()...)
bucketActions = minioIAMPolicy.NewActionSet(mergedActions...)
} else {
for _, resource := range st.Resources.ToSlice() {
resourceName := resource.String()
if resourceName == bucketNameARN || resourceName == "arn:aws:s3:::*" {
mergedActions := append(bucketActions.ToSlice(), st.Actions.ToSlice()...)
bucketActions = minioIAMPolicy.NewActionSet(mergedActions...)
}
}
}
}
}
return bucketActions
}
// getBucketInfo return bucket information including name, policy access, size and creation date
func getBucketInfo(client MinioClient, bucketName string) (*models.Bucket, error) {
policyStr, err := client.getBucketPolicy(context.Background(), bucketName)
func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdmin, bucketName string) (*models.Bucket, error) {
policyInfo, err := getAccountPolicy(ctx, adminClient)
if err != nil {
return nil, err
}
var bucketAdminRole bool
// Retrieve list of allowed bucketActionsArray on the bucket
bucketActions := getPolicyActionSetForBucket(bucketName, policyInfo.Statements)
// Check if one of these bucketActionsArray belongs to administrative bucketActionsArray
bucketAdminRoleActions := bucketActions.Intersection(acl.BucketAdminRole)
bucketAdminRole = len(bucketAdminRoleActions) > 0
var bucketActionsArray []string
for _, action := range bucketActions.ToSlice() {
bucketActionsArray = append(bucketActionsArray, string(action))
}
var bucketAccess models.BucketAccess
policyStr, err := client.getBucketPolicy(context.Background(), bucketName)
if err != nil {
// we can tolerate this error
LogError("error getting bucket policy: %v", err)
}
var policyAccess policy.BucketPolicy
if policyStr == "" {
policyAccess = policy.BucketPolicyNone
@@ -523,21 +603,27 @@ func getBucketInfo(client MinioClient, bucketName string) (*models.Bucket, error
}
policyAccess = policy.GetPolicy(p.Statements, bucketName, "")
}
bucketAccess := policyAccess2consoleAccess(policyAccess)
bucketAccess = policyAccess2consoleAccess(policyAccess)
if bucketAccess == models.BucketAccessPRIVATE && policyStr != "" {
bucketAccess = models.BucketAccessCUSTOM
}
bucket := &models.Bucket{
Name: &bucketName,
Access: &bucketAccess,
CreationDate: "", // to be implemented
Size: 0, // to be implemented
Name: &bucketName,
Access: &bucketAccess,
CreationDate: "", // to be implemented
Size: 0, // to be implemented
AllowedActions: bucketActionsArray,
Manage: bucketAdminRole,
}
return bucket, nil
}
// getBucketInfoResponse calls getBucketInfo() to get the bucket's info
func getBucketInfoResponse(session *models.Principal, params user_api.BucketInfoParams) (*models.Bucket, *models.Error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()
mClient, err := newMinioClient(session)
if err != nil {
return nil, prepareError(err)
@@ -546,7 +632,15 @@ func getBucketInfoResponse(session *models.Principal, params user_api.BucketInfo
// defining the client to be used
minioClient := minioClient{client: mClient}
bucket, err := getBucketInfo(minioClient, params.Name)
mAdmin, err := NewMinioAdminClient(session)
if err != nil {
return nil, prepareError(err)
}
// create a minioClient interface implementation
// defining the client to be used
adminClient := AdminClient{Client: mAdmin}
bucket, err := getBucketInfo(ctx, minioClient, adminClient, params.Name)
if err != nil {
return nil, prepareError(err)
}

View File

@@ -116,12 +116,35 @@ func TestListBucket(t *testing.T) {
adminClient := adminClientMock{}
ctx := context.Background()
// Test-1 : getaAcountUsageInfo() Get response from minio client with two buckets
infoPolicy := `
{
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}`
mockBucketList := madmin.AccountInfo{
AccountName: "test",
Buckets: []madmin.BucketAccessInfo{
{Name: "bucket-1", Created: time.Now(), Size: 1024},
{Name: "bucket-2", Created: time.Now().Add(time.Hour * 1), Size: 0},
},
Policy: []byte(infoPolicy),
}
// mock function response from listBucketsWithContext(ctx)
minioAccountInfoMock = func(ctx context.Context) (madmin.AccountInfo, error) {
@@ -206,6 +229,8 @@ func TestBucketInfo(t *testing.T) {
assert := assert.New(t)
// mock minIO client
minClient := minioClientMock{}
ctx := context.Background()
adminClient := adminClientMock{}
function := "getBucketInfo()"
// Test-1: getBucketInfo() get a bucket with PRIVATE access
@@ -221,7 +246,42 @@ func TestBucketInfo(t *testing.T) {
CreationDate: "", // to be implemented
Size: 0, // to be implemented
}
bucketInfo, err := getBucketInfo(minClient, bucketToSet)
infoPolicy := `
{
"Version": "2012-10-17",
"Statement": [{
"Action": [
"admin:*"
],
"Effect": "Allow",
"Sid": ""
},
{
"Action": [
"s3:*"
],
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*"
],
"Sid": ""
}
]
}`
mockBucketList := madmin.AccountInfo{
AccountName: "test",
Buckets: []madmin.BucketAccessInfo{
{Name: "bucket-1", Created: time.Now(), Size: 1024},
{Name: "bucket-2", Created: time.Now().Add(time.Hour * 1), Size: 0},
},
Policy: []byte(infoPolicy),
}
// mock function response from listBucketsWithContext(ctx)
minioAccountInfoMock = func(ctx context.Context) (madmin.AccountInfo, error) {
return mockBucketList, nil
}
bucketInfo, err := getBucketInfo(ctx, minClient, adminClient, bucketToSet)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -243,7 +303,7 @@ func TestBucketInfo(t *testing.T) {
CreationDate: "", // to be implemented
Size: 0, // to be implemented
}
bucketInfo, err = getBucketInfo(minClient, bucketToSet)
bucketInfo, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -265,7 +325,7 @@ func TestBucketInfo(t *testing.T) {
CreationDate: "", // to be implemented
Size: 0, // to be implemented
}
bucketInfo, err = getBucketInfo(minClient, bucketToSet)
bucketInfo, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet)
if err != nil {
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
}
@@ -286,27 +346,13 @@ func TestBucketInfo(t *testing.T) {
CreationDate: "", // to be implemented
Size: 0, // to be implemented
}
_, err = getBucketInfo(minClient, bucketToSet)
_, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet)
if assert.Error(err) {
assert.Equal("invalid character 'p' looking for beginning of value", err.Error())
}
// Test-4: getBucketInfo() handle GetBucketPolicy error correctly
mockPolicy = ""
minioGetBucketPolicyMock = func(bucketName string) (string, error) {
return "", errors.New("error")
}
bucketToSet = "csbucket"
outputExpected = &models.Bucket{
Name: swag.String(bucketToSet),
Access: models.NewBucketAccess(models.BucketAccessCUSTOM),
CreationDate: "", // to be implemented
Size: 0, // to be implemented
}
_, err = getBucketInfo(minClient, bucketToSet)
if assert.Error(err) {
assert.Equal("error", err.Error())
}
// Test removed since we can tolerate this scenario now
}
func TestSetBucketAccess(t *testing.T) {

View File

@@ -18,6 +18,7 @@ package restapi
import (
"context"
"encoding/json"
"net/http"
"net/url"
"time"
@@ -84,6 +85,17 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
return nil, prepareError(err, errorGenericInvalidSession)
}
userAdminClient := AdminClient{Client: mAdminClient}
// Policy used by the current user
accountInfo, err := userAdminClient.AccountInfo(ctx)
if err != nil {
return nil, prepareError(err)
}
var sessionPolicy *models.IamPolicy
err = json.Unmarshal(accountInfo.Policy, &sessionPolicy)
if err != nil {
return nil, prepareError(err)
}
// Obtain the current policy assigned to this user
// necessary for generating the list of allowed endpoints
policy, err := getAccountPolicy(ctx, userAdminClient)
@@ -105,6 +117,7 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
Status: models.SessionResponseStatusOk,
Operator: false,
DistributedMode: isErasureMode(),
Policy: sessionPolicy,
}
return sessionResp, nil
}

View File

@@ -2335,6 +2335,8 @@ definitions:
required:
- name
properties:
manage:
type: boolean
name:
type: string
minLength: 3
@@ -2381,6 +2383,10 @@ definitions:
- hard
creation_date:
type: string
allowedActions:
type: array
items:
type: string
bucketEncryptionRequest:
type: object
@@ -3101,6 +3107,10 @@ definitions:
type: boolean
distributedMode:
type: boolean
policy:
type: object
$ref: "#/definitions/iamPolicy"
widgetResult:
type: object
properties:
@@ -3847,3 +3857,32 @@ definitions:
type: array
items:
$ref: "#/definitions/rewindItem"
iamPolicy:
type: object
properties:
version:
type: string
statement:
type: array
items:
$ref: "#/definitions/iamPolicyStatement"
iamPolicyStatement:
type: object
properties:
effect:
type: string
action:
type: array
items:
type: string
resource:
type: array
items:
type: string
condition:
type: object
additionalProperties:
type: object