From 184f8648738fdfcd33ac4d14b66d0e5512f9885e Mon Sep 17 00:00:00 2001 From: Lenin Alevski Date: Tue, 2 Nov 2021 17:34:39 -0700 Subject: [PATCH] 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 Co-authored-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- models/bucket.go | 6 + models/iam_policy.go | 132 +++++ models/iam_policy_statement.go | 76 +++ models/session_response.go | 49 +- pkg/acl/permissions.go | 81 +++ .../BucketDetails/AccessDetailsPanel.tsx | 79 ++- .../Buckets/BucketDetails/AccessRulePanel.tsx | 67 ++- .../Buckets/BucketDetails/BrowserHandler.tsx | 39 +- .../Buckets/BucketDetails/BucketDetails.tsx | 144 +++--- .../BucketDetails/BucketEventsPanel.tsx | 72 ++- .../BucketDetails/BucketLifecyclePanel.tsx | 73 ++- .../BucketDetails/BucketReplicationPanel.tsx | 119 ++--- .../BucketDetails/BucketSummaryPanel.tsx | 337 +++++++----- .../Buckets/ListBuckets/BucketListItem.tsx | 28 +- .../Buckets/ListBuckets/ListBuckets.tsx | 8 +- .../Objects/ListObjects/ListObjects.tsx | 482 +++++++++++------- .../Objects/ObjectDetails/ObjectDetails.tsx | 321 ++++++++---- .../src/screens/Console/Buckets/types.tsx | 4 + .../DateRangeSelector/DateRangeSelector.tsx | 2 +- .../Common/TableWrapper/TableWrapper.tsx | 12 +- .../Dashboard/Prometheus/PrDashboard.tsx | 2 +- portal-ui/src/screens/Console/reducer.ts | 4 + portal-ui/src/screens/Console/types.ts | 12 + portal-ui/src/theme/main.ts | 2 +- portal-ui/src/types.ts | 52 ++ portal-ui/src/utils/permissions.ts | 49 ++ restapi/embedded_spec.go | 110 +++- restapi/operations/user_api/logout.go | 2 +- restapi/user_buckets.go | 114 ++++- restapi/user_buckets_test.go | 84 ++- restapi/user_session.go | 13 + swagger-console.yml | 39 ++ 32 files changed, 1905 insertions(+), 709 deletions(-) create mode 100644 models/iam_policy.go create mode 100644 models/iam_policy_statement.go create mode 100644 pkg/acl/permissions.go create mode 100644 portal-ui/src/utils/permissions.ts diff --git a/models/bucket.go b/models/bucket.go index 6486ce338..87d56d942 100644 --- a/models/bucket.go +++ b/models/bucket.go @@ -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 diff --git a/models/iam_policy.go b/models/iam_policy.go new file mode 100644 index 000000000..5deb2d637 --- /dev/null +++ b/models/iam_policy.go @@ -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 . +// + +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 +} diff --git a/models/iam_policy_statement.go b/models/iam_policy_statement.go new file mode 100644 index 000000000..77b5c07db --- /dev/null +++ b/models/iam_policy_statement.go @@ -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 . +// + +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 +} diff --git a/models/session_response.go b/models/session_response.go index 64ac27a05..fc4752123 100644 --- a/models/session_response.go +++ b/models/session_response.go @@ -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 } diff --git a/pkg/acl/permissions.go b/pkg/acl/permissions.go new file mode 100644 index 000000000..edc847c20 --- /dev/null +++ b/pkg/acl/permissions.go @@ -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 . + +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, +) diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx index 359bb4376..f3345fff2 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx @@ -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(0); const [loadingPolicies, setLoadingPolicies] = useState(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" > - {usersEnabled && } + {displayUsersList && } - {usersEnabled && ( + {displayUsersList && ( 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(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 = ({ )}

Access Rules

- + {displayAddAccessRules && ( + + )}

