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:
@@ -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
132
models/iam_policy.go
Normal 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
|
||||
}
|
||||
76
models/iam_policy_statement.go
Normal file
76
models/iam_policy_statement.go
Normal 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
|
||||
}
|
||||
@@ -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
81
pkg/acl/permissions.go
Normal 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,
|
||||
)
|
||||
@@ -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}
|
||||
|
||||
@@ -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={[
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,6 +28,10 @@ const initialState: ConsoleState = {
|
||||
pages: [],
|
||||
features: [],
|
||||
distributedMode: false,
|
||||
policy: {
|
||||
version: "",
|
||||
statement: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ const theme = createTheme({
|
||||
fontWeight: 600,
|
||||
color: "#767676",
|
||||
},
|
||||
"& .MuiButton-iconSizeMedium > *:first-child": {
|
||||
"& .MuiButton-iconSizeMedium > *:first-of-type": {
|
||||
fontSize: 12,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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:*";
|
||||
|
||||
49
portal-ui/src/utils/permissions.ts
Normal file
49
portal-ui/src/utils/permissions.ts
Normal 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;
|
||||
};
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user