From aae493ac829311c62822acbb5ecee444ea6edf8c Mon Sep 17 00:00:00 2001 From: Lenin Alevski Date: Thu, 18 Nov 2021 08:25:01 -0800 Subject: [PATCH] Re-implement policy handling in react (#1234) Signed-off-by: Lenin Alevski --- models/bucket.go | 6 - models/session_response.go | 50 +- pkg/acl/permissions.go | 82 ---- .../SecureComponent/SecureComponent.tsx | 69 +++ .../src/common/SecureComponent/permissions.ts | 177 ++++++++ .../BucketDetails/AccessDetailsPanel.tsx | 69 +-- .../Buckets/BucketDetails/AccessRulePanel.tsx | 66 ++- .../Buckets/BucketDetails/BrowserHandler.tsx | 36 +- .../Buckets/BucketDetails/BucketDetails.tsx | 83 ++-- .../BucketDetails/BucketEventsPanel.tsx | 74 +-- .../BucketDetails/BucketLifecyclePanel.tsx | 57 +-- .../BucketDetails/BucketReplicationPanel.tsx | 92 ++-- .../BucketDetails/BucketSummaryPanel.tsx | 429 +++++++++--------- .../Buckets/ListBuckets/BucketListItem.tsx | 12 +- .../Buckets/ListBuckets/ListBuckets.tsx | 51 +-- .../Objects/ListObjects/ListObjects.tsx | 182 ++++---- .../Objects/ObjectDetails/ObjectDetails.tsx | 313 ++++++------- .../src/screens/Console/Buckets/types.tsx | 3 - .../Common/TableWrapper/TableWrapper.tsx | 13 +- portal-ui/src/screens/Console/reducer.ts | 5 +- portal-ui/src/screens/Console/types.ts | 13 +- portal-ui/src/store.ts | 7 +- portal-ui/src/types.ts | 52 --- portal-ui/src/utils/permissions.ts | 44 -- restapi/admin_remote_buckets.go | 2 +- restapi/embedded_spec.go | 36 +- restapi/user_buckets.go | 103 +---- restapi/user_buckets_test.go | 13 +- restapi/user_session.go | 78 +++- swagger-console.yml | 13 +- 30 files changed, 1098 insertions(+), 1132 deletions(-) delete mode 100644 pkg/acl/permissions.go create mode 100644 portal-ui/src/common/SecureComponent/SecureComponent.tsx create mode 100644 portal-ui/src/common/SecureComponent/permissions.ts delete mode 100644 portal-ui/src/utils/permissions.ts diff --git a/models/bucket.go b/models/bucket.go index 87d56d942..6486ce338 100644 --- a/models/bucket.go +++ b/models/bucket.go @@ -40,18 +40,12 @@ 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/session_response.go b/models/session_response.go index fc4752123..70a892dfd 100644 --- a/models/session_response.go +++ b/models/session_response.go @@ -49,8 +49,8 @@ type SessionResponse struct { // pages Pages []string `json:"pages"` - // policy - Policy *IamPolicy `json:"policy,omitempty"` + // permissions + Permissions map[string][]string `json:"permissions,omitempty"` // status // Enum: [ok] @@ -61,10 +61,6 @@ 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) } @@ -75,23 +71,6 @@ 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() { @@ -131,31 +110,8 @@ func (m *SessionResponse) validateStatus(formats strfmt.Registry) error { return nil } -// ContextValidate validate this session response based on the context it is used +// ContextValidate validates this session response based on 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 deleted file mode 100644 index 73392d5ef..000000000 --- a/pkg/acl/permissions.go +++ /dev/null @@ -1,82 +0,0 @@ -// 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, - iampolicy.AllAdminActions, -) diff --git a/portal-ui/src/common/SecureComponent/SecureComponent.tsx b/portal-ui/src/common/SecureComponent/SecureComponent.tsx new file mode 100644 index 000000000..817cbd4ac --- /dev/null +++ b/portal-ui/src/common/SecureComponent/SecureComponent.tsx @@ -0,0 +1,69 @@ +// 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 React, { cloneElement } from "react"; +import { store } from "../../store"; +import { hasAccessToResource } from "./permissions"; + +export const hasPermission = ( + resource: string | undefined, + scopes: string[], + matchAll?: boolean +) => { + if (!resource) { + return false; + } + const state = store.getState(); + const sessionGrants = state.console.session.permissions; + const resourceGrants = + sessionGrants[resource] || + sessionGrants[`arn:aws:s3:::${resource}/*`] || + []; + const globalGrants = sessionGrants["arn:aws:s3:::*"] || []; + return hasAccessToResource( + [...resourceGrants, ...globalGrants], + scopes, + matchAll + ); +}; + +interface ISecureComponentProps { + errorProps?: any; + RenderError?: any; + matchAll?: boolean; + children: any; + scopes: string[]; + resource: string; +} + +const SecureComponent = ({ + children, + RenderError = () => <>, + errorProps = null, + matchAll = false, + scopes = [], + resource, +}: ISecureComponentProps) => { + const permissionGranted = hasPermission(resource, scopes, matchAll); + if (!permissionGranted && !errorProps) return ; + if (!permissionGranted && errorProps) { + return cloneElement(children, { ...errorProps }); + } + + return <>{children}; +}; + +export default SecureComponent; diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts new file mode 100644 index 000000000..e8e79bbb7 --- /dev/null +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -0,0 +1,177 @@ +// 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 . + +// hasAccessToResource 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 hasAccessToResource = ( + userPermissionsOnBucket: string[] | null | undefined, + requiredPermissions: string[], + matchAll?: boolean +) => { + if (!userPermissionsOnBucket) { + return false; + } + + const s3All = userPermissionsOnBucket.includes(IAM_SCOPES.S3_ALL_ACTIONS); + const AdminAll = userPermissionsOnBucket.includes( + IAM_SCOPES.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; +}; + +export const IAM_ROLES = { + viewer: "VIEWER", + editor: "EDITOR", + owner: "OWNER", + admin: "ADMIN", +}; + +export const IAM_SCOPES = { + S3_LIST_BUCKET: "s3:ListBucket", + S3_GET_BUCKET_POLICY: "s3:GetBucketPolicy", + S3_PUT_BUCKET_POLICY: "s3:PutBucketPolicy", + S3_GET_OBJECT: "s3:GetObject", + S3_PUT_OBJECT: "s3:PutObject", + S3_GET_OBJECT_LEGAL_HOLD: "s3:GetObjectLegalHold", + S3_PUT_OBJECT_LEGAL_HOLD: "s3:PutObjectLegalHold", + S3_DELETE_OBJECT: "s3:DeleteObject", + S3_GET_BUCKET_VERSIONING: "s3:GetBucketVersioning", + S3_PUT_BUCKET_VERSIONING: "s3:PutBucketVersioning", + S3_GET_OBJECT_RETENTION: "s3:GetObjectRetention", + S3_PUT_OBJECT_RETENTION: "s3:PutObjectRetention", + S3_GET_OBJECT_TAGGING: "s3:GetObjectTagging", + S3_PUT_OBJECT_TAGGING: "s3:PutObjectTagging", + S3_DELETE_OBJECT_TAGGING: "s3:DeleteObjectTagging", + S3_GET_BUCKET_ENCRYPTION_CONFIGURATION: "s3:GetEncryptionConfiguration", + S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION: "s3:PutEncryptionConfiguration", + S3_CREATE_BUCKET: "s3:CreateBucket", + S3_DELETE_BUCKET: "s3:DeleteBucket", + S3_FORCE_DELETE_BUCKET: "s3:ForceDeleteBucket", + S3_GET_BUCKET_NOTIFICATIONS: "s3:GetBucketNotification", + S3_LISTEN_BUCKET_NOTIFICATIONS: "s3:ListenBucketNotification", + S3_PUT_BUCKET_NOTIFICATIONS: "s3:PutBucketNotification", + S3_GET_REPLICATION_CONFIGURATION: "s3:GetReplicationConfiguration", + S3_PUT_REPLICATION_CONFIGURATION: "s3:PutReplicationConfiguration", + S3_GET_LIFECYCLE_CONFIGURATION: "s3:GetLifecycleConfiguration", + S3_PUT_LIFECYCLE_CONFIGURATION: "s3:PutLifecycleConfiguration", + S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION: + "s3:GetBucketObjectLockConfiguration", + S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION: + "s3:PutBucketObjectLockConfiguration", + ADMIN_GET_POLICY: "admin:GetPolicy", + ADMIN_LIST_USERS: "admin:ListUsers", + ADMIN_LIST_USER_POLICIES: "admin:ListUserPolicies", + ADMIN_SERVER_INFO: "admin:ServerInfo", + ADMIN_GET_BUCKET_QUOTA: "admin:GetBucketQuota", + ADMIN_SET_BUCKET_QUOTA: "admin:SetBucketQuota", + ADMIN_LIST_TIERS: "admin:ListTier", + ADMIN_LIST_GROUPS: "admin:ListGroups", + S3_GET_OBJECT_VERSION_FOR_REPLICATION: "s3:GetObjectVersionForReplication", + S3_REPLICATE_TAGS: "s3:ReplicateTags", + S3_REPLICATE_DELETE: "s3:ReplicateDelete", + S3_REPLICATE_OBJECT: "s3:ReplicateObject", + S3_PUT_OBJECT_VERSION_TAGGING: "s3:PutObjectVersionTagging", + S3_DELETE_OBJECT_VERSION_TAGGING: "s3:DeleteObjectVersionTagging", + S3_DELETE_OBJECT_VERSION: "s3:DeleteObjectVersion", + S3_GET_OBJECT_VERSION_TAGGING: "s3:GetObjectVersionTagging", + S3_GET_OBJECT_VERSION: "s3:GetObjectVersion", + S3_PUT_BUCKET_TAGGING: "s3:PutBucketTagging", + S3_GET_BUCKET_TAGGING: "s3:GetBucketTagging", + S3_BYPASS_GOVERNANCE_RETENTION: "s3:BypassGovernanceRetention", + S3_LIST_MULTIPART_UPLOAD_PARTS: "s3:ListMultipartUploadParts", + S3_LISTEN_NOTIFICATIONS: "s3:ListenNotification", + S3_LIST_BUCKET_MULTIPART_UPLOADS: "s3:ListBucketMultipartUploads", + S3_LIST_BUCKET_VERSIONS: "s3:ListBucketVersions", + S3_GET_BUCKET_POLICY_STATUS: "s3:GetBucketPolicyStatus", + S3_LIST_ALL_MY_BUCKETS: "s3:ListAllMyBuckets", + S3_HEAD_BUCKET: "s3:HeadBucket", + S3_GET_BUCKET_LOCATION: "s3:GetBucketLocation", + S3_DELETE_BUCKET_POLICY: "s3:DeleteBucketPolicy", + S3_ABORT_MULTIPART_UPLOAD: "s3:AbortMultipartUpload", + S3_ALL_ACTIONS: "s3:*", + ADMIN_ALL_ACTIONS: "admin:*", +}; + +export const IAM_PERMISSIONS = { + [IAM_ROLES.admin]: [ + IAM_SCOPES.S3_ALL_ACTIONS, + IAM_SCOPES.ADMIN_ALL_ACTIONS, + IAM_SCOPES.S3_REPLICATE_OBJECT, + IAM_SCOPES.S3_REPLICATE_DELETE, + IAM_SCOPES.S3_REPLICATE_TAGS, + IAM_SCOPES.S3_GET_OBJECT_VERSION_FOR_REPLICATION, + IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION, + IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, + IAM_SCOPES.S3_GET_BUCKET_VERSIONING, + IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, + IAM_SCOPES.S3_GET_BUCKET_ENCRYPTION_CONFIGURATION, + IAM_SCOPES.S3_PUT_BUCKET_ENCRYPTION_CONFIGURATION, + IAM_SCOPES.S3_DELETE_OBJECT_TAGGING, + IAM_SCOPES.S3_PUT_OBJECT_TAGGING, + IAM_SCOPES.S3_GET_OBJECT_TAGGING, + IAM_SCOPES.S3_PUT_OBJECT_VERSION_TAGGING, + IAM_SCOPES.S3_DELETE_OBJECT_VERSION_TAGGING, + IAM_SCOPES.S3_DELETE_OBJECT_VERSION, + IAM_SCOPES.S3_GET_OBJECT_VERSION_TAGGING, + IAM_SCOPES.S3_GET_OBJECT_VERSION, + IAM_SCOPES.S3_PUT_BUCKET_TAGGING, + IAM_SCOPES.S3_GET_BUCKET_TAGGING, + IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION, + IAM_SCOPES.S3_GET_BUCKET_OBJECT_LOCK_CONFIGURATION, + IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD, + IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD, + IAM_SCOPES.S3_GET_OBJECT_RETENTION, + IAM_SCOPES.S3_PUT_OBJECT_RETENTION, + IAM_SCOPES.S3_BYPASS_GOVERNANCE_RETENTION, + IAM_SCOPES.S3_PUT_BUCKET_POLICY, + IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS, + IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION, + IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION, + IAM_SCOPES.S3_LIST_MULTIPART_UPLOAD_PARTS, + IAM_SCOPES.S3_LISTEN_BUCKET_NOTIFICATIONS, + IAM_SCOPES.S3_LISTEN_NOTIFICATIONS, + IAM_SCOPES.S3_LIST_BUCKET_MULTIPART_UPLOADS, + IAM_SCOPES.S3_LIST_BUCKET_VERSIONS, + IAM_SCOPES.S3_GET_BUCKET_POLICY_STATUS, + IAM_SCOPES.S3_LIST_ALL_MY_BUCKETS, + IAM_SCOPES.S3_HEAD_BUCKET, + IAM_SCOPES.S3_GET_BUCKET_POLICY, + IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS, + IAM_SCOPES.S3_GET_BUCKET_LOCATION, + IAM_SCOPES.S3_DELETE_BUCKET_POLICY, + IAM_SCOPES.S3_FORCE_DELETE_BUCKET, + IAM_SCOPES.S3_DELETE_BUCKET, + IAM_SCOPES.S3_CREATE_BUCKET, + IAM_SCOPES.S3_ABORT_MULTIPART_UPLOAD, + IAM_SCOPES.ADMIN_GET_POLICY, + IAM_SCOPES.ADMIN_LIST_USER_POLICIES, + IAM_SCOPES.ADMIN_LIST_USERS, + ], +}; + +export const S3_ALL_RESOURCES = "arn:aws:s3:::*"; +export const CONSOLE_UI_RESOURCE = "console-ui"; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx index 1ea452077..a5d536f1e 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessDetailsPanel.tsx @@ -30,14 +30,11 @@ 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"; +import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; import PanelTitle from "../../Common/PanelTitle/PanelTitle"; +import SecureComponent, { + hasPermission, +} from "../../../../common/SecureComponent/SecureComponent"; const mapState = (state: AppState) => ({ session: state.console.session, @@ -79,13 +76,17 @@ const AccessDetails = ({ const bucketName = match.params["bucketName"]; - const displayPoliciesList = displayComponent(bucketInfo?.allowedActions, [ - ADMIN_LIST_USER_POLICIES, + const displayPoliciesList = hasPermission(bucketName, [ + IAM_SCOPES.ADMIN_LIST_USER_POLICIES, ]); - const displayUsersList = displayComponent( - bucketInfo?.allowedActions, - [ADMIN_GET_POLICY, ADMIN_LIST_USERS, ADMIN_LIST_GROUPS], + const displayUsersList = hasPermission( + bucketName, + [ + IAM_SCOPES.ADMIN_GET_POLICY, + IAM_SCOPES.ADMIN_LIST_USERS, + IAM_SCOPES.ADMIN_LIST_GROUPS, + ], true ); @@ -135,7 +136,6 @@ const AccessDetails = ({ useEffect(() => { if (loadingPolicies) { - console.log("displayPoliciesList", displayPoliciesList); if (displayPoliciesList) { api .invoke("GET", `/api/v1/bucket-policy/${bucketName}`) @@ -167,24 +167,37 @@ const AccessDetails = ({ variant="scrollable" scrollButtons="auto" > - + {displayPoliciesList && } {displayUsersList && } - + + + - {displayUsersList && ( - + + + - - )} + + ); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessRulePanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessRulePanel.tsx index 958602dd6..2f8df7722 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessRulePanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/AccessRulePanel.tsx @@ -38,9 +38,11 @@ import { 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"; +import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; import PanelTitle from "../../Common/PanelTitle/PanelTitle"; +import SecureComponent, { + hasPermission, +} from "../../../../common/SecureComponent/SecureComponent"; const styles = (theme: Theme) => createStyles({ @@ -146,15 +148,17 @@ const AccessRule = ({ const bucketName = match.params["bucketName"]; - const displayAccessRules = displayComponent(bucketInfo?.allowedActions, [ - S3_GET_BUCKET_POLICY, + const displayAccessRules = hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_BUCKET_POLICY, ]); - const displayAddAccessRules = displayComponent( - bucketInfo?.allowedActions, - [S3_GET_BUCKET_POLICY, S3_PUT_BUCKET_POLICY], - true - ); + const deleteAccessRules = hasPermission(bucketName, [ + IAM_SCOPES.S3_DELETE_BUCKET_POLICY, + ]); + + const editAccessRules = hasPermission(bucketName, [ + IAM_SCOPES.S3_PUT_BUCKET_POLICY, + ]); useEffect(() => { if (loadingBucket) { @@ -165,6 +169,7 @@ const AccessRule = ({ const AccessRuleActions = [ { type: "delete", + disableButtonFunction: () => !deleteAccessRules, onClick: (accessRule: any) => { setDeleteAccessRuleOpen(true); setAccessRuleToDelete(accessRule.prefix); @@ -172,6 +177,7 @@ const AccessRule = ({ }, { type: "view", + disableButtonFunction: () => !editAccessRules, onClick: (accessRule: any) => { setAccessRuleToEdit(accessRule.prefix); setInitialAccess(accessRule.access); @@ -247,7 +253,14 @@ const AccessRule = ({ )} Access Rules - {displayAddAccessRules && ( + - )} + - + + + ); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx index b3b9e096c..b59618bf6 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx @@ -31,6 +31,11 @@ import PageHeader from "../../Common/PageHeader/PageHeader"; import { SettingsIcon } from "../../../../icons"; import { BucketInfo } from "../types"; import { setErrorSnackMessage } from "../../../../actions"; +import SecureComponent from "../../../../common/SecureComponent/SecureComponent"; +import { + IAM_PERMISSIONS, + IAM_ROLES, +} from "../../../../common/SecureComponent/permissions"; interface IBrowserHandlerProps { fileMode: boolean; @@ -82,21 +87,22 @@ const BrowserHandler = ({ } actions={ - bucketInfo?.manage && ( - - - - - - - - ) + + + + + + + } /> {fileMode ? : } diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx index 38465f2e9..7415b440d 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketDetails.tsx @@ -52,24 +52,13 @@ import DeleteBucket from "../ListBuckets/DeleteBucket"; import AccessRulePanel from "./AccessRulePanel"; import RefreshIcon from "../../../../icons/RefreshIcon"; import BoxIconButton from "../../Common/BoxIconButton/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_PUT_BUCKET_NOTIFICATIONS, - S3_PUT_LIFECYCLE_CONFIGURATION, - S3_PUT_REPLICATION_CONFIGURATION, -} from "../../../../types"; -import { displayComponent } from "../../../../utils/permissions"; +import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; import PageLayout from "../../Common/Layout/PageLayout"; import VerticalTabs from "../../Common/VerticalTabs/VerticalTabs"; import BackLink from "../../../../common/BackLink"; +import SecureComponent, { + hasPermission, +} from "../../../../common/SecureComponent/SecureComponent"; const styles = (theme: Theme) => createStyles({ @@ -337,25 +326,25 @@ const BucketDetails = ({ } title={bucketName} subTitle={ - displayComponent( - bucketInfo?.allowedActions, - [S3_GET_BUCKET_POLICY], - false - ) && ( - - Access:{" "} - - {bucketInfo?.access.toLowerCase()} - - - ) + + Access:{" "} + + {bucketInfo?.access.toLowerCase()} + + } actions={ - {displayComponent(bucketInfo?.allowedActions, [ - S3_DELETE_BUCKET, - S3_FORCE_DELETE_BUCKET, - ]) && ( + - )} + createStyles({ @@ -76,16 +74,10 @@ const BucketEventsPanel = ({ const bucketName = match.params["bucketName"]; - const displayEvents = displayComponent(bucketInfo?.allowedActions, [ - S3_GET_BUCKET_NOTIFICATIONS, + const displayEvents = hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS, ]); - const displaySubscribeToEvents = displayComponent( - bucketInfo?.allowedActions, - [S3_PUT_BUCKET_NOTIFICATIONS, ADMIN_SERVER_INFO], - true - ); - useEffect(() => { if (loadingBucket) { setLoadingEvents(true); @@ -156,7 +148,14 @@ const BucketEventsPanel = ({ Events - {displaySubscribeToEvents && ( + - )} + - + + + {!loadingEvents && ( diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx index 374aeaf27..81f2b3c6c 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketLifecyclePanel.tsx @@ -37,13 +37,11 @@ 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_PUT_LIFECYCLE_CONFIGURATION, -} from "../../../../types"; import PanelTitle from "../../Common/PanelTitle/PanelTitle"; +import SecureComponent, { + hasPermission, +} from "../../../../common/SecureComponent/SecureComponent"; +import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; const styles = (theme: Theme) => createStyles({ @@ -76,16 +74,10 @@ const BucketLifecyclePanel = ({ const bucketName = match.params["bucketName"]; - const displayLifeCycleRules = displayComponent(bucketInfo?.allowedActions, [ - S3_GET_LIFECYCLE_CONFIGURATION, + const displayLifeCycleRules = hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION, ]); - const displayAddLifeCycleRules = displayComponent( - bucketInfo?.allowedActions, - [S3_PUT_LIFECYCLE_CONFIGURATION, ADMIN_LIST_TIERS], - true - ); - useEffect(() => { if (loadingBucket) { setLoadingLifecycle(true); @@ -212,7 +204,14 @@ const BucketLifecyclePanel = ({ Lifecycle Rules - {displayAddLifeCycleRules && ( + - )} + - + + + {!loadingLifecycle && ( diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx index 249cdbfd1..2b79c07f7 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BucketReplicationPanel.tsx @@ -40,12 +40,11 @@ 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 { - S3_GET_REPLICATION_CONFIGURATION, - S3_PUT_REPLICATION_CONFIGURATION, -} from "../../../../types"; import PanelTitle from "../../Common/PanelTitle/PanelTitle"; +import SecureComponent, { + hasPermission, +} from "../../../../common/SecureComponent/SecureComponent"; +import { IAM_SCOPES } from "../../../../common/SecureComponent/permissions"; interface IBucketReplicationProps { classes: any; @@ -82,16 +81,10 @@ const BucketReplicationPanel = ({ const bucketName = match.params["bucketName"]; - const displayReplicationRules = displayComponent(bucketInfo?.allowedActions, [ - S3_GET_REPLICATION_CONFIGURATION, + const displayReplicationRules = hasPermission(bucketName, [ + IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION, ]); - const displayAddReplicationRules = displayComponent( - bucketInfo?.allowedActions, - [S3_PUT_REPLICATION_CONFIGURATION], - true - ); - useEffect(() => { if (loadingBucket) { setLoadingReplication(true); @@ -182,7 +175,11 @@ const BucketReplicationPanel = ({ Replication - {displayAddReplicationRules && ( + - )} + - + + + { if (loadingBucket) { setBucketLoading(true); @@ -576,34 +521,45 @@ const BucketSummary = ({ - {displayGetBucketPolicy && ( + - )} + {distributedSetup && ( - {displayGetReplicationConfiguration && ( + - )} - {displayGetBucketObjectLockConfiguration && ( + + - )} + )} - {displayGetBucketEncryptionConfiguration && ( + - )} +
Access Policy: - + +
Replication: @@ -612,16 +568,24 @@ const BucketSummary = ({
Object Locking: {!hasObjectLocking ? "Disabled" : "Enabled"}
Encryption: @@ -632,20 +596,27 @@ const BucketSummary = ({ variant="indeterminate" /> ) : ( - + + )}
Tags: @@ -705,39 +676,48 @@ const BucketSummary = ({

- {distributedSetup && displayGetBucketVersioning && ( - - - - -

Versioning

-
- - - - -
Versioning: - {loadingVersioning ? ( - - ) : ( - - - )} + 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 467baf222..505e0f585 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 @@ -94,16 +94,14 @@ import { FileZipIcon, } from "../../../../../../icons"; import ShareFile from "../ObjectDetails/ShareFile"; -import { displayComponent } from "../../../../../../utils/permissions"; -import { - S3_DELETE_OBJECT, - S3_LIST_BUCKET, - S3_PUT_OBJECT, -} from "../../../../../../types"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; import { setBucketDetailsLoad, setBucketInfo } from "../../../actions"; import { AppState } from "../../../../../../store"; import PageLayout from "../../../../Common/Layout/PageLayout"; import BoxIconButton from "../../../../Common/BoxIconButton/BoxIconButton"; +import SecureComponent, { + hasPermission, +} from "../../../../../../common/SecureComponent/SecureComponent"; const commonIcon = { backgroundRepeat: "no-repeat", @@ -291,16 +289,12 @@ const ListObjects = ({ const fileUpload = useRef(null); - const displayPutObject = displayComponent(bucketInfo?.allowedActions, [ - S3_PUT_OBJECT, + const displayDeleteObject = hasPermission(bucketName, [ + IAM_SCOPES.S3_DELETE_OBJECT, ]); - const displayDeleteObject = displayComponent(bucketInfo?.allowedActions, [ - S3_DELETE_OBJECT, - ]); - - const displayListObjects = displayComponent(bucketInfo?.allowedActions, [ - S3_LIST_BUCKET, + const displayListObjects = hasPermission(bucketName, [ + IAM_SCOPES.S3_LIST_BUCKET, ]); const updateMessage = () => { @@ -339,11 +333,7 @@ const ListObjects = ({ }, 1000); useEffect(() => { - if ( - loadingVersioning && - bucketInfo?.allowedActions && - bucketInfo?.name === bucketName - ) { + if (loadingVersioning) { if (displayListObjects) { api .invoke("GET", `/api/v1/buckets/${bucketName}/versioning`) @@ -359,14 +349,7 @@ const ListObjects = ({ setLoadingVersioning(false); } } - }, [ - bucketName, - loadingVersioning, - setErrorSnackMessage, - bucketInfo?.allowedActions, - bucketInfo?.name, - displayListObjects, - ]); + }, [bucketName, loadingVersioning, setErrorSnackMessage, displayListObjects]); // Rewind useEffect(() => { @@ -423,11 +406,7 @@ const ListObjects = ({ }, [internalPaths]); useEffect(() => { - if ( - loading && - bucketInfo?.allowedActions && - bucketInfo?.name === bucketName - ) { + if (loading) { if (displayListObjects) { let pathPrefix = ""; if (internalPaths) { @@ -1085,44 +1064,45 @@ const ListObjects = ({ } actions={ - {displayPutObject && ( - - { - setCreateFolderOpen(true); - }} - 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} - /> - - )} + + { + setCreateFolderOpen(true); + }} + 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} + /> + - {displayListObjects && ( + - )} - - {displayDeleteObject && ( + + - )} +
- + + + 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 bbd0bcfc6..92104210f 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 @@ -63,18 +63,7 @@ import { setSnackBarMessage, } from "../../../../../../actions"; 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_PUT_OBJECT_LEGAL_HOLD, - S3_PUT_OBJECT_RETENTION, - S3_PUT_OBJECT_TAGGING, -} from "../../../../../../types"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; import SetRetention from "./SetRetention"; import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs"; import DeleteObject from "../ListObjects/DeleteObject"; @@ -91,6 +80,7 @@ import PageLayout from "../../../../Common/Layout/PageLayout"; import VerticalTabs from "../../../../Common/VerticalTabs/VerticalTabs"; import BoxIconButton from "../../../../Common/BoxIconButton/BoxIconButton"; import { RecoverIcon } from "../../../../../../icons"; +import SecureComponent from "../../../../../../common/SecureComponent/SecureComponent"; const styles = (theme: Theme) => createStyles({ @@ -271,8 +261,6 @@ const ObjectDetails = ({ const [deleteOpen, setDeleteOpen] = useState(false); const [metadataLoad, setMetadataLoad] = useState(true); const [metadata, setMetadata] = useState({}); - const [loadingBucket, setLoadingBucket] = useState(false); - const [bucketInfo, setBucketInfo] = useState(null); const [restoreVersionOpen, setRestoreVersionOpen] = useState(false); const [restoreVersion, setRestoreVersion] = useState(""); @@ -288,64 +276,6 @@ 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 @@ -682,7 +612,11 @@ const ObjectDetails = ({ )} - {displayDeleteObject && ( + - )} +
} /> @@ -711,80 +645,105 @@ const ObjectDetails = ({

Details


- {(displayObjectLegalHold || - displayObjectRetention || - displayObjectTag) && ( - - - - {displayObjectLegalHold && ( - - - + + + +
- Legal Hold: - - {actualInfo.version_id && - actualInfo.version_id !== "null" ? ( - - {actualInfo.legal_hold_status - ? actualInfo.legal_hold_status.toLowerCase() - : "Off"} - {displayEditObjectLegalHold && ( - { - setLegalholdOpen(true); + + + + + + + + + + + + + + + + + + + - - )} - {displayObjectRetention && ( - - - - - )} - {displayObjectTag && ( - - - - - )} - -
Legal Hold: + {actualInfo.version_id && + actualInfo.version_id !== "null" ? ( + + {actualInfo.legal_hold_status + ? actualInfo.legal_hold_status.toLowerCase() + : "Off"} + + { + 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 ( + - - - )} - - ) : ( - "Disabled" - )} -
Retention: - {actualInfo.retention_mode - ? actualInfo.retention_mode.toLowerCase() - : "None"} - {displayEditObjectRetention && ( - { - openRetentionModal(); - }} - > - - - )} -
Tags: - {tagKeys && - tagKeys.map((tagKey, index) => { - const tag = get( - actualInfo, - `tags.${tagKey}`, - "" - ); - if (tag !== "") { - return displayRemoveObjectTagging ? ( - ) : ( - - ); - } - 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 2d6405ab0..e91d05d82 100644 --- a/portal-ui/src/screens/Console/Buckets/types.tsx +++ b/portal-ui/src/screens/Console/Buckets/types.tsx @@ -25,7 +25,6 @@ export interface Bucket { size?: number; objects?: number; rw_access?: RwAccess; - allowedActions?: string[]; manage: boolean; details?: Details; } @@ -42,8 +41,6 @@ export interface Details { export interface BucketInfo { name: string; access: string; - allowedActions?: string[]; - manage?: boolean; } export interface BucketList { diff --git a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx index 6b970379b..aeb3e20e6 100644 --- a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx @@ -533,12 +533,21 @@ const TableWrapper = ({ const clickAction = (rowItem: any) => { if (findView) { const valueClick = findView.sendOnlyId ? rowItem[idField] : rowItem; - if (findView.to) { + + let disabled = false; + + if (findView.disableButtonFunction) { + if (findView.disableButtonFunction(valueClick)) { + disabled = true; + } + } + + if (findView.to && !disabled) { history.push(`${findView.to}/${valueClick}`); return; } - if (findView.onClick) { + if (findView.onClick && !disabled) { findView.onClick(valueClick); } } diff --git a/portal-ui/src/screens/Console/reducer.ts b/portal-ui/src/screens/Console/reducer.ts index 2924e9e7e..1af137ad5 100644 --- a/portal-ui/src/screens/Console/reducer.ts +++ b/portal-ui/src/screens/Console/reducer.ts @@ -28,10 +28,7 @@ const initialState: ConsoleState = { pages: [], features: [], distributedMode: false, - policy: { - version: "", - statement: [], - }, + permissions: {}, }, }; diff --git a/portal-ui/src/screens/Console/types.ts b/portal-ui/src/screens/Console/types.ts index 5e4c40673..51ab2d0b4 100644 --- a/portal-ui/src/screens/Console/types.ts +++ b/portal-ui/src/screens/Console/types.ts @@ -14,15 +14,8 @@ // 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 ISessionPermissions { + [key: string]: string[]; } export interface ISessionResponse { @@ -31,5 +24,5 @@ export interface ISessionResponse { features: string[]; operator: boolean; distributedMode: boolean; - policy: ISessionPolicy; + permissions: ISessionPermissions; } diff --git a/portal-ui/src/store.ts b/portal-ui/src/store.ts index 0566eddb0..a4a8418b6 100644 --- a/portal-ui/src/store.ts +++ b/portal-ui/src/store.ts @@ -52,6 +52,11 @@ const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; export type AppState = ReturnType; +export const store = createStore( + globalReducer, + composeEnhancers(applyMiddleware(thunk)) +); + export default function configureStore() { - return createStore(globalReducer, composeEnhancers(applyMiddleware(thunk))); + return store; } diff --git a/portal-ui/src/types.ts b/portal-ui/src/types.ts index d01ff42f8..08a0718a1 100644 --- a/portal-ui/src/types.ts +++ b/portal-ui/src/types.ts @@ -122,55 +122,3 @@ 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 deleted file mode 100644 index 6406c5e10..000000000 --- a/portal-ui/src/utils/permissions.ts +++ /dev/null @@ -1,44 +0,0 @@ -// 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 } 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/admin_remote_buckets.go b/restapi/admin_remote_buckets.go index 20f05e0ac..300cde76f 100644 --- a/restapi/admin_remote_buckets.go +++ b/restapi/admin_remote_buckets.go @@ -458,7 +458,7 @@ func listExternalBucketsResponse(params user_api.ListExternalBucketsParams) (*mo // create a minioClient interface implementation // defining the client to be used remoteClient := AdminClient{Client: remoteAdmin} - buckets, err := getAccountBuckets(ctx, remoteClient, *params.Body.AccessKey) + buckets, err := getAccountBuckets(ctx, remoteClient) if err != nil { return nil, prepareError(err) } diff --git a/restapi/embedded_spec.go b/restapi/embedded_spec.go index 43638dbf2..9aa0b7fef 100644 --- a/restapi/embedded_spec.go +++ b/restapi/embedded_spec.go @@ -3661,12 +3661,6 @@ func init() { "access": { "$ref": "#/definitions/bucketAccess" }, - "allowedActions": { - "type": "array", - "items": { - "type": "string" - } - }, "creation_date": { "type": "string" }, @@ -3709,9 +3703,6 @@ func init() { } } }, - "manage": { - "type": "boolean" - }, "name": { "type": "string", "minLength": 3 @@ -5204,9 +5195,14 @@ func init() { "type": "string" } }, - "policy": { + "permissions": { "type": "object", - "$ref": "#/definitions/iamPolicy" + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } }, "status": { "type": "string", @@ -9482,12 +9478,6 @@ func init() { "access": { "$ref": "#/definitions/bucketAccess" }, - "allowedActions": { - "type": "array", - "items": { - "type": "string" - } - }, "creation_date": { "type": "string" }, @@ -9530,9 +9520,6 @@ func init() { } } }, - "manage": { - "type": "boolean" - }, "name": { "type": "string", "minLength": 3 @@ -11025,9 +11012,14 @@ func init() { "type": "string" } }, - "policy": { + "permissions": { "type": "object", - "$ref": "#/definitions/iamPolicy" + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } }, "status": { "type": "string", diff --git a/restapi/user_buckets.go b/restapi/user_buckets.go index d1ea0f961..5db46e5a3 100644 --- a/restapi/user_buckets.go +++ b/restapi/user_buckets.go @@ -24,10 +24,6 @@ import ( "strings" "time" - "github.com/minio/pkg/bucket/policy/condition" - - "github.com/minio/console/pkg/acl" - "github.com/minio/mc/cmd" "github.com/minio/mc/pkg/probe" "github.com/minio/minio-go/v7" @@ -290,25 +286,13 @@ func getBucketVersionedResponse(session *models.Principal, bucketName string) (* } // getAccountBuckets fetches a list of all buckets allowed to that particular client from MinIO Servers -func getAccountBuckets(ctx context.Context, client MinioAdmin, accessKey string) ([]*models.Bucket, error) { +func getAccountBuckets(ctx context.Context, client MinioAdmin) ([]*models.Bucket, error) { info, err := client.AccountInfo(ctx) if err != nil { return []*models.Bucket{}, err } - policyInfo, err := getAccountPolicy(ctx, client) - if err != nil { - return nil, err - } var bucketInfos []*models.Bucket for _, bucket := range info.Buckets { - var bucketAdminRole bool - conditionValues := map[string][]string{ - condition.AWSUsername.Name(): {accessKey}, - } - bucketActions := policyInfo.IsAllowedActions(bucket.Name, "", conditionValues) - bucketAdminRoleActions := bucketActions.Intersection(acl.BucketAdminRole) - bucketAdminRole = len(bucketAdminRoleActions) > 0 - bucketElem := &models.Bucket{ CreationDate: bucket.Created.Format(time.RFC3339), Details: &models.BucketDetails{ @@ -321,7 +305,6 @@ func getAccountBuckets(ctx context.Context, client MinioAdmin, accessKey string) Name: swag.String(bucket.Name), Objects: int64(bucket.Objects), Size: int64(bucket.Size), - Manage: bucketAdminRole, } if bucket.Details != nil { @@ -358,7 +341,7 @@ func getListBucketsResponse(session *models.Principal) (*models.ListBucketsRespo // create a minioClient interface implementation // defining the client to be used adminClient := AdminClient{Client: mAdmin} - buckets, err := getAccountBuckets(ctx, adminClient, session.AccountAccessKey) + buckets, err := getAccountBuckets(ctx, adminClient) if err != nil { return nil, prepareError(err) } @@ -493,21 +476,12 @@ func getBucketSetPolicyResponse(session *models.Principal, bucketName string, re // create a minioClient interface implementation // 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(ctx, minioClient, adminClient, bucketName, session.AccountAccessKey) + bucket, err := getBucketInfo(ctx, minioClient, bucketName) if err != nil { return nil, prepareError(err) } @@ -566,29 +540,7 @@ func getDeleteBucketResponse(session *models.Principal, params user_api.DeleteBu } // getBucketInfo return bucket information including name, policy access, size and creation date -func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdmin, bucketName string, accountName string) (*models.Bucket, error) { - // Get Account Policy - policyInfo, err := getAccountPolicy(ctx, adminClient) - if err != nil { - return nil, err - } - - var bucketAdminRole bool - // Retrieve list of allowed bucketActionsArray on the bucket - // TODO: Add all the possible variables - conditionValues := map[string][]string{ - condition.AWSUsername.Name(): {accountName}, - } - - bucketActions := policyInfo.IsAllowedActions(bucketName, "", conditionValues) - // 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)) - } - +func getBucketInfo(ctx context.Context, client MinioClient, bucketName string) (*models.Bucket, error) { var bucketAccess models.BucketAccess policyStr, err := client.getBucketPolicy(context.Background(), bucketName) if err != nil { @@ -611,29 +563,21 @@ func getBucketInfo(ctx context.Context, client MinioClient, adminClient MinioAdm bucketAccess = models.BucketAccessCUSTOM } bucketTags, err := client.GetBucketTagging(ctx, bucketName) - var bucket *models.Bucket - if err == nil && bucketTags != nil { - bucket = &models.Bucket{ - Name: &bucketName, - Access: &bucketAccess, - CreationDate: "", // to be implemented - Size: 0, // to be implemented - AllowedActions: bucketActionsArray, - Manage: bucketAdminRole, - Details: &models.BucketDetails{Tags: bucketTags.ToMap()}, - } - } else { - bucket = &models.Bucket{ - Name: &bucketName, - Access: &bucketAccess, - CreationDate: "", // to be implemented - Size: 0, // to be implemented - AllowedActions: bucketActionsArray, - Manage: bucketAdminRole, - Details: &models.BucketDetails{}, - } + if err != nil { + // we can tolerate this error + LogError("error getting bucket tags: %v", err) } - return bucket, nil + bucketDetails := &models.BucketDetails{} + if bucketTags != nil { + bucketDetails.Tags = bucketTags.ToMap() + } + return &models.Bucket{ + Name: &bucketName, + Access: &bucketAccess, + CreationDate: "", // to be implemented + Size: 0, // to be implemented + Details: bucketDetails, + }, nil } // getBucketInfoResponse calls getBucketInfo() to get the bucket's info @@ -647,16 +591,7 @@ func getBucketInfoResponse(session *models.Principal, params user_api.BucketInfo // create a minioClient interface implementation // 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} - - bucket, err := getBucketInfo(ctx, minioClient, adminClient, params.Name, session.AccountAccessKey) + bucket, err := getBucketInfo(ctx, minioClient, params.Name) if err != nil { return nil, prepareError(err) } diff --git a/restapi/user_buckets_test.go b/restapi/user_buckets_test.go index 29c53ba32..26127412a 100644 --- a/restapi/user_buckets_test.go +++ b/restapi/user_buckets_test.go @@ -180,7 +180,7 @@ func TestListBucket(t *testing.T) { // get list buckets response this response should have Name, CreationDate, Size and Access // as part of of each bucket function := "getaAcountUsageInfo()" - bucketList, err := getAccountBuckets(ctx, adminClient, "") + bucketList, err := getAccountBuckets(ctx, adminClient) if err != nil { t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) } @@ -197,7 +197,7 @@ func TestListBucket(t *testing.T) { minioAccountInfoMock = func(ctx context.Context) (madmin.AccountInfo, error) { return madmin.AccountInfo{}, errors.New("error") } - _, err = getAccountBuckets(ctx, adminClient, "") + _, err = getAccountBuckets(ctx, adminClient) if assert.Error(err) { assert.Equal("error", err.Error()) } @@ -257,7 +257,6 @@ func TestBucketInfo(t *testing.T) { // mock minIO client minClient := minioClientMock{} ctx := context.Background() - adminClient := adminClientMock{} function := "getBucketInfo()" // Test-1: getBucketInfo() get a bucket with PRIVATE access @@ -308,7 +307,7 @@ func TestBucketInfo(t *testing.T) { return mockBucketList, nil } - bucketInfo, err := getBucketInfo(ctx, minClient, adminClient, bucketToSet, "user1") + bucketInfo, err := getBucketInfo(ctx, minClient, bucketToSet) if err != nil { t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) } @@ -330,7 +329,7 @@ func TestBucketInfo(t *testing.T) { CreationDate: "", // to be implemented Size: 0, // to be implemented } - bucketInfo, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet, "bucket1") + bucketInfo, err = getBucketInfo(ctx, minClient, bucketToSet) if err != nil { t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) } @@ -352,7 +351,7 @@ func TestBucketInfo(t *testing.T) { CreationDate: "", // to be implemented Size: 0, // to be implemented } - bucketInfo, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet, "bucket1") + bucketInfo, err = getBucketInfo(ctx, minClient, bucketToSet) if err != nil { t.Errorf("Failed on %s:, error occurred: %s", function, err.Error()) } @@ -373,7 +372,7 @@ func TestBucketInfo(t *testing.T) { CreationDate: "", // to be implemented Size: 0, // to be implemented } - _, err = getBucketInfo(ctx, minClient, adminClient, bucketToSet, "bucket1") + _, err = getBucketInfo(ctx, minClient, bucketToSet) if assert.Error(err) { assert.Equal("invalid character 'p' looking for beginning of value", err.Error()) } diff --git a/restapi/user_session.go b/restapi/user_session.go index 35dbf9208..5d3f12d66 100644 --- a/restapi/user_session.go +++ b/restapi/user_session.go @@ -23,6 +23,10 @@ import ( "net/url" "time" + "github.com/minio/pkg/bucket/policy/condition" + + minioIAMPolicy "github.com/minio/pkg/iam/policy" + "github.com/go-openapi/runtime/middleware" "github.com/minio/console/models" "github.com/minio/console/pkg/acl" @@ -91,14 +95,82 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo if err != nil { return nil, prepareError(err, errorGenericInvalidSession) } - // by default every user starts with an empty array of available actions + // by default every user starts with an empty array of available val // therefore we would have access only to pages that doesn't require any privilege // ie: service-account page var actions []string - // if a policy is assigned to this user we parse the actions from there + // if a policy is assigned to this user we parse the val from there if policy != nil { actions = acl.GetActionsStringFromPolicy(policy) } + + // This actions will be global, meaning has to be attached to all resources + conditionValues := map[string][]string{ + condition.AWSUsername.Name(): {session.AccountAccessKey}, + } + defaultActions := policy.IsAllowedActions("", "", conditionValues) + consoleResourceName := "console-ui" + permissions := map[string]minioIAMPolicy.ActionSet{ + consoleResourceName: defaultActions, + } + deniedActions := map[string]minioIAMPolicy.ActionSet{} + for _, statement := range policy.Statements { + for _, resource := range statement.Resources.ToSlice() { + resourceName := resource.String() + statementActions := statement.Actions.ToSlice() + if statement.Effect == "Allow" { + // check if val are denied before adding them to the map + var allowedActions []minioIAMPolicy.Action + if dActions, ok := deniedActions[resourceName]; ok { + for _, action := range statementActions { + if len(dActions.Intersection(minioIAMPolicy.NewActionSet(action))) == 0 { + // It's ok to allow this action + allowedActions = append(allowedActions, action) + } + } + } else { + allowedActions = statementActions + } + + // Add validated actions + if resourceActions, ok := permissions[resourceName]; ok { + mergedActions := append(resourceActions.ToSlice(), allowedActions...) + permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...) + } else { + mergedActions := append(defaultActions.ToSlice(), allowedActions...) + permissions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...) + } + } else { + // Add new banned actions to the map + if resourceActions, ok := deniedActions[resourceName]; ok { + mergedActions := append(resourceActions.ToSlice(), statementActions...) + deniedActions[resourceName] = minioIAMPolicy.NewActionSet(mergedActions...) + } else { + deniedActions[resourceName] = statement.Actions + } + // Remove existing val from key if necessary + if currentResourceActions, ok := permissions[resourceName]; ok { + var newAllowedActions []minioIAMPolicy.Action + for _, action := range currentResourceActions.ToSlice() { + if len(deniedActions[resourceName].Intersection(minioIAMPolicy.NewActionSet(action))) == 0 { + // It's ok to allow this action + newAllowedActions = append(newAllowedActions, action) + } + } + permissions[resourceName] = minioIAMPolicy.NewActionSet(newAllowedActions...) + } + } + } + } + resourcePermissions := map[string][]string{} + for key, val := range permissions { + var resourceActions []string + for _, action := range val.ToSlice() { + resourceActions = append(resourceActions, string(action)) + } + resourcePermissions[key] = resourceActions + + } rawPolicy, err := json.Marshal(policy) if err != nil { return nil, prepareError(err, errorGenericInvalidSession) @@ -114,7 +186,7 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo Status: models.SessionResponseStatusOk, Operator: false, DistributedMode: isErasureMode(), - Policy: sessionPolicy, + Permissions: resourcePermissions, } return sessionResp, nil } diff --git a/swagger-console.yml b/swagger-console.yml index b74ddbb28..2a4f89b2a 100644 --- a/swagger-console.yml +++ b/swagger-console.yml @@ -2363,8 +2363,6 @@ definitions: required: - name properties: - manage: - type: boolean name: type: string minLength: 3 @@ -2411,10 +2409,6 @@ definitions: - hard creation_date: type: string - allowedActions: - type: array - items: - type: string bucketEncryptionRequest: type: object @@ -3128,9 +3122,12 @@ definitions: type: boolean distributedMode: type: boolean - policy: + permissions: type: object - $ref: "#/definitions/iamPolicy" + additionalProperties: + type: array + items: + type: string widgetResult: type: object