. -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 = ({ } actions={ - - - - - - - + bucketInfo?.manage && ( + + + + + + + + ) } /> @@ -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); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx index 8ebd9310a..d8f874fea 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx @@ -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(false); - const [loadingPerms, setLoadingPerms] = useState(true); - const [canGetReplication, setCanGetReplication] = useState(false); const [deleteOpen, setDeleteOpen] = useState(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={ - - Access:{" "} - {bucketInfo && - bucketInfo?.access[0].toUpperCase() + - bucketInfo?.access.substr(1).toLowerCase()} - + displayComponent( + bucketInfo?.allowedActions, + [S3_GET_BUCKET_POLICY], + false + ) && ( + + Access:{" "} + + {bucketInfo?.access.toLowerCase()} + + + ) } actions={ - - { - setDeleteOpen(true); - }} - size="large" - > - - - + {displayComponent(bucketInfo?.allowedActions, [ + S3_DELETE_BUCKET, + S3_FORCE_DELETE_BUCKET, + ]) && ( + + { + setDeleteOpen(true); + }} + size="large" + > + + + + )} { @@ -423,7 +419,13 @@ const BucketDetails = ({ { changeRoute("replication"); @@ -431,9 +433,15 @@ const BucketDetails = ({ > - { changeRoute("lifecycle"); @@ -443,6 +451,13 @@ const BucketDetails = ({ { changeRoute("access"); @@ -452,6 +467,11 @@ const BucketDetails = ({ { changeRoute("prefix"); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketEventsPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketEventsPanel.tsx index 0b7fb6bc2..b0c833eb1 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketEventsPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketEventsPanel.tsx @@ -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(false); const [loadingEvents, setLoadingEvents] = useState(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 = ({

Events

- + {displaySubscribeToEvents && ( + + )}
({ session: state.console.session, loadingBucket: state.buckets.bucketDetails.loadingBucket, + bucketInfo: state.buckets.bucketDetails.bucketInfo, }); const connector = connect(mapState, { diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx index 35c248090..4d3f2095a 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx @@ -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(true); const [lifecycleRecords, setLifecycleRecords] = useState([]); @@ -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 = ({

Lifecycle Rules

- + {displayAddLifeCycleRules && ( + + )}
({ session: state.console.session, loadingBucket: state.buckets.bucketDetails.loadingBucket, + bucketInfo: state.buckets.bucketDetails.bucketInfo, }); const connector = connect(mapState, { diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx index de2fd9e80..97af047ac 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx @@ -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(false); const [loadingReplication, setLoadingReplication] = useState(true); const [replicationRules, setReplicationRules] = useState< BucketReplicationRule[] >([]); - const [loadingPerms, setLoadingPerms] = useState(true); const [deleteReplicationModal, setDeleteReplicationModal] = useState(false); const [openSetReplication, setOpenSetReplication] = useState(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 = ({

Replication

- + {displayAddReplicationRules && ( + + )}
({ session: state.console.session, loadingBucket: state.buckets.bucketDetails.loadingBucket, + bucketInfo: state.buckets.bucketDetails.bucketInfo, }); const connector = connect(mapState, { diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx index 54042db0a..284e6f963 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketSummaryPanel.tsx @@ -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 = ({ - - - + + + + )} + {distributedSetup && ( + + {displayGetReplicationConfiguration && ( + + + + + )} + {displayGetBucketObjectLockConfiguration && ( + + + + + )} + + )} + {displayGetBucketEncryptionConfiguration && ( + + + - - {distributedSetup && ( - - - - - - - - - - + + )} - - - -
Access Policy: -
Access Policy: + +
Replication: + + {replicationRules ? "Enabled" : "Disabled"} + +
Object Locking:{!hasObjectLocking ? "Disabled" : "Enabled"}
Encryption: + {loadingEncryption ? ( ) : ( - accessPolicy.toLowerCase() + )} - -
Replication: - {replicationRules ? "Enabled" : "Disabled"} -
Object Locking:{!hasObjectLocking ? "Disabled" : "Enabled"}
Encryption: - {loadingEncryption ? ( - - ) : ( - - )} -
@@ -485,7 +583,7 @@ const BucketSummary = ({


- {distributedSetup && ( + {distributedSetup && displayGetBucketVersioning && ( @@ -506,6 +604,7 @@ const BucketSummary = ({ ) : ( - - )} - + {displayGetBucketQuota && ( + + Quota: + + {loadingQuota ? ( + + ) : ( + + + + )} + + + )} @@ -563,7 +667,7 @@ const BucketSummary = ({ )} - {hasObjectLocking && ( + {hasObjectLocking && displayGetObjectRetention && ( @@ -583,6 +687,7 @@ const BucketSummary = ({ ) : ( - - + + + + )} diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/ListBuckets.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/ListBuckets.tsx index 45755a574..0aeefa76b 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/ListBuckets.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/ListBuckets.tsx @@ -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([]); const [loading, setLoading] = useState(true); @@ -327,7 +330,7 @@ const ListBuckets = ({ }} inputProps={{ disableUnderline: true, - endAdornment: ( + endadornment: ( @@ -345,7 +348,7 @@ const ListBuckets = ({ }} inputProps={{ disableUnderline: true, - endAdornment: ( + endadornment: ( @@ -462,6 +465,7 @@ const ListBuckets = ({ const mapState = (state: AppState) => ({ addBucketModalOpen: state.buckets.open, + session: state.console.session, }); const connector = connect(mapState, { diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index c9fed125f..e4cfe6a34 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -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([]); const [loading, setLoading] = useState(true); @@ -269,12 +288,29 @@ const ListObjects = ({ "ASC" | "DESC" | undefined >("ASC"); const [currentSortField, setCurrentSortField] = useState("name"); + const [iniLoad, setIniLoad] = useState(false); const internalPaths = get(match.params, "subpaths", ""); const bucketName = match.params["bucketName"]; const fileUpload = useRef(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={ - - { - setCreateFolderOpen(true); - }} - disabled={rewindEnabled} - size="large" - > - - - + {displayPutObject && ( + + + { + setCreateFolderOpen(true); + }} + disabled={rewindEnabled} + size="large" + > + + + - - { - if (fileUpload && fileUpload.current) { - fileUpload.current.click(); - } - }} - disabled={rewindEnabled} - size="large" - > - - - + + { + if (fileUpload && fileUpload.current) { + fileUpload.current.click(); + } + }} + disabled={rewindEnabled} + size="large" + > + + + + uploadObject(e)} + id="file-input" + style={{ display: "none" }} + ref={fileUpload} + /> + + )} - uploadObject(e)} - id="file-input" - style={{ display: "none" }} - ref={fileUpload} - /> - { - setFilterObjects(val.target.value); - }} - InputProps={{ - disableUnderline: true, - startAdornment: ( - - - - ), - }} - variant="standard" - /> + {displayListObjects && ( + { + setFilterObjects(val.target.value); + }} + InputProps={{ + disableUnderline: true, + startAdornment: ( + + + + ), + }} + variant="standard" + /> + )} - + {displayDeleteObject && ( + + )}
({ +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); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx index 8e1f84ece..fa92caa44 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx @@ -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(true); const [metadata, setMetadata] = useState({}); const [selectedTab, setSelectedTab] = useState(0); + const [loadingBucket, setLoadingBucket] = useState(false); + const [bucketInfo, setBucketInfo] = useState(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} /> )} - {!actualInfo && ( @@ -576,19 +648,21 @@ const ObjectDetails = ({
)} - - { - setDeleteOpen(true); - }} - disabled={actualInfo.is_delete_marker} - size="large" - > - - - + {displayDeleteObject && ( + + { + setDeleteOpen(true); + }} + disabled={actualInfo.is_delete_marker} + size="large" + > + + + + )}
} /> @@ -635,109 +709,138 @@ const ObjectDetails = ({

Details


- - - - - - - - + + )} + +
Legal Hold: - {actualInfo.version_id && - actualInfo.version_id !== "null" ? ( - - {actualInfo.legal_hold_status - ? actualInfo.legal_hold_status.toLowerCase() - : "Off"} - { - setLegalholdOpen(true); - }} - > - - - - ) : ( - "Disabled" + {(displayObjectLegalHold || + displayObjectRetention || + displayObjectTag) && ( + + + + + + + {displayObjectLegalHold && ( + + + + )} - - - - - - - - - + + + )} + {displayObjectTag && ( + + + - - -
+ Legal Hold: + + {actualInfo.version_id && + actualInfo.version_id !== "null" ? ( + + {actualInfo.legal_hold_status + ? actualInfo.legal_hold_status.toLowerCase() + : "Off"} + {displayEditObjectLegalHold && ( + { + setLegalholdOpen(true); + }} + > + + + )} + + ) : ( + "Disabled" + )} +
Retention: - {actualInfo.retention_mode - ? actualInfo.retention_mode.toLowerCase() - : "None"} - { - openRetentionModal(); - }} - > - - -
Tags: - {tagKeys && - tagKeys.map((tagKey, index) => { - const tag = get( - actualInfo, - `tags.${tagKey}`, - "" - ); - if (tag !== "") { - return ( - + + Retention: + + {actualInfo.retention_mode + ? actualInfo.retention_mode.toLowerCase() + : "None"} + {displayEditObjectRetention && ( + } - onDelete={() => { - deleteTag(tagKey, tag); + aria-label="retention" + size="small" + className={classes.propertiesIcon} + onClick={() => { + openRetentionModal(); + }} + > + + + )} +
Tags: + {tagKeys && + tagKeys.map((tagKey, index) => { + const tag = get( + actualInfo, + `tags.${tagKey}`, + "" + ); + if (tag !== "") { + return displayRemoveObjectTagging ? ( + } + onDelete={() => { + deleteTag(tagKey, tag); + }} + /> + ) : ( + + ); + } + return null; + })} + {displayEditObjectTagging && ( + } + clickable + size="small" + label="Add tag" + color="primary" + variant="outlined" + onClick={() => { + setTagModalOpen(true); }} /> - ); - } - return null; - })} - } - clickable - size="small" - label="Add tag" - color="primary" - variant="outlined" - onClick={() => { - setTagModalOpen(true); - }} - /> -
-
-
-
-
-
+ )} +
+
+
+
+
+ + )}

Object Metadata

-
+
diff --git a/portal-ui/src/screens/Console/Buckets/types.tsx b/portal-ui/src/screens/Console/Buckets/types.tsx index d3ff318f7..5a3d3135f 100644 --- a/portal-ui/src/screens/Console/Buckets/types.tsx +++ b/portal-ui/src/screens/Console/Buckets/types.tsx @@ -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 { diff --git a/portal-ui/src/screens/Console/Common/FormComponents/DateRangeSelector/DateRangeSelector.tsx b/portal-ui/src/screens/Console/Common/FormComponents/DateRangeSelector/DateRangeSelector.tsx index 8bbff9fcf..191871b2a 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/DateRangeSelector/DateRangeSelector.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/DateRangeSelector/DateRangeSelector.tsx @@ -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, }, }, diff --git a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx index eb1001107..fb24da058 100644 --- a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx @@ -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(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -597,9 +603,9 @@ const TableWrapper = ({ return ( ...actionsTray, ...widgetContainerCommon, syncButton: { - "&.MuiButton-root .MuiButton-iconSizeMedium > *:first-child": { + "&.MuiButton-root .MuiButton-iconSizeMedium > *:first-of-type": { fontSize: 18, }, }, diff --git a/portal-ui/src/screens/Console/reducer.ts b/portal-ui/src/screens/Console/reducer.ts index 19752f6d4..2924e9e7e 100644 --- a/portal-ui/src/screens/Console/reducer.ts +++ b/portal-ui/src/screens/Console/reducer.ts @@ -28,6 +28,10 @@ const initialState: ConsoleState = { pages: [], features: [], distributedMode: false, + policy: { + version: "", + statement: [], + }, }, }; diff --git a/portal-ui/src/screens/Console/types.ts b/portal-ui/src/screens/Console/types.ts index 4dc813068..dce307f36 100644 --- a/portal-ui/src/screens/Console/types.ts +++ b/portal-ui/src/screens/Console/types.ts @@ -14,10 +14,22 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +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, } diff --git a/portal-ui/src/theme/main.ts b/portal-ui/src/theme/main.ts index 99eb92c94..dc995f005 100644 --- a/portal-ui/src/theme/main.ts +++ b/portal-ui/src/theme/main.ts @@ -85,7 +85,7 @@ const theme = createTheme({ fontWeight: 600, color: "#767676", }, - "& .MuiButton-iconSizeMedium > *:first-child": { + "& .MuiButton-iconSizeMedium > *:first-of-type": { fontSize: 12, }, }, diff --git a/portal-ui/src/types.ts b/portal-ui/src/types.ts index 08a0718a1..d01ff42f8 100644 --- a/portal-ui/src/types.ts +++ b/portal-ui/src/types.ts @@ -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:*"; diff --git a/portal-ui/src/utils/permissions.ts b/portal-ui/src/utils/permissions.ts new file mode 100644 index 000000000..1949d3b11 --- /dev/null +++ b/portal-ui/src/utils/permissions.ts @@ -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 . + +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; +}; diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index f5f7ed15c..276207395 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -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": [ diff --git a/restapi/operations/user_api/logout.go b/restapi/operations/user_api/logout.go index ad4b10d4a..4ccc7a698 100644 --- a/restapi/operations/user_api/logout.go +++ b/restapi/operations/user_api/logout.go @@ -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 { diff --git a/restapi/user_buckets.go b/restapi/user_buckets.go index 8e71f6d65..1448f6a6a 100644 --- a/restapi/user_buckets.go +++ b/restapi/user_buckets.go @@ -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) } diff --git a/restapi/user_buckets_test.go b/restapi/user_buckets_test.go index 4f271a562..738e73d3c 100644 --- a/restapi/user_buckets_test.go +++ b/restapi/user_buckets_test.go @@ -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) { diff --git a/restapi/user_session.go b/restapi/user_session.go index 57e4e22bf..00ad8b26d 100644 --- a/restapi/user_session.go +++ b/restapi/user_session.go @@ -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 } diff --git a/swagger-console.yml b/swagger-console.yml index fb44e49ae..d44cd2137 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -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