Application routing now uses SecureComponent and hasPermission function (#1288)

- Some menu options were not showing even if the user has access to
  perform the operations (IAM Policies)
- Deleted unecessary backend endpoints.go logic, instead using
  SecureComponent to validate application routes and Menu options
  rendering
- All the logic related to routes and permissions is now in the
  permissions.ts file
- Added SecureComponent to List Users page
- Separated Menu options and routing logic for AdminConsole and
  OperatorConsole
- Tools are hidden if user don't have access to them or MinIO is running
  in fs mode (heal, audit log, etc
- Hide change-password button if user don't have access
- Hide create user button if user don't have access
- fixed some bugs when ldap/oidc is enabled

Signed-off-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Lenin Alevski
2021-12-13 22:37:22 -08:00
committed by GitHub
parent db5ae3e09f
commit 3b2c740fe0
47 changed files with 1331 additions and 1482 deletions

View File

@@ -37,15 +37,9 @@ import (
// swagger:model operatorSessionResponse
type OperatorSessionResponse struct {
// features
Features []string `json:"features"`
// operator
Operator bool `json:"operator,omitempty"`
// pages
Pages []string `json:"pages"`
// status
// Enum: [ok]
Status string `json:"status,omitempty"`

View File

@@ -46,9 +46,6 @@ type SessionResponse struct {
// operator
Operator bool `json:"operator,omitempty"`
// pages
Pages []string `json:"pages"`
// permissions
Permissions map[string][]string `json:"permissions,omitempty"`

View File

@@ -2296,21 +2296,9 @@ func init() {
"operatorSessionResponse": {
"type": "object",
"properties": {
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"operator": {
"type": "boolean"
},
"pages": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string",
"enum": [
@@ -6103,21 +6091,9 @@ func init() {
"operatorSessionResponse": {
"type": "object",
"properties": {
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"operator": {
"type": "boolean"
},
"pages": {
"type": "array",
"items": {
"type": "string"
}
},
"status": {
"type": "string",
"enum": [

View File

@@ -21,7 +21,6 @@ import (
"github.com/minio/console/models"
"github.com/minio/console/operatorapi/operations"
"github.com/minio/console/operatorapi/operations/user_api"
"github.com/minio/console/pkg/acl"
)
func registerSessionHandlers(api *operations.OperatorAPI) {
@@ -42,16 +41,8 @@ func getSessionResponse(session *models.Principal) (*models.OperatorSessionRespo
return nil, prepareError(errorGenericInvalidSession)
}
sessionResp := &models.OperatorSessionResponse{
Pages: acl.GetAuthorizedEndpoints([]string{}),
Features: getListOfEnabledFeatures(),
Status: models.OperatorSessionResponseStatusOk,
Operator: true,
}
return sessionResp, nil
}
// getListOfEnabledFeatures returns a list of features
func getListOfEnabledFeatures() []string {
var features []string
return features
}

View File

@@ -1,32 +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 <http://www.gnu.org/licenses/>.
package acl
import (
"strings"
"github.com/minio/pkg/env"
)
// GetOperatorMode gets Console Operator mode status set on env variable or default one
func GetOperatorMode() bool {
return strings.ToLower(env.Get(consoleOperatorMode, "off")) == "on"
}
func GetLDAPEnabled() bool {
return strings.ToLower(env.Get(ConsoleLDAPEnabled, "off")) == "on"
}

View File

@@ -1,23 +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 <http://www.gnu.org/licenses/>.
package acl
const (
consoleOperatorMode = "CONSOLE_OPERATOR_MODE"
// const for ldap configuration
ConsoleLDAPEnabled = "CONSOLE_LDAP_ENABLED"
)

View File

@@ -1,469 +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 <http://www.gnu.org/licenses/>.
package acl
import (
iampolicy "github.com/minio/pkg/iam/policy"
)
// endpoints definition
var (
configuration = "/settings"
configurationItem = "/settings/:option"
notificationEndpoints = "/notification-endpoints"
notificationEndpointsAddAny = "/notification-endpoints/add/:service"
notificationEndpointsAdd = "/notification-endpoints/add"
tiers = "/tiers"
tiersAddAny = "/tiers/add/:service"
tiersAdd = "/tiers/add"
users = "/users"
usersDetail = "/users/:userName+"
groups = "/groups"
groupsDetails = "/groups/:groupName+"
iamPolicies = "/policies"
policiesDetail = "/policies/*"
dashboard = "/dashboard"
metrics = "/metrics"
profiling = "/profiling"
addBucket = "/add-bucket"
buckets = "/buckets"
bucketsGeneral = "/buckets/*"
bucketsAdmin = "/buckets/:bucketName/admin/*"
bucketsAdminMain = "/buckets/:bucketName/admin"
bucketsBrowserMenu = "/buckets"
bucketsBrowserList = "/buckets/*"
bucketsBrowser = "/buckets/:bucketName/browse/*"
bucketsBrowserMain = "/buckets/:bucketName/browse"
serviceAccounts = "/account"
changePassword = "/account/change-password"
tenants = "/tenants"
tenantsAdd = "/tenants/add"
tenantsAddSub = "/tenants/add/*"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
tenantHop = "/namespaces/:tenantNamespace/tenants/:tenantName/hop"
podsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName"
tenantsDetailSummary = "/namespaces/:tenantNamespace/tenants/:tenantName/summary"
tenantsDetailMetrics = "/namespaces/:tenantNamespace/tenants/:tenantName/metrics"
tenantsDetailPods = "/namespaces/:tenantNamespace/tenants/:tenantName/pods"
tenantsDetailPools = "/namespaces/:tenantNamespace/tenants/:tenantName/pools"
tenantsDetailVolumes = "/namespaces/:tenantNamespace/tenants/:tenantName/volumes"
tenantsDetailLicense = "/namespaces/:tenantNamespace/tenants/:tenantName/license"
tenantsDetailSecurity = "/namespaces/:tenantNamespace/tenants/:tenantName/security"
storage = "/storage"
storageVolumes = "/storage/volumes"
storageDrives = "/storage/drives"
remoteBuckets = "/remote-buckets"
replication = "/replication"
license = "/license"
watch = "/tools/watch"
heal = "/tools/heal"
trace = "/tools/trace"
tools = "/tools"
logs = "/tools/logs"
auditLogs = "/tools/audit-logs"
speedtest = "/tools/speedtest"
healthInfo = "/tools/diagnostics"
)
type ConfigurationActionSet struct {
actionTypes iampolicy.ActionSet
actions iampolicy.ActionSet
}
// configurationActionSet contains the list of admin actions required for this endpoint to work
var configurationActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
// dashboardActionSet contains the list of admin actions required for this endpoint to work
var dashboardActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ServerInfoAdminAction,
),
}
// groupsActionSet contains the list of admin actions required for this endpoint to work
var groupsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListGroupsAdminAction,
iampolicy.AddUserToGroupAdminAction,
//iampolicy.GetGroupAdminAction,
iampolicy.EnableGroupAdminAction,
iampolicy.DisableGroupAdminAction,
),
}
// iamPoliciesActionSet contains the list of admin actions required for this endpoint to work
var iamPoliciesActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.GetPolicyAdminAction,
iampolicy.DeletePolicyAdminAction,
iampolicy.CreatePolicyAdminAction,
iampolicy.AttachPolicyAdminAction,
iampolicy.ListUserPoliciesAdminAction,
),
}
// profilingActionSet contains the list of admin actions required for this endpoint to work
var profilingActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ProfilingAdminAction,
),
}
// usersActionSet contains the list of admin actions required for this endpoint to work
var usersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListUsersAdminAction,
iampolicy.CreateUserAdminAction,
iampolicy.DeleteUserAdminAction,
iampolicy.GetUserAdminAction,
iampolicy.EnableUserAdminAction,
iampolicy.DisableUserAdminAction,
),
}
// bucketsActionSet contains the list of admin actions required for this endpoint to work
var bucketsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllActions,
),
actions: iampolicy.NewActionSet(
// Read access to buckets
iampolicy.ListMultipartUploadPartsAction,
iampolicy.ListBucketMultipartUploadsAction,
iampolicy.ListBucketAction,
iampolicy.HeadBucketAction,
iampolicy.GetObjectAction,
iampolicy.GetBucketLocationAction,
// Write access to buckets
iampolicy.AbortMultipartUploadAction,
iampolicy.CreateBucketAction,
iampolicy.PutObjectAction,
iampolicy.DeleteObjectAction,
iampolicy.DeleteBucketAction,
// Assign bucket policies
iampolicy.PutBucketPolicyAction,
iampolicy.DeleteBucketPolicyAction,
iampolicy.GetBucketPolicyAction,
),
}
// serviceAccountsActionSet no actions needed for this module to work
var serviceAccountsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// changePasswordActionSet requires admin:CreateUser policy permission
var changePasswordActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// tenantsActionSet temporally no actions needed for tenants sections to work
var tenantsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
var storageActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
var remoteBucketsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
var replicationActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConfigUpdateAdminAction,
),
}
// objectBrowserActionSet no actions needed for this module to work
var objectBrowserActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// licenseActionSet no actions needed for this module to work
var licenseActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// watchActionSet contains the list of admin actions required for this endpoint to work
var watchActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ListenBucketNotificationAction,
),
}
// healActionSet contains the list of admin actions required for this endpoint to work
var healActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealAdminAction,
),
}
// logsActionSet contains the list of admin actions required for this endpoint to work
var logsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConsoleLogAdminAction,
),
}
// toolsActionSet contains the list of admin actions required for this endpoint to work
var toolsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.ConsoleLogAdminAction,
),
}
// traceActionSet contains the list of admin actions required for this endpoint to work
var traceActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.TraceAdminAction,
),
}
// healthInfoActionSet contains the list of admin actions required for this endpoint to work
var healthInfoActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealthInfoAdminAction,
),
}
// logsActionSet contains the list of admin actions required for this endpoint to work
var speedtestActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(
iampolicy.AllAdminActions,
),
actions: iampolicy.NewActionSet(
iampolicy.HealthInfoAdminAction,
),
}
var displayRules = map[string]func() bool{
// disable users page if LDAP is enabled
users: func() bool {
return !GetLDAPEnabled()
},
// disable groups page if LDAP is enabled
groups: func() bool {
return !GetLDAPEnabled()
},
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
configurationItem: configurationActionSet,
notificationEndpoints: configurationActionSet,
notificationEndpointsAdd: configurationActionSet,
notificationEndpointsAddAny: configurationActionSet,
tiers: configurationActionSet,
tiersAdd: configurationActionSet,
tiersAddAny: configurationActionSet,
users: usersActionSet,
usersDetail: usersActionSet,
groups: groupsActionSet,
groupsDetails: groupsActionSet,
iamPolicies: iamPoliciesActionSet,
policiesDetail: iamPoliciesActionSet,
dashboard: dashboardActionSet,
metrics: dashboardActionSet,
profiling: profilingActionSet,
addBucket: bucketsActionSet,
buckets: bucketsActionSet,
bucketsGeneral: bucketsActionSet,
bucketsAdmin: bucketsActionSet,
bucketsAdminMain: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
changePassword: changePasswordActionSet,
remoteBuckets: remoteBucketsActionSet,
replication: replicationActionSet,
bucketsBrowser: objectBrowserActionSet,
bucketsBrowserMenu: objectBrowserActionSet,
bucketsBrowserList: objectBrowserActionSet,
bucketsBrowserMain: objectBrowserActionSet,
license: licenseActionSet,
watch: watchActionSet,
heal: healActionSet,
trace: traceActionSet,
logs: logsActionSet,
auditLogs: logsActionSet,
tools: toolsActionSet,
healthInfo: healthInfoActionSet,
speedtest: speedtestActionSet,
}
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode
var operatorRules = map[string]ConfigurationActionSet{
tenants: tenantsActionSet,
tenantsAdd: tenantsActionSet,
tenantsAddSub: tenantsActionSet,
tenantsDetail: tenantsActionSet,
tenantHop: tenantsActionSet,
tenantsDetailSummary: tenantsActionSet,
tenantsDetailMetrics: tenantsActionSet,
tenantsDetailPods: tenantsActionSet,
tenantsDetailPools: tenantsActionSet,
tenantsDetailVolumes: tenantsActionSet,
tenantsDetailLicense: tenantsActionSet,
tenantsDetailSecurity: tenantsActionSet,
podsDetail: tenantsActionSet,
storage: storageActionSet,
storageDrives: storageActionSet,
storageVolumes: storageActionSet,
license: licenseActionSet,
}
// operatorOnly ENV variable
var operatorOnly = GetOperatorMode()
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format
//
// ie:
// {
// "Version": "2012-10-17",
// "Statement": [{
// "Action": [
// "admin:ServerInfo",
// "admin:CreatePolicy",
// "admin:GetUser"
// ],
// ...
// },
// {
// "Action": [
// "s3:ListenBucketNotification",
// "s3:PutBucketNotification"
// ],
// ...
// }
// ]
// }
// Will produce an array like: ["admin:ServerInfo", "admin:CreatePolicy", "admin:GetUser", "s3:ListenBucketNotification", "s3:PutBucketNotification"]\
func GetActionsStringFromPolicy(policy *iampolicy.Policy) []string {
var actions []string
for _, statement := range policy.Statements {
// We only care about allowed actions
if statement.Effect.IsAllowed(true) {
for _, action := range statement.Actions.ToSlice() {
actions = append(actions, string(action))
}
}
}
return actions
}
// actionsStringToActionSet convert a given string array to iampolicy.ActionSet structure
// this avoids ending with duplicate actions
func actionsStringToActionSet(actions []string) iampolicy.ActionSet {
actionsSet := iampolicy.ActionSet{}
for _, action := range actions {
actionsSet.Add(iampolicy.Action(action))
}
return actionsSet
}
// GetAuthorizedEndpoints return a list of allowed endpoint based on a provided *iampolicy.Policy
// ie: pages the user should have access based on his current privileges
func GetAuthorizedEndpoints(actions []string) []string {
rangeTake := endpointRules
if operatorOnly {
rangeTake = operatorRules
}
// Prepare new ActionSet structure that will hold all the user actions
userAllowedAction := actionsStringToActionSet(actions)
var allowedEndpoints []string
for endpoint, rules := range rangeTake {
// check if display rule exists for this endpoint, this will control
// what user sees on the console UI
if rule, ok := displayRules[endpoint]; ok {
if rule != nil && !rule() {
continue
}
}
// check if user policy matches s3:* or admin:* typesIntersection
endpointActionTypes := rules.actionTypes
typesIntersection := endpointActionTypes.Intersection(userAllowedAction)
if len(typesIntersection) == len(endpointActionTypes.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
continue
}
// check if user policy matches explicitly defined endpoint required actions
endpointRequiredActions := rules.actions
actionsIntersection := endpointRequiredActions.Intersection(userAllowedAction)
if len(actionsIntersection) == len(endpointRequiredActions.ToSlice()) {
allowedEndpoints = append(allowedEndpoints, endpoint)
}
}
return allowedEndpoints
}

View File

@@ -1,118 +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 <http://www.gnu.org/licenses/>.
package acl
import (
"reflect"
"testing"
)
type args struct {
actions []string
}
type endpoint struct {
name string
args args
want int
}
func validateEndpoints(t *testing.T, configs []endpoint) {
for _, tt := range configs {
t.Run(tt.name, func(t *testing.T) {
if got := GetAuthorizedEndpoints(tt.args.actions); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetAuthorizedEndpoints() = %v, want %v", len(got), tt.want)
}
})
}
}
func TestGetAuthorizedEndpoints(t *testing.T) {
tests := []endpoint{
{
name: "dashboard endpoint",
args: args{
[]string{"admin:ServerInfo"},
},
want: 9,
},
{
name: "policies endpoint",
args: args{
[]string{
"admin:CreatePolicy",
"admin:DeletePolicy",
"admin:GetPolicy",
"admin:AttachUserOrGroupPolicy",
"admin:ListUserPolicies",
},
},
want: 9,
},
{
name: "all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 34,
},
{
name: "all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 10,
},
{
name: "all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 37,
},
{
name: "Console User - default endpoints",
args: args{
[]string{},
},
want: 7,
},
}
validateEndpoints(t, tests)
}
func TestOperatorOnlyEndpoints(t *testing.T) {
operatorOnly = true
tests := []endpoint{
{
name: "Operator Only - all admin endpoints",
args: args{},
want: 17,
},
}
validateEndpoints(t, tests)
}

View File

@@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*

View File

@@ -1,3 +1,4 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*

View File

@@ -66,10 +66,7 @@
<div id="loader-block">
<svg class="loader-svg-container" viewBox="22 22 44 44">
<circle
class="
loader-style
MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate
"
class="loader-style MuiCircularProgress-circle MuiCircularProgress-circleIndeterminate"
cx="44"
cy="44"
fill="none"

View File

@@ -19,7 +19,7 @@
// match some of the permissions)
export const hasAccessToResource = (
userPermissionsOnBucket: string[] | null | undefined,
requiredPermissions: string[],
requiredPermissions: string[] = [],
matchAll?: boolean
) => {
if (!userPermissionsOnBucket) {
@@ -44,10 +44,9 @@ export const hasAccessToResource = (
};
export const IAM_ROLES = {
viewer: "VIEWER",
editor: "EDITOR",
owner: "OWNER",
admin: "ADMIN",
BUCKET_OWNER: "BUCKET_OWNER", // upload/delete objects from the bucket
BUCKET_VIEWER: "BUCKET_VIEWER", // only view objects on the bucket
BUCKET_ADMIN: "BUCKET_ADMIN", // administrate the bucket
};
export const IAM_SCOPES = {
@@ -84,11 +83,17 @@ export const IAM_SCOPES = {
"s3:PutBucketObjectLockConfiguration",
ADMIN_GET_POLICY: "admin:GetPolicy",
ADMIN_LIST_USERS: "admin:ListUsers",
ADMIN_CREATE_USER: "admin:CreateUser",
ADMIN_DELETE_USER: "admin:DeleteUser",
ADMIN_ENABLE_USER: "admin:EnableUser",
ADMIN_DISABLE_USER: "admin:DisableUser",
ADMIN_GET_USER: "admin:GetUser",
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_SET_TIER: "admin:SetTier",
ADMIN_LIST_GROUPS: "admin:ListGroups",
S3_GET_OBJECT_VERSION_FOR_REPLICATION: "s3:GetObjectVersionForReplication",
S3_REPLICATE_TAGS: "s3:ReplicateTags",
@@ -117,20 +122,87 @@ export const IAM_SCOPES = {
ADMIN_GET_GROUP: "admin:GetGroup",
ADMIN_ENABLE_GROUP: "admin:EnableGroup",
ADMIN_DISABLE_GROUP: "admin:DisableGroup",
ADMIN_GET_USER: "admin:GetUser",
ADMIN_CREATE_POLICY: "admin:CreatePolicy",
ADMIN_DELETE_POLICY: "admin:DeletePolicy",
ADMIN_ATTACH_USER_OR_GROUP_POLICY: "admin:AttachUserOrGroupPolicy",
ADMIN_HEAL_ACTION: "admin:Heal",
ADMIN_HEALTH_ACTION: "admin:OBDInfo",
ADMIN_CONSOLE_LOG_ACTION: "admin:ConsoleLog",
ADMIN_TRACE_ACTION: "admin:ServerTrace",
ADMIN_CREATE_SERVICEACCOUNT: "admin:CreateServiceAccount",
ADMIN_UPDATE_SERVICEACCOUNT: "admin:UpdateServiceAccount",
ADMIN_REMOVE_SERVICEACCOUNT: "admin:RemoveServiceAccount",
ADMIN_LIST_SERVICEACCOUNTS: "admin:ListServiceAccounts",
ADMIN_CONFIG_UPDATE: "admin:ConfigUpdate",
ADMIN_GET_CONSOLE_LOG: "admin:ConsoleLog",
ADMIN_SERVER_TRACE: "admin:ServerTrace",
ADMIN_HEALTH_INFO: "admin:OBDInfo",
ADMIN_HEAL: "admin:Heal",
S3_ALL_ACTIONS: "s3:*",
ADMIN_ALL_ACTIONS: "admin:*",
};
export const IAM_PAGES = {
POLICIES: "/policies",
POLICIES_VIEW: "/policies/*",
DASHBOARD: "/dashboard",
METRICS: "/metrics",
ADD_BUCKETS: "/add-bucket",
BUCKETS: "/buckets",
BUCKETS_ADMIN_VIEW: "/buckets/:bucketName/admin*",
BUCKETS_BROWSE_VIEW: "/buckets/:bucketName/browse*",
TOOLS_WATCH: "/tools/watch",
TOOLS_SPEEDTEST: "/tools/speedtest",
USERS: "/users",
USERS_VIEW: "/users/:userName+",
GROUPS: "/groups",
GROUPS_VIEW: "/groups/:groupName+",
TOOLS_HEAL: "/tools/heal",
TOOLS_TRACE: "/tools/trace",
TOOLS_DIAGNOSTICS: "/tools/diagnostics",
TOOLS_LOGS: "/tools/logs",
TOOLS_AUDITLOGS: "/tools/audit-logs",
TOOLS: "/tools",
SETTINGS: "/settings",
SETTINGS_VIEW: "/settings/:option",
NOTIFICATIONS_ENDPOINTS_ADD: "/notification-endpoints/add",
NOTIFICATIONS_ENDPOINTS_ADD_SERVICE: "/notification-endpoints/add/:service",
NOTIFICATIONS_ENDPOINTS: "/notification-endpoints",
TIERS: "/tiers",
TIERS_ADD: "/tiers/add",
TIERS_ADD_SERVICE: "/tiers/add/:service",
ACCOUNT: "/account",
TENANTS: "/tenants",
TENANTS_ADD: "/tenants/add",
STORAGE: "/storage",
STORAGE_VOLUMES: "/storage/volumes",
STORAGE_DRIVES: "/storage/drives",
NAMESPACE_TENANT: "/namespaces/:tenantNamespace/tenants/:tenantName",
NAMESPACE_TENANT_HOP: "/namespaces/:tenantNamespace/tenants/:tenantName/hop",
NAMESPACE_TENANT_PODS:
"/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName",
NAMESPACE_TENANT_PODS_LIST:
"/namespaces/:tenantNamespace/tenants/:tenantName/pods",
NAMESPACE_TENANT_SUMMARY:
"/namespaces/:tenantNamespace/tenants/:tenantName/summary",
NAMESPACE_TENANT_METRICS:
"/namespaces/:tenantNamespace/tenants/:tenantName/metrics",
NAMESPACE_TENANT_POOLS:
"/namespaces/:tenantNamespace/tenants/:tenantName/pools",
NAMESPACE_TENANT_VOLUMES:
"/namespaces/:tenantNamespace/tenants/:tenantName/volumes",
NAMESPACE_TENANT_LICENSE:
"/namespaces/:tenantNamespace/tenants/:tenantName/license",
NAMESPACE_TENANT_SECURITY:
"/namespaces/:tenantNamespace/tenants/:tenantName/security",
LICENSE: "/license",
DOCUMENTATION: "/documentation",
};
// roles
export const IAM_PERMISSIONS = {
[IAM_ROLES.admin]: [
[IAM_ROLES.BUCKET_OWNER]: [
IAM_SCOPES.S3_PUT_OBJECT,
IAM_SCOPES.S3_DELETE_OBJECT,
],
[IAM_ROLES.BUCKET_VIEWER]: [IAM_SCOPES.S3_LIST_BUCKET],
[IAM_ROLES.BUCKET_ADMIN]: [
IAM_SCOPES.S3_ALL_ACTIONS,
IAM_SCOPES.ADMIN_ALL_ACTIONS,
IAM_SCOPES.S3_REPLICATE_OBJECT,
@@ -183,9 +255,111 @@ export const IAM_PERMISSIONS = {
IAM_SCOPES.ADMIN_GET_POLICY,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_USERS,
IAM_SCOPES.ADMIN_HEAL_ACTION,
IAM_SCOPES.ADMIN_HEAL,
],
};
// application pages/routes and required scopes/roles
export const IAM_PAGES_PERMISSIONS = {
[IAM_PAGES.ADD_BUCKETS]: [
IAM_SCOPES.S3_CREATE_BUCKET, // create bucket page
],
[IAM_PAGES.BUCKETS_ADMIN_VIEW]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN], // bucket admin page
],
[IAM_PAGES.BUCKETS_BROWSE_VIEW]: [
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_OWNER],
...IAM_PERMISSIONS[IAM_ROLES.BUCKET_VIEWER],
],
[IAM_PAGES.GROUPS]: [
IAM_SCOPES.ADMIN_LIST_GROUPS, // displays groups
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP, // displays create group button
],
[IAM_PAGES.GROUPS_VIEW]: [
IAM_SCOPES.ADMIN_GET_GROUP,
IAM_SCOPES.ADMIN_DISABLE_GROUP,
IAM_SCOPES.ADMIN_ENABLE_GROUP,
IAM_SCOPES.ADMIN_REMOVE_USER_FROM_GROUP,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP, // display "edit members" button in groups detail page
IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY, // display "set policy" button in groups details page
],
[IAM_PAGES.USERS]: [
IAM_SCOPES.ADMIN_LIST_USERS, // displays users
IAM_SCOPES.ADMIN_CREATE_USER, // displays create user button
],
[IAM_PAGES.USERS_VIEW]: [
IAM_SCOPES.ADMIN_GET_USER, // displays list of users
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP, // displays "add to gorups" button in user details page
IAM_SCOPES.ADMIN_ENABLE_USER,
IAM_SCOPES.ADMIN_DISABLE_USER,
IAM_SCOPES.ADMIN_DELETE_USER,
],
[IAM_PAGES.DASHBOARD]: [
IAM_SCOPES.ADMIN_SERVER_INFO, // displays dashboard information
],
[IAM_PAGES.POLICIES_VIEW]: [
IAM_SCOPES.ADMIN_DELETE_POLICY,
IAM_SCOPES.ADMIN_LIST_GROUPS,
IAM_SCOPES.ADMIN_GET_GROUP,
IAM_SCOPES.ADMIN_GET_POLICY,
IAM_SCOPES.ADMIN_CREATE_POLICY,
],
[IAM_PAGES.POLICIES]: [
IAM_SCOPES.ADMIN_LIST_USER_POLICIES, // displays policies
IAM_SCOPES.ADMIN_CREATE_POLICY, // displays create policy button
],
[IAM_PAGES.SETTINGS]: [
IAM_SCOPES.ADMIN_CONFIG_UPDATE, // displays configuration list
],
[IAM_PAGES.SETTINGS_VIEW]: [
IAM_SCOPES.ADMIN_CONFIG_UPDATE, // displays configuration list
],
[IAM_PAGES.NOTIFICATIONS_ENDPOINTS_ADD_SERVICE]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.NOTIFICATIONS_ENDPOINTS_ADD]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.NOTIFICATIONS_ENDPOINTS]: [
IAM_SCOPES.ADMIN_SERVER_INFO, // displays notifications endpoints
IAM_SCOPES.ADMIN_CONFIG_UPDATE, // displays create notification button
],
[IAM_PAGES.TIERS]: [
IAM_SCOPES.ADMIN_LIST_TIERS, // display tiers list
IAM_SCOPES.ADMIN_SET_TIER, // display "add tier" button
],
[IAM_PAGES.TIERS_ADD]: [
IAM_SCOPES.ADMIN_SET_TIER, // display "add tier" button / shows add service tier page
],
[IAM_PAGES.TIERS_ADD_SERVICE]: [
IAM_SCOPES.ADMIN_SET_TIER, // display "add tier" button / shows add service tier page
],
[IAM_PAGES.TOOLS]: [
IAM_SCOPES.S3_LISTEN_NOTIFICATIONS, // displays watch notifications
IAM_SCOPES.S3_LISTEN_BUCKET_NOTIFICATIONS, // display watch notifications
IAM_SCOPES.ADMIN_GET_CONSOLE_LOG, // display minio console logs
IAM_SCOPES.ADMIN_SERVER_TRACE, // display minio trace
IAM_SCOPES.ADMIN_HEAL, // display heal
IAM_SCOPES.ADMIN_HEALTH_INFO, // display diagnostics / display speedtest / display audit log
IAM_SCOPES.ADMIN_SERVER_INFO, // display diagnostics
],
[IAM_PAGES.TOOLS_LOGS]: [IAM_SCOPES.ADMIN_GET_CONSOLE_LOG],
[IAM_PAGES.TOOLS_AUDITLOGS]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
[IAM_PAGES.TOOLS_WATCH]: [
IAM_SCOPES.S3_LISTEN_NOTIFICATIONS, // displays watch notifications
IAM_SCOPES.S3_LISTEN_BUCKET_NOTIFICATIONS, // display watch notifications
],
[IAM_PAGES.TOOLS_TRACE]: [IAM_SCOPES.ADMIN_SERVER_TRACE],
[IAM_PAGES.TOOLS_HEAL]: [IAM_SCOPES.ADMIN_HEAL],
[IAM_PAGES.TOOLS_DIAGNOSTICS]: [
IAM_SCOPES.ADMIN_HEALTH_INFO,
IAM_SCOPES.ADMIN_SERVER_INFO,
],
[IAM_PAGES.TOOLS_SPEEDTEST]: [IAM_SCOPES.ADMIN_HEALTH_INFO],
};
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";
export const CONSOLE_UI_RESOURCE = "console-ui";

View File

@@ -41,6 +41,11 @@ import HelpBox from "../../../common/HelpBox";
import PageLayout from "../Common/Layout/PageLayout";
import SearchBox from "../Common/SearchBox";
import withSuspense from "../Common/Components/withSuspense";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import SecureComponent from "../../../common/SecureComponent/SecureComponent";
const AddServiceAccount = withSuspense(
React.lazy(() => import("./AddServiceAccount"))
@@ -68,14 +73,9 @@ const styles = (theme: Theme) =>
interface IServiceAccountsProps {
classes: any;
displayErrorMessage: typeof setErrorSnackMessage;
changePassword: boolean;
}
const Account = ({
classes,
displayErrorMessage,
changePassword,
}: IServiceAccountsProps) => {
const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
const [records, setRecords] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
@@ -193,21 +193,24 @@ const Account = ({
<PageHeader
label="Service Accounts"
actions={
<React.Fragment>
{changePassword && (
<Tooltip title="Change Password">
<IconButton
color="primary"
aria-label="Change Password"
component="span"
onClick={() => setChangePasswordModalOpen(true)}
size="large"
>
<LockIcon />
</IconButton>
</Tooltip>
)}
</React.Fragment>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CREATE_USER]}
resource={CONSOLE_UI_RESOURCE}
matchAll
errorProps={{ disabled: true }}
>
<Tooltip title="Change Password">
<IconButton
color="primary"
aria-label="Change Password"
component="span"
onClick={() => setChangePasswordModalOpen(true)}
size="large"
>
<LockIcon />
</IconButton>
</Tooltip>
</SecureComponent>
}
/>
<PageLayout>

View File

@@ -64,12 +64,9 @@ interface IAccessDetailsProps {
}
const AccessDetails = ({
classes,
match,
setErrorSnackMessage,
session,
loadingBucket,
bucketInfo,
}: IAccessDetailsProps) => {
const [curTab, setCurTab] = useState<number>(0);
const [loadingPolicies, setLoadingPolicies] = useState<boolean>(true);
@@ -189,6 +186,7 @@ const AccessDetails = ({
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_LIST_USER_POLICIES]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<TableWrapper
noBackground={true}
@@ -211,6 +209,7 @@ const AccessDetails = ({
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<TableWrapper
noBackground={true}

View File

@@ -222,6 +222,7 @@ const AccessRule = ({
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -88,8 +88,9 @@ const BrowserHandler = ({
}
actions={
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.admin]}
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Tooltip title={"Configure Bucket"}>
<IconButton

View File

@@ -265,6 +265,7 @@ const BucketDetails = ({
IAM_SCOPES.S3_FORCE_DELETE_BUCKET,
]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Delete"}

View File

@@ -160,6 +160,7 @@ const BucketEventsPanel = ({
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -211,6 +211,7 @@ const BucketLifecyclePanel = ({
]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -188,6 +188,7 @@ const BucketReplicationPanel = ({
scopes={[IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -666,6 +666,7 @@ const BucketSummary = ({
<SecureComponent
scopes={[IAM_SCOPES.S3_PUT_BUCKET_TAGGING]}
resource={bucketName}
errorProps={{ disabled: true, onClick: null }}
>
<Chip
className={classes.tag}

View File

@@ -238,7 +238,7 @@ const BucketListItem = ({
</Grid>
<Grid item xs={12} sm={5} className={classes.bucketActionButtons}>
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.admin]}
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucket.name}
>
<Link

View File

@@ -242,6 +242,7 @@ const ListBuckets = ({
<SecureComponent
scopes={[IAM_SCOPES.S3_CREATE_BUCKET]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -1012,7 +1012,6 @@ const ListObjects = ({
onClosePreview={closePreviewWindow}
/>
)}
<PageLayout>
<Grid item xs={12}>
<ScreenTitle
@@ -1038,6 +1037,7 @@ const ListObjects = ({
<SecureComponent
resource={bucketName}
scopes={[IAM_SCOPES.S3_PUT_OBJECT]}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Choose or create a new path"}
@@ -1051,6 +1051,12 @@ const ListObjects = ({
>
<AddFolderIcon />
</BoxIconButton>
</SecureComponent>
<SecureComponent
resource={bucketName}
scopes={[IAM_SCOPES.S3_PUT_OBJECT]}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Upload file"}
color="primary"
@@ -1065,14 +1071,20 @@ const ListObjects = ({
>
<UploadIcon />
</BoxIconButton>
<input
type="file"
multiple
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
ref={fileUpload}
/>
</SecureComponent>
<input
type="file"
multiple
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
ref={fileUpload}
/>
<SecureComponent
resource={bucketName}
scopes={[IAM_SCOPES.S3_PUT_OBJECT]}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Upload folder"}
color="primary"
@@ -1087,48 +1099,60 @@ const ListObjects = ({
>
<UploadFolderIcon />
</BoxIconButton>
<input
type="file"
multiple
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
ref={folderUpload}
/>
</SecureComponent>
<Badge
badgeContent=" "
color="secondary"
variant="dot"
invisible={!rewindEnabled}
className={classes.badgeOverlap}
<input
type="file"
multiple
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
ref={folderUpload}
/>
<SecureComponent
resource={bucketName}
scopes={[IAM_SCOPES.S3_PUT_OBJECT]}
errorProps={{ disabled: true }}
>
<Badge
badgeContent=" "
color="secondary"
variant="dot"
invisible={!rewindEnabled}
className={classes.badgeOverlap}
>
<BoxIconButton
tooltip={"Rewind"}
color="primary"
aria-label="Rewind"
onClick={() => {
setRewindSelect(true);
}}
disabled={!isVersioned}
size="large"
>
<HistoryIcon />
</BoxIconButton>
</Badge>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Rewind"}
tooltip={"Refresh list"}
color="primary"
aria-label="Rewind"
aria-label="Refresh List"
onClick={() => {
setRewindSelect(true);
setLoading(true);
}}
disabled={!isVersioned}
disabled={rewindEnabled}
size="large"
variant={"contained"}
>
<HistoryIcon />
<RefreshIcon />
</BoxIconButton>
</Badge>
<BoxIconButton
tooltip={"Refresh list"}
color="primary"
aria-label="Refresh List"
onClick={() => {
setLoading(true);
}}
disabled={rewindEnabled}
size="large"
variant={"contained"}
>
<RefreshIcon />
</BoxIconButton>
</SecureComponent>
</Fragment>
}
/>
@@ -1137,6 +1161,7 @@ const ListObjects = ({
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<SearchBox
onChange={setFilterObjects}
@@ -1157,6 +1182,7 @@ const ListObjects = ({
<SecureComponent
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -589,6 +589,7 @@ const ObjectDetails = ({
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Delete Object"}
@@ -640,6 +641,10 @@ const ObjectDetails = ({
]}
resource={bucketName}
matchAll
errorProps={{
disabled: true,
onClick: null,
}}
>
<IconButton
color="primary"
@@ -737,6 +742,7 @@ const ObjectDetails = ({
scopes={[IAM_SCOPES.S3_PUT_OBJECT_TAGGING]}
resource={bucketName}
matchAll
errorProps={{ disabled: true, onClick: null }}
>
<Chip
className={classes.tag}

View File

@@ -0,0 +1,60 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { Grid } from "@mui/material";
import HelpBox from "../../../../common/HelpBox";
interface IMissingIntegration {
iconComponent: any;
entity: string;
documentationLink: string;
}
const MissingIntegration = ({
iconComponent,
entity,
documentationLink,
}: IMissingIntegration) => {
return (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
title={`${entity} not available`}
iconComponent={iconComponent}
help={
<Fragment>
This feature is not available.
<br />
Please configure{" "}
<a href={documentationLink} target="_blank" rel="noreferrer">
{entity}
</a>{" "}
first to use this feature.
</Fragment>
}
/>
</Grid>
</Grid>
);
};
export default MissingIntegration;

View File

@@ -47,6 +47,13 @@ import PageLayout from "../../Common/Layout/PageLayout";
import SearchBox from "../../Common/SearchBox";
import withSuspense from "../../Common/Components/withSuspense";
import { AppState } from "../../../../store";
import DistributedOnly from "../../Common/DistributedOnly/DistributedOnly";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import SecureComponent from "../../../../common/SecureComponent/SecureComponent";
const UpdateTierCredentialsModal = withSuspense(
React.lazy(() => import("./UpdateTierCredentialsModal"))
@@ -56,6 +63,7 @@ interface IListTiersConfig {
classes: any;
history: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
distributedSetup: boolean;
}
const styles = (theme: Theme) =>
@@ -90,6 +98,7 @@ const ListTiersConfiguration = ({
classes,
history,
setErrorSnackMessage,
distributedSetup,
}: IListTiersConfig) => {
const [records, setRecords] = useState<ITierElement[]>([]);
const [filter, setFilter] = useState<string>("");
@@ -102,21 +111,25 @@ const ListTiersConfiguration = ({
useEffect(() => {
if (isLoading) {
const fetchRecords = () => {
api
.invoke("GET", `/api/v1/admin/tiers`)
.then((res: ITierResponse) => {
setRecords(res.items || []);
setIsLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setIsLoading(false);
});
};
fetchRecords();
if (distributedSetup) {
const fetchRecords = () => {
api
.invoke("GET", `/api/v1/admin/tiers`)
.then((res: ITierResponse) => {
setRecords(res.items || []);
setIsLoading(false);
})
.catch((err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
setIsLoading(false);
});
};
fetchRecords();
} else {
setIsLoading(false);
}
}
}, [isLoading, setErrorSnackMessage]);
}, [isLoading, setErrorSnackMessage, distributedSetup]);
const filteredRecords = records.filter((b: ITierElement) => {
if (filter === "") {
@@ -197,150 +210,169 @@ const ListTiersConfiguration = ({
)}
<PageHeader label="Tiers" />
<PageLayout>
<Grid item xs={12} className={classes.actionsTray}>
<SearchBox
placeholder="Filter"
onChange={setFilter}
overrideClass={classes.searchField}
/>
<div className={classes.rightActionButtons}>
<BoxIconButton
color="primary"
aria-label="Refresh List"
onClick={() => {
setIsLoading(true);
}}
size="large"
>
<RefreshIcon />
</BoxIconButton>
<Button
variant="contained"
color="primary"
endIcon={<AddIcon />}
onClick={addTier}
>
Add Tier
</Button>
</div>
</Grid>
{isLoading && <LinearProgress />}
{!isLoading && (
{!distributedSetup ? (
<DistributedOnly entity={"Tiers"} iconComponent={<TiersIcon />} />
) : (
<Fragment>
{records.length > 0 && (
<Grid item xs={12} className={classes.actionsTray}>
<SearchBox
placeholder="Filter"
onChange={setFilter}
overrideClass={classes.searchField}
/>
<div className={classes.rightActionButtons}>
<BoxIconButton
color="primary"
aria-label="Refresh List"
onClick={() => {
setIsLoading(true);
}}
size="large"
>
<RefreshIcon />
</BoxIconButton>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_SET_TIER]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<Button
variant="contained"
color="primary"
endIcon={<AddIcon />}
onClick={addTier}
>
Add Tier
</Button>
</SecureComponent>
</div>
</Grid>
{isLoading && <LinearProgress />}
{!isLoading && (
<Fragment>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={[
{
type: "edit",
onClick: (tierData: ITierElement) => {
setSelectedTier(tierData);
setUpdateCredentialsOpen(true);
},
},
]}
columns={[
{
label: "Tier Name",
elementKey: "type",
renderFunction: renderTierName,
renderFullObject: true,
},
{
label: "Type",
elementKey: "type",
width: 150,
},
{
label: "Endpoint",
elementKey: "type",
renderFunction: renderTierEndpoint,
renderFullObject: true,
},
{
label: "Bucket",
elementKey: "type",
renderFunction: renderTierBucket,
renderFullObject: true,
},
{
label: "Prefix",
elementKey: "type",
renderFunction: renderTierPrefix,
renderFullObject: true,
},
{
label: "Region",
elementKey: "type",
renderFunction: renderTierRegion,
renderFullObject: true,
},
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tiers"
idField="service_name"
customPaperHeight={classes.customConfigurationPage}
/>
</Grid>
<Grid item xs={12}>
<HelpBox
title={"Learn more about TIERS"}
iconComponent={<TiersIcon />}
help={
<Fragment>
Tiers are used by the MinIO Object Lifecycle Management
which allows creating rules for time or date based
automatic transition or expiry of objects. For object
transition, MinIO automatically moves the object to a
configured remote storage tier.
<br />
<br />
You can learn more at our{" "}
<a
href="https://docs.min.io/minio/baremetal/lifecycle-management/lifecycle-management-overview.html?ref=con"
target="_blank"
rel="noreferrer"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
{records.length > 0 && (
<Fragment>
<Grid item xs={12} className={classes.tableBlock}>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_LIST_TIERS]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={[
{
type: "edit",
onClick: (tierData: ITierElement) => {
setSelectedTier(tierData);
setUpdateCredentialsOpen(true);
},
},
]}
columns={[
{
label: "Tier Name",
elementKey: "type",
renderFunction: renderTierName,
renderFullObject: true,
},
{
label: "Type",
elementKey: "type",
width: 150,
},
{
label: "Endpoint",
elementKey: "type",
renderFunction: renderTierEndpoint,
renderFullObject: true,
},
{
label: "Bucket",
elementKey: "type",
renderFunction: renderTierBucket,
renderFullObject: true,
},
{
label: "Prefix",
elementKey: "type",
renderFunction: renderTierPrefix,
renderFullObject: true,
},
{
label: "Region",
elementKey: "type",
renderFunction: renderTierRegion,
renderFullObject: true,
},
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tiers"
idField="service_name"
customPaperHeight={classes.customConfigurationPage}
/>
</SecureComponent>
</Grid>
<Grid item xs={12}>
<HelpBox
title={"Learn more about TIERS"}
iconComponent={<TiersIcon />}
help={
<Fragment>
Tiers are used by the MinIO Object Lifecycle
Management which allows creating rules for time or
date based automatic transition or expiry of
objects. For object transition, MinIO automatically
moves the object to a configured remote storage
tier.
<br />
<br />
You can learn more at our{" "}
<a
href="https://docs.min.io/minio/baremetal/lifecycle-management/lifecycle-management-overview.html?ref=con"
target="_blank"
rel="noreferrer"
>
documentation
</a>
.
</Fragment>
}
/>
</Grid>
</Fragment>
)}
{records.length === 0 && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
title={"Tiers"}
iconComponent={<TiersIcon />}
help={
<Fragment>
Tiers are used by the MinIO Object Lifecycle
Management which allows creating rules for time or
date based automatic transition or expiry of
objects. For object transition, MinIO automatically
moves the object to a configured remote storage
tier.
<br />
<br />
To get started,{" "}
<AButton onClick={addTier}>Add A Tier</AButton>.
</Fragment>
}
/>
</Grid>
</Grid>
)}
</Fragment>
)}
{records.length === 0 && (
<Grid
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
>
<Grid item xs={8}>
<HelpBox
title={"Tiers"}
iconComponent={<TiersIcon />}
help={
<Fragment>
Tiers are used by the MinIO Object Lifecycle Management
which allows creating rules for time or date based
automatic transition or expiry of objects. For object
transition, MinIO automatically moves the object to a
configured remote storage tier.
<br />
<br />
To get started,{" "}
<AButton onClick={addTier}>Add A Tier</AButton>.
</Fragment>
}
/>
</Grid>
</Grid>
)}
</Fragment>
)}
</PageLayout>
@@ -348,10 +380,14 @@ const ListTiersConfiguration = ({
);
};
const mapState = (state: AppState) => ({
distributedSetup: state.system.distributedSetup,
});
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
const connector = connect(mapState, mapDispatchToProps);
export default withStyles(styles)(connector(ListTiersConfiguration));

View File

@@ -58,4 +58,5 @@ export interface IElement {
configuration_id: string;
configuration_label: string;
icon?: any;
disabled?: boolean;
}

View File

@@ -40,6 +40,15 @@ import Menu from "./Menu/Menu";
import api from "../../common/api";
import MainError from "./Common/MainError/MainError";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES,
IAM_PAGES_PERMISSIONS,
IAM_SCOPES,
S3_ALL_RESOURCES,
} from "../../common/SecureComponent/permissions";
import { hasPermission } from "../../common/SecureComponent/SecureComponent";
import { IRouteRule } from "./Menu/types";
const Trace = React.lazy(() => import("./Trace/Trace"));
const Heal = React.lazy(() => import("./Heal/Heal"));
@@ -149,6 +158,9 @@ interface IConsoleProps {
loadingProgress: number;
snackBarMessage: snackBarMessage;
setSnackBarMessage: typeof setSnackBarMessage;
operatorMode: boolean;
distributedSetup: boolean;
features: string[] | null;
}
const Console = ({
@@ -162,9 +174,13 @@ const Console = ({
loadingProgress,
snackBarMessage,
setSnackBarMessage,
operatorMode,
distributedSetup,
features,
}: IConsoleProps) => {
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
const restartServer = () => {
serverIsLoading(true);
api
@@ -185,199 +201,259 @@ const Console = ({
});
};
const allowedPages = !session
? []
: session.pages.reduce((result: any, item: any, index: any) => {
if (item.startsWith("/tools")) {
result["/tools"] = true;
}
result[item] = true;
return result;
}, {});
const routes = [
const consoleAdminRoutes: IRouteRule[] = [
{
component: Dashboard,
path: "/dashboard",
path: IAM_PAGES.DASHBOARD,
},
{
component: Metrics,
path: "/metrics",
path: IAM_PAGES.METRICS,
},
{
component: Buckets,
path: "/add-bucket",
path: IAM_PAGES.ADD_BUCKETS,
},
{
component: Buckets,
path: "/buckets",
path: IAM_PAGES.BUCKETS,
forceDisplay: true,
},
{
component: Buckets,
path: "/buckets/*",
},
{
component: Watch,
path: "/tools/watch",
},
{
component: Speedtest,
path: "/tools/speedtest",
},
{
component: Users,
path: "/users/:userName+",
},
{
component: Users,
path: "/users",
},
{
component: Groups,
path: "/groups",
},
{
component: GroupsDetails,
path: "/groups/:groupName+",
},
{
component: Policies,
path: "/policies/*",
},
{
component: Policies,
path: "/policies",
},
{
component: Heal,
path: "/tools/heal",
},
{
component: Trace,
path: "/tools/trace",
},
{
component: HealthInfo,
path: "/tools/diagnostics",
},
{
component: ErrorLogs,
path: "/tools/logs",
},
{
component: LogsSearchMain,
path: "/tools/audit-logs",
},
{
component: Tools,
path: "/tools",
},
{
component: ConfigurationOptions,
path: "/settings",
},
{
component: ConfigurationOptions,
path: "/settings/:option",
},
{
component: AddNotificationEndpoint,
path: "/notification-endpoints/add/:service",
},
{
component: NotificationTypeSelector,
path: "/notification-endpoints/add",
},
{
component: NotificationEndpoints,
path: "/notification-endpoints",
},
{
component: AddTierConfiguration,
path: "/tiers/add/:service",
},
{
component: TierTypeSelector,
path: "/tiers/add",
},
{
component: ListTiersConfiguration,
path: "/tiers",
},
{
component: Account,
path: "/account",
props: {
changePassword: (!session ? [] : session.pages).includes(
"/account/change-password"
),
path: IAM_PAGES.BUCKETS_ADMIN_VIEW,
customPermissionFnc: () => {
const path = window.location.pathname;
const resource = path.match(/buckets\/(.*)\/admin*/);
return (
resource &&
resource.length > 0 &&
hasPermission(
resource[1],
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_ADMIN_VIEW]
)
);
},
},
{
component: ListTenants,
path: "/tenants",
component: Buckets,
path: IAM_PAGES.BUCKETS_BROWSE_VIEW,
customPermissionFnc: () => {
const path = window.location.pathname;
const resource = path.match(/buckets\/(.*)\/browse*/);
return (
resource &&
resource.length > 0 &&
hasPermission(
resource[1],
IAM_PAGES_PERMISSIONS[IAM_PAGES.BUCKETS_BROWSE_VIEW]
)
);
},
},
{
component: AddTenant,
path: "/tenants/add",
component: Watch,
path: IAM_PAGES.TOOLS_WATCH,
},
{
component: Storage,
path: "/storage",
component: Speedtest,
path: IAM_PAGES.TOOLS_SPEEDTEST,
},
{
component: Storage,
path: "/storage/volumes",
component: Users,
path: IAM_PAGES.USERS_VIEW,
},
{
component: Storage,
path: "/storage/drives",
component: Users,
path: IAM_PAGES.USERS,
fsHidden: ldapIsEnabled,
customPermissionFnc: () =>
hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.ADMIN_LIST_USERS]) ||
hasPermission(S3_ALL_RESOURCES, [IAM_SCOPES.ADMIN_CREATE_USER]),
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName",
component: Groups,
path: IAM_PAGES.GROUPS,
fsHidden: ldapIsEnabled,
},
{
component: Hop,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/hop",
component: GroupsDetails,
path: IAM_PAGES.GROUPS_VIEW,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName",
component: Policies,
path: IAM_PAGES.POLICIES_VIEW,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/summary",
component: Policies,
path: IAM_PAGES.POLICIES,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/metrics",
component: Heal,
path: IAM_PAGES.TOOLS_HEAL,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/pods",
component: Trace,
path: IAM_PAGES.TOOLS_TRACE,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/pools",
component: HealthInfo,
path: IAM_PAGES.TOOLS_DIAGNOSTICS,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/volumes",
component: ErrorLogs,
path: IAM_PAGES.TOOLS_LOGS,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/license",
component: LogsSearchMain,
path: IAM_PAGES.TOOLS_AUDITLOGS,
},
{
component: TenantDetails,
path: "/namespaces/:tenantNamespace/tenants/:tenantName/security",
component: Tools,
path: IAM_PAGES.TOOLS,
},
{
component: ConfigurationOptions,
path: IAM_PAGES.SETTINGS,
},
{
component: ConfigurationOptions,
path: IAM_PAGES.SETTINGS_VIEW,
},
{
component: AddNotificationEndpoint,
path: IAM_PAGES.NOTIFICATIONS_ENDPOINTS_ADD_SERVICE,
},
{
component: NotificationTypeSelector,
path: IAM_PAGES.NOTIFICATIONS_ENDPOINTS_ADD,
},
{
component: NotificationEndpoints,
path: IAM_PAGES.NOTIFICATIONS_ENDPOINTS,
},
{
component: AddTierConfiguration,
path: IAM_PAGES.TIERS_ADD_SERVICE,
fsHidden: !distributedSetup,
},
{
component: TierTypeSelector,
path: IAM_PAGES.TIERS_ADD,
fsHidden: !distributedSetup,
},
{
component: ListTiersConfiguration,
path: IAM_PAGES.TIERS,
},
{
component: Account,
path: IAM_PAGES.ACCOUNT,
forceDisplay: true, // user has implicit access to service-accounts
},
{
component: License,
path: "/license",
path: IAM_PAGES.LICENSE,
forceDisplay: true,
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);
const operatorConsoleRoutes: IRouteRule[] = [
{
component: ListTenants,
path: IAM_PAGES.TENANTS,
forceDisplay: true,
},
{
component: AddTenant,
path: IAM_PAGES.TENANTS_ADD,
forceDisplay: true,
},
{
component: Storage,
path: IAM_PAGES.STORAGE,
forceDisplay: true,
},
{
component: Storage,
path: IAM_PAGES.STORAGE_VOLUMES,
forceDisplay: true,
},
{
component: Storage,
path: IAM_PAGES.STORAGE_DRIVES,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT,
forceDisplay: true,
},
{
component: Hop,
path: IAM_PAGES.NAMESPACE_TENANT_HOP,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_PODS,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_SUMMARY,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_METRICS,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_PODS_LIST,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_POOLS,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_VOLUMES,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_LICENSE,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_SECURITY,
forceDisplay: true,
},
{
component: License,
path: IAM_PAGES.LICENSE,
forceDisplay: true,
},
];
const allowedRoutes = (
operatorMode ? operatorConsoleRoutes : consoleAdminRoutes
).filter(
(route: any) =>
(route.forceDisplay ||
(route.customPermissionFnc
? route.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[route.path]
))) &&
!route.fsHidden
);
const closeSnackBar = () => {
setOpenSnackbar(false);
@@ -409,7 +485,7 @@ const Console = ({
{session && session.status === "ok" ? (
<div className={classes.root}>
<CssBaseline />
{!hideMenu && <Menu pages={session.pages} />}
{!hideMenu && <Menu />}
<main className={classes.content}>
{needsRestart && (
@@ -505,6 +581,9 @@ const mapState = (state: AppState) => ({
session: state.console.session,
loadingProgress: state.system.loadingProgress,
snackBarMessage: state.system.snackBar,
operatorMode: state.system.operatorMode,
distributedSetup: state.system.distributedSetup,
features: state.console.session.features,
});
const connector = connect(mapState, {

View File

@@ -219,6 +219,7 @@ const Groups = ({ classes, setErrorSnackMessage }: IGroupsProps) => {
IAM_SCOPES.ADMIN_LIST_USERS,
]}
matchAll
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -17,7 +17,7 @@ import { connect } from "react-redux";
import withStyles from "@mui/styles/withStyles";
import { Button, Grid, Tooltip } from "@mui/material";
import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
import { IAMPoliciesIcon, TrashIcon, UsersIcon } from "../../../icons";
import { IAMPoliciesIcon, TrashIcon, GroupsIcon } from "../../../icons";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import history from "../../../history";
import api from "../../../common/api";
@@ -194,7 +194,7 @@ const GroupsDetails = ({ classes }: IGroupDetailsProps) => {
<Button
variant="contained"
color="primary"
endIcon={<UsersIcon />}
endIcon={<GroupsIcon />}
size="medium"
onClick={() => {
setUsersOpen(true);
@@ -212,6 +212,14 @@ const GroupsDetails = ({ classes }: IGroupDetailsProps) => {
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={[
{
type: "view",
onClick: (userName) => {
history.push(`/users/${userName}`);
},
},
]}
columns={[{ label: "Access Key", elementKey: "" }]}
selectedItems={[]}
isLoading={false}
@@ -269,7 +277,7 @@ const GroupsDetails = ({ classes }: IGroupDetailsProps) => {
<ScreenTitle
icon={
<Fragment>
<UsersIcon width={40} />
<GroupsIcon width={40} />
</Fragment>
}
title={groupName}
@@ -305,6 +313,7 @@ const GroupsDetails = ({ classes }: IGroupDetailsProps) => {
<SecureComponent
resource={CONSOLE_UI_RESOURCE}
scopes={[IAM_SCOPES.ADMIN_REMOVE_USER_FROM_GROUP]}
errorProps={{ disabled: true }}
>
<Tooltip title="Delete Group">
<div className={classes.spacerLeft}>

View File

@@ -17,7 +17,6 @@
import React, { useEffect, useState, Fragment } from "react";
import { connect } from "react-redux";
import { HorizontalBar } from "react-chartjs-2";
import { Redirect } from "react-router-dom";
import {
Button,
FormControl,
@@ -264,9 +263,8 @@ const Heal = ({ classes, distributedSetup }: IHeal) => {
<DistributedOnly entity={"Heal"} iconComponent={<HealIcon />} />
) : (
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_HEAL_ACTION]}
scopes={[IAM_SCOPES.ADMIN_HEAL]}
resource={CONSOLE_UI_RESOURCE}
RenderError={<Redirect to={"/"} />}
>
<Grid xs={12} item className={classes.formBox}>
<Grid item xs={12} className={classes.actionsTray}>

View File

@@ -42,6 +42,13 @@ import PageHeader from "../../Common/PageHeader/PageHeader";
import BackLink from "../../../../common/BackLink";
import PageLayout from "../../Common/Layout/PageLayout";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import SecureComponent from "../../../../common/SecureComponent/SecureComponent";
import { SearchIcon } from "../../../../icons";
import MissingIntegration from "../../Common/MissingIntegration/MissingIntegration";
interface ILogSearchProps {
classes: any;
@@ -199,6 +206,9 @@ const LogsSearchMain = ({
setAlreadyFetching(false);
setErrorSnackMessage(err);
});
} else {
setLoading(false);
setAlreadyFetching(false);
}
}, [
alreadyFetching,
@@ -279,192 +289,221 @@ const LogsSearchMain = ({
<PageHeader label="Audit Logs" />
<BackLink to="/tools" label="Return to Tools" />
<PageLayout>
<Grid xs={12} className={classes.formBox}>
<Grid item xs={12} className={`${classes.searchOptions}`}>
<div className={classes.dateRangePicker}>
<DateRangeSelector
setTimeEnd={setTimeEnd}
setTimeStart={setTimeStart}
timeEnd={timeEnd}
timeStart={timeStart}
/>
</div>
<Grid item className={classes.advancedButton}>
<button
onClick={() => {
setFilterOpen(!filterOpen);
}}
className={classes.advancedConfiguration}
>
{filterOpen ? "Hide" : "Show"} advanced Filters{" "}
<span
className={
filterOpen ? classes.advancedOpen : classes.advancedClosed
}
>
<ArrowForwardIosIcon />
</span>
</button>
</Grid>
</Grid>
<Grid
item
xs={12}
className={`${classes.blockCollapsed} ${
filterOpen ? classes.filterOpen : ""
}`}
>
<div className={classes.innerContainer}>
<div className={classes.noticeLabel}>
Enable your preferred options to get filtered records.
<br />
You can use '*' to match any character, '.' to signify a single
character or '\' to scape an special character (E.g. mybucket-*)
</div>
<div className={classes.filtersContainer}>
<FilterInputWrapper
onChange={setBucket}
value={bucket}
label={"Bucket"}
id="bucket"
name="bucket"
/>
<FilterInputWrapper
onChange={setApiName}
value={apiName}
label={"API Name"}
id="api_name"
name="api_name"
/>
<FilterInputWrapper
onChange={setUserAgent}
value={userAgent}
label={"User Agent"}
id="user_agent"
name="user_agent"
/>
</div>
<div className={classes.filtersContainer}>
<FilterInputWrapper
onChange={setObject}
value={object}
label={"Object"}
id="object"
name="object"
/>
<FilterInputWrapper
onChange={setRequestID}
value={requestID}
label={"Request ID"}
id="request_id"
name="request_id"
/>
<FilterInputWrapper
onChange={setResponseStatus}
value={responseStatus}
label={"Response Status"}
id="response_status"
name="response_status"
/>
</div>
</div>
</Grid>
<Grid item xs={12} className={classes.endLineAction}>
<Button
type="button"
variant="contained"
color="primary"
onClick={triggerLoad}
>
Get Information
</Button>
</Grid>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
columns={[
{
label: LogSearchColumnLabels.time,
elementKey: "time",
enableSort: true,
},
{
label: LogSearchColumnLabels.api_name,
elementKey: "api_name",
},
{ label: LogSearchColumnLabels.bucket, elementKey: "bucket" },
{ label: LogSearchColumnLabels.object, elementKey: "object" },
{
label: LogSearchColumnLabels.remote_host,
elementKey: "remote_host",
},
{
label: LogSearchColumnLabels.request_id,
elementKey: "request_id",
},
{
label: LogSearchColumnLabels.user_agent,
elementKey: "user_agent",
},
{
label: LogSearchColumnLabels.response_status,
elementKey: "response_status",
renderFunction: (element) => (
<Fragment>
<span>
{element.response_status_code} ({element.response_status})
</span>
</Fragment>
),
renderFullObject: true,
},
{
label: LogSearchColumnLabels.request_content_length,
elementKey: "request_content_length",
renderFunction: niceBytes,
},
{
label: LogSearchColumnLabels.response_content_length,
elementKey: "response_content_length",
renderFunction: niceBytes,
},
{
label: LogSearchColumnLabels.time_to_response_ns,
elementKey: "time_to_response_ns",
renderFunction: nsToSeconds,
contentTextAlign: "right",
},
]}
isLoading={loading}
records={records}
entityName="Logs"
customEmptyMessage={"There is no information with this criteria"}
idField="request_id"
columnsSelector
columnsShown={columnsShown}
onColumnChange={selectColumn}
customPaperHeight={
filterOpen ? classes.tableFOpen : classes.tableFClosed
}
sortConfig={{
currentSort: "time",
currentDirection: sortOrder,
triggerSort: sortChange,
}}
infiniteScrollConfig={{
recordsCount: 1000000,
loadMoreRecords: loadMoreRecords,
}}
itemActions={[
{
type: "view",
onClick: openExtraInformation,
},
]}
textSelectable
{!logSearchEnabled ? (
<MissingIntegration
entity={"Audit Logs"}
iconComponent={<SearchIcon />}
documentationLink="https://github.com/minio/operator/tree/master/logsearchapi"
/>
</Grid>
) : (
<Fragment>
{" "}
<Grid xs={12} className={classes.formBox}>
<Grid item xs={12} className={`${classes.searchOptions}`}>
<div className={classes.dateRangePicker}>
<DateRangeSelector
setTimeEnd={setTimeEnd}
setTimeStart={setTimeStart}
timeEnd={timeEnd}
timeStart={timeStart}
/>
</div>
<Grid item className={classes.advancedButton}>
<button
onClick={() => {
setFilterOpen(!filterOpen);
}}
className={classes.advancedConfiguration}
>
{filterOpen ? "Hide" : "Show"} advanced Filters{" "}
<span
className={
filterOpen
? classes.advancedOpen
: classes.advancedClosed
}
>
<ArrowForwardIosIcon />
</span>
</button>
</Grid>
</Grid>
<Grid
item
xs={12}
className={`${classes.blockCollapsed} ${
filterOpen ? classes.filterOpen : ""
}`}
>
<div className={classes.innerContainer}>
<div className={classes.noticeLabel}>
Enable your preferred options to get filtered records.
<br />
You can use '*' to match any character, '.' to signify a
single character or '\' to scape an special character (E.g.
mybucket-*)
</div>
<div className={classes.filtersContainer}>
<FilterInputWrapper
onChange={setBucket}
value={bucket}
label={"Bucket"}
id="bucket"
name="bucket"
/>
<FilterInputWrapper
onChange={setApiName}
value={apiName}
label={"API Name"}
id="api_name"
name="api_name"
/>
<FilterInputWrapper
onChange={setUserAgent}
value={userAgent}
label={"User Agent"}
id="user_agent"
name="user_agent"
/>
</div>
<div className={classes.filtersContainer}>
<FilterInputWrapper
onChange={setObject}
value={object}
label={"Object"}
id="object"
name="object"
/>
<FilterInputWrapper
onChange={setRequestID}
value={requestID}
label={"Request ID"}
id="request_id"
name="request_id"
/>
<FilterInputWrapper
onChange={setResponseStatus}
value={responseStatus}
label={"Response Status"}
id="response_status"
name="response_status"
/>
</div>
</div>
</Grid>
<Grid item xs={12} className={classes.endLineAction}>
<Button
type="button"
variant="contained"
color="primary"
onClick={triggerLoad}
>
Get Information
</Button>
</Grid>
</Grid>
<Grid item xs={12} className={classes.tableBlock}>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_HEALTH_INFO]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TableWrapper
columns={[
{
label: LogSearchColumnLabels.time,
elementKey: "time",
enableSort: true,
},
{
label: LogSearchColumnLabels.api_name,
elementKey: "api_name",
},
{
label: LogSearchColumnLabels.bucket,
elementKey: "bucket",
},
{
label: LogSearchColumnLabels.object,
elementKey: "object",
},
{
label: LogSearchColumnLabels.remote_host,
elementKey: "remote_host",
},
{
label: LogSearchColumnLabels.request_id,
elementKey: "request_id",
},
{
label: LogSearchColumnLabels.user_agent,
elementKey: "user_agent",
},
{
label: LogSearchColumnLabels.response_status,
elementKey: "response_status",
renderFunction: (element) => (
<Fragment>
<span>
{element.response_status_code} (
{element.response_status})
</span>
</Fragment>
),
renderFullObject: true,
},
{
label: LogSearchColumnLabels.request_content_length,
elementKey: "request_content_length",
renderFunction: niceBytes,
},
{
label: LogSearchColumnLabels.response_content_length,
elementKey: "response_content_length",
renderFunction: niceBytes,
},
{
label: LogSearchColumnLabels.time_to_response_ns,
elementKey: "time_to_response_ns",
renderFunction: nsToSeconds,
contentTextAlign: "right",
},
]}
isLoading={loading}
records={records}
entityName="Logs"
customEmptyMessage={
"There is no information with this criteria"
}
idField="request_id"
columnsSelector
columnsShown={columnsShown}
onColumnChange={selectColumn}
customPaperHeight={
filterOpen ? classes.tableFOpen : classes.tableFClosed
}
sortConfig={{
currentSort: "time",
currentDirection: sortOrder,
triggerSort: sortChange,
}}
infiniteScrollConfig={{
recordsCount: 1000000,
loadMoreRecords: loadMoreRecords,
}}
itemActions={[
{
type: "view",
onClick: openExtraInformation,
},
]}
textSelectable
/>
</SecureComponent>
</Grid>
</Fragment>
)}
</PageLayout>
</Fragment>
);

View File

@@ -60,6 +60,14 @@ import {
UsersIcon,
VersionIcon,
} from "../../../icons";
import {
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS,
IAM_PAGES,
S3_ALL_RESOURCES,
IAM_SCOPES,
} from "../../../common/SecureComponent/permissions";
import { hasPermission } from "../../../common/SecureComponent/SecureComponent";
const drawerWidth = 245;
@@ -217,23 +225,23 @@ const styles = (theme: Theme) =>
interface IMenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
pages: string[];
operatorMode: boolean;
distributedSetup: boolean;
sidebarOpen: boolean;
setMenuOpen: typeof setMenuOpen;
resetSession: typeof resetSession;
features: string[] | null;
}
const Menu = ({
userLoggedIn,
classes,
pages,
operatorMode,
distributedSetup,
sidebarOpen,
setMenuOpen,
resetSession,
features,
}: IMenuProps) => {
const logout = () => {
const deleteSession = () => {
@@ -254,12 +262,13 @@ const Menu = ({
});
};
let menuItems: IMenuItem[] = [
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
let menuConsoleAdmin: IMenuItem[] = [
{
group: "common",
type: "item",
component: NavLink,
to: "/dashboard",
to: IAM_PAGES.DASHBOARD,
name: "Dashboard",
icon: DashboardIcon,
},
@@ -267,40 +276,46 @@ const Menu = ({
group: "common",
type: "item",
component: NavLink,
to: "/buckets",
to: IAM_PAGES.BUCKETS,
name: "Buckets",
icon: BucketsIcon,
forceDisplay: true,
},
{
group: "common",
type: "item",
component: NavLink,
to: "/users",
to: IAM_PAGES.USERS,
customPermissionFnc: () =>
hasPermission(CONSOLE_UI_RESOURCE, [IAM_SCOPES.ADMIN_LIST_USERS]) ||
hasPermission(S3_ALL_RESOURCES, [IAM_SCOPES.ADMIN_CREATE_USER]),
name: "Users",
icon: UsersIcon,
fsHidden: ldapIsEnabled,
},
{
group: "common",
type: "item",
component: NavLink,
to: "/groups",
to: IAM_PAGES.GROUPS,
name: "Groups",
icon: GroupsIcon,
fsHidden: ldapIsEnabled,
},
{
group: "common",
type: "item",
component: NavLink,
to: "/account",
to: IAM_PAGES.ACCOUNT,
name: "Service Accounts",
icon: AccountIcon,
forceDisplay: true,
},
{
group: "common",
type: "item",
component: NavLink,
to: "/policies",
to: IAM_PAGES.POLICIES,
name: "IAM Policies",
icon: IAMPoliciesIcon,
},
@@ -308,7 +323,7 @@ const Menu = ({
group: "common",
type: "item",
component: NavLink,
to: "/settings",
to: IAM_PAGES.SETTINGS,
name: "Settings",
icon: SettingsIcon,
},
@@ -316,7 +331,7 @@ const Menu = ({
group: "common",
type: "item",
component: NavLink,
to: "/notification-endpoints",
to: IAM_PAGES.NOTIFICATIONS_ENDPOINTS,
name: "Notification Endpoints",
icon: LambdaIcon,
},
@@ -324,7 +339,7 @@ const Menu = ({
group: "common",
type: "item",
component: NavLink,
to: "/tiers",
to: IAM_PAGES.TIERS,
name: "Tiers",
icon: TiersIcon,
},
@@ -332,107 +347,97 @@ const Menu = ({
group: "common",
type: "item",
component: NavLink,
to: "/tools",
to: IAM_PAGES.TOOLS,
name: "Tools",
icon: ToolsIcon,
},
{
group: "License",
type: "item",
component: NavLink,
to: IAM_PAGES.LICENSE,
name: "License",
icon: LicenseIcon,
forceDisplay: true,
},
{
group: "License",
type: "item",
component: NavLink,
to: IAM_PAGES.DOCUMENTATION,
name: "Documentation",
icon: DocumentationIcon,
forceDisplay: true,
onClick: (
e:
| React.MouseEvent<HTMLLIElement>
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLDivElement>
) => {
e.preventDefault();
window.open("https://docs.min.io/?ref=con", "_blank");
},
},
];
let menuOperatorConsole: IMenuItem[] = [
{
group: "Operator",
type: "item",
component: NavLink,
to: "/tenants",
to: IAM_PAGES.TENANTS,
name: "Tenants",
icon: TenantsOutlineIcon,
forceDisplay: true,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/storage",
to: IAM_PAGES.STORAGE,
name: "Storage",
icon: StorageIcon,
forceDisplay: true,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: IAM_PAGES.LICENSE,
name: "License",
icon: LicenseIcon,
forceDisplay: true,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: IAM_PAGES.DOCUMENTATION,
name: "Documentation",
icon: DocumentationIcon,
forceDisplay: true,
onClick: (
e:
| React.MouseEvent<HTMLLIElement>
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLDivElement>
) => {
e.preventDefault();
window.open("https://docs.min.io/?ref=op", "_blank");
},
},
];
const allowedPages = pages.reduce((result: any, item: any) => {
if (item.startsWith("/tools")) {
result["/tools"] = true;
}
result[item] = true;
return result;
}, {});
const documentation: IMenuItem = {
group: "License",
type: "item",
component: NavLink,
to: "/documentation",
name: "Documentation",
icon: DocumentationIcon,
forceDisplay: true,
};
// Append the license page according to the allowedPages
if (allowedPages.hasOwnProperty("/tenants")) {
menuItems.push(
{
group: "Operator",
type: "item",
component: NavLink,
to: "/license",
name: "License",
icon: LicenseIcon,
},
{
...documentation,
group: "Operator",
onClick: (
e:
| React.MouseEvent<HTMLLIElement>
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLDivElement>
) => {
e.preventDefault();
window.open(
`https://docs.min.io/?ref=${operatorMode ? "op" : "con"}`,
"_blank"
);
},
}
);
} else {
menuItems.push(
{
group: "License",
type: "item",
component: NavLink,
to: "/license",
name: "License",
icon: LicenseIcon,
},
{
...documentation,
group: "License",
onClick: (
e:
| React.MouseEvent<HTMLLIElement>
| React.MouseEvent<HTMLAnchorElement>
| React.MouseEvent<HTMLDivElement>
) => {
e.preventDefault();
window.open(
`https://docs.min.io/?ref=${operatorMode ? "op" : "con"}`,
"_blank"
);
},
}
);
}
const allowedItems = menuItems.filter(
const allowedItems = (
operatorMode ? menuOperatorConsole : menuConsoleAdmin
).filter(
(item: any) =>
(allowedPages[item.to] || item.forceDisplay || item.type !== "item") &&
item.fsHidden !== false
((item.customPermissionFnc
? item.customPermissionFnc()
: hasPermission(CONSOLE_UI_RESOURCE, IAM_PAGES_PERMISSIONS[item.to])) ||
item.forceDisplay ||
item.type !== "item") &&
!item.fsHidden
);
return (
@@ -561,6 +566,7 @@ const mapState = (state: AppState) => ({
sidebarOpen: state.system.sidebarOpen,
operatorMode: state.system.operatorMode,
distributedSetup: state.system.distributedSetup,
features: state.console.session.features,
});
const connector = connect(mapState, {

View File

@@ -14,8 +14,6 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { userLoggedIn } from "../../../actions";
export interface IMenuItem {
group: string;
type: string;
@@ -27,4 +25,13 @@ export interface IMenuItem {
forceDisplay?: boolean;
extraMargin?: boolean;
fsHidden?: boolean;
customPermissionFnc?: any;
}
export interface IRouteRule {
component: any;
path: string;
forceDisplay?: boolean;
fsHidden?: boolean;
customPermissionFnc?: any;
}

View File

@@ -206,6 +206,7 @@ const ListPolicies = ({ classes, setErrorSnackMessage }: IPoliciesProps) => {
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CREATE_POLICY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<Button
variant="contained"

View File

@@ -56,6 +56,7 @@ import SecureComponent, {
} from "../../../common/SecureComponent/SecureComponent";
import withSuspense from "../Common/Components/withSuspense";
import { AppState } from "../../../store";
const DeletePolicy = withSuspense(React.lazy(() => import("./DeletePolicy")));
@@ -64,6 +65,7 @@ interface IPolicyDetailsProps {
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
features: string[] | null;
}
const styles = (theme: Theme) =>
@@ -103,6 +105,7 @@ const PolicyDetails = ({
match,
setErrorSnackMessage,
setSnackBarMessage,
features,
}: IPolicyDetailsProps) => {
const [policy, setPolicy] = useState<Policy | null>(null);
const [policyStatements, setPolicyStatements] = useState<IAMStatement[]>([]);
@@ -118,6 +121,8 @@ const PolicyDetails = ({
const [loadingGroups, setLoadingGroups] = useState<boolean>(true);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
const displayGroups = hasPermission(
CONSOLE_UI_RESOURCE,
[IAM_SCOPES.ADMIN_LIST_GROUPS, IAM_SCOPES.ADMIN_GET_GROUP],
@@ -172,7 +177,7 @@ const PolicyDetails = ({
useEffect(() => {
const loadUsersForPolicy = () => {
if (loadingUsers) {
if (displayUsers) {
if (displayUsers && !ldapIsEnabled) {
api
.invoke(
"GET",
@@ -194,7 +199,7 @@ const PolicyDetails = ({
const loadGroupsForPolicy = () => {
if (loadingGroups) {
if (displayGroups) {
if (displayGroups && !ldapIsEnabled) {
api
.invoke(
"GET",
@@ -264,6 +269,7 @@ const PolicyDetails = ({
displayUsers,
displayGroups,
displayPolicy,
ldapIsEnabled,
]);
const resetForm = () => {
@@ -347,6 +353,7 @@ const PolicyDetails = ({
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_DELETE_POLICY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<BoxIconButton
tooltip={"Delete Policy"}
@@ -456,7 +463,10 @@ const PolicyDetails = ({
),
}}
{{
tabConfig: { label: "Users", disabled: !displayUsers },
tabConfig: {
label: "Users",
disabled: !displayUsers || ldapIsEnabled,
},
content: (
<Fragment>
<div className={classes.sectionTitle}>Users</div>
@@ -497,7 +507,10 @@ const PolicyDetails = ({
),
}}
{{
tabConfig: { label: "Groups", disabled: !displayGroups },
tabConfig: {
label: "Groups",
disabled: !displayGroups || ldapIsEnabled,
},
content: (
<Fragment>
<div className={classes.sectionTitle}>Groups</div>
@@ -576,6 +589,7 @@ const PolicyDetails = ({
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CREATE_POLICY]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<Button
type="submit"
@@ -604,7 +618,11 @@ const PolicyDetails = ({
);
};
const connector = connect(null, {
const mapState = (state: AppState) => ({
features: state.console.session.features,
});
const connector = connect(mapState, {
setErrorSnackMessage,
setSnackBarMessage,
});

View File

@@ -19,7 +19,6 @@ import { connect } from "react-redux";
import { IMessageEvent, w3cwebsocket as W3CWebSocket } from "websocket";
import { Button, CircularProgress, Grid } from "@mui/material";
import { Theme } from "@mui/material/styles";
import { Redirect } from "react-router-dom";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
@@ -205,9 +204,8 @@ const Speedtest = ({ classes, distributedSetup }: ISpeedtest) => {
/>
) : (
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_HEAL_ACTION]}
scopes={[IAM_SCOPES.ADMIN_HEAL]}
resource={CONSOLE_UI_RESOURCE}
RenderError={<Redirect to={"/"} />}
>
<Grid item xs={12} className={classes.boxy}>
<Grid container>

View File

@@ -36,16 +36,20 @@ import {
SearchIcon,
TraceIcon,
WatchIcon,
SpeedtestIcon,
} from "../../../../icons";
import { hasPermission } from "../../../../common/SecureComponent/SecureComponent";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
IAM_PAGES,
IAM_PAGES_PERMISSIONS,
} from "../../../../common/SecureComponent/permissions";
import SpeedtestIcon from "../../../../icons/SpeedtestIcon";
import { hasPermission } from "../../../../common/SecureComponent/SecureComponent";
import { AppState } from "../../../../store";
import { connect } from "react-redux";
interface IConfigurationOptions {
classes: any;
features: string[];
}
const styles = (theme: Theme) =>
@@ -65,57 +69,70 @@ const styles = (theme: Theme) =>
...containerForHeader(theme.spacing(4)),
});
const ToolsList = ({ classes }: IConfigurationOptions) => {
const ToolsList = ({ classes, features }: IConfigurationOptions) => {
const configurationElements: IElement[] = [
{
icon: <LogsIcon />,
configuration_id: "logs",
configuration_label: "Logs",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_CONSOLE_LOG_ACTION,
]),
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_LOGS]
),
},
{
icon: <SearchIcon />,
configuration_id: "audit-logs",
configuration_label: "Audit Logs",
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_AUDITLOGS]
),
},
{
icon: <WatchIcon />,
configuration_id: "watch",
configuration_label: "Watch",
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_WATCH]
),
},
{
icon: <TraceIcon />,
configuration_id: "trace",
configuration_label: "Trace",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_TRACE_ACTION,
]),
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_TRACE]
),
},
{
icon: <HealIcon />,
configuration_id: "heal",
configuration_label: "Heal",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEAL_ACTION,
]),
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_HEAL]
),
},
{
icon: <DiagnosticsIcon />,
configuration_id: "diagnostics",
configuration_label: "Diagnostics",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEALTH_ACTION,
]),
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_DIAGNOSTICS]
),
},
{
icon: <SpeedtestIcon />,
configuration_id: "speedtest",
configuration_label: "Speedtest",
disabled: !hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_HEAL_ACTION,
]),
disabled: !hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[IAM_PAGES.TOOLS_SPEEDTEST]
),
},
];
@@ -142,4 +159,10 @@ const ToolsList = ({ classes }: IConfigurationOptions) => {
);
};
export default withStyles(styles)(ToolsList);
const mapState = (state: AppState) => ({
features: state.console.session.features,
});
const connector = connect(mapState, null);
export default connector(withStyles(styles)(ToolsList));

View File

@@ -42,6 +42,14 @@ import AButton from "../Common/AButton/AButton";
import PageLayout from "../Common/Layout/PageLayout";
import SearchBox from "../Common/SearchBox";
import withSuspense from "../Common/Components/withSuspense";
import {
CONSOLE_UI_RESOURCE,
IAM_SCOPES,
S3_ALL_RESOURCES,
} from "../../../common/SecureComponent/permissions";
import SecureComponent, {
hasPermission,
} from "../../../common/SecureComponent/SecureComponent";
const AddUser = withSuspense(React.lazy(() => import("./AddUser")));
const SetPolicy = withSuspense(
@@ -81,6 +89,22 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const displayListUsers = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_LIST_USERS,
]);
const deleteUser = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_DELETE_USER,
]);
const viewUser = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_GET_USER,
]);
const addUserToGroup = hasPermission(CONSOLE_UI_RESOURCE, [
IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP,
]);
const closeAddModalAndRefresh = () => {
setAddScreenOpen(false);
setLoading(true);
@@ -102,20 +126,24 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
const users = res.users === null ? [] : res.users;
if (displayListUsers) {
api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
const users = res.users === null ? [] : res.users;
setLoading(false);
setRecords(users.sort(usersSort));
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
setLoading(false);
setRecords(users.sort(usersSort));
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setErrorSnackMessage(err);
});
} else {
setLoading(false);
}
}
}, [loading, setErrorSnackMessage]);
}, [loading, setErrorSnackMessage, displayListUsers]);
const filteredRecords = records.filter((elementItem) =>
elementItem.accessKey.includes(filter)
@@ -155,11 +183,16 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
);
const tableActions = [
{ type: "view", onClick: viewAction },
{
type: "view",
onClick: viewAction,
disableButtonFunction: () => !viewUser,
},
{
type: "delete",
onClick: deleteAction,
disableButtonFunction: (topValue: any) => topValue === userLoggedIn,
disableButtonFunction: (topValue: any) =>
topValue === userLoggedIn || !deleteUser,
},
];
@@ -211,32 +244,48 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
onChange={setFilter}
overrideClass={classes.searchField}
/>
<Button
variant="outlined"
color="primary"
endIcon={<GroupIcon />}
disabled={checkedUsers.length <= 0}
onClick={() => {
if (checkedUsers.length > 0) {
setAddGroupOpen(true);
}
}}
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
Add to Group
</Button>
<Button
variant="contained"
color="primary"
endIcon={<AddIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedUser(null);
}}
<Button
variant="outlined"
color="primary"
endIcon={<GroupIcon />}
disabled={checkedUsers.length <= 0}
onClick={() => {
if (checkedUsers.length > 0) {
setAddGroupOpen(true);
}
}}
>
Add to Group
</Button>
</SecureComponent>
<SecureComponent
scopes={[
IAM_SCOPES.ADMIN_CREATE_USER,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_GROUPS,
]}
resource={S3_ALL_RESOURCES}
matchAll
errorProps={{ disabled: true }}
>
Create User
</Button>
<Button
variant="contained"
color="primary"
endIcon={<AddIcon />}
onClick={() => {
setAddScreenOpen(true);
setSelectedUser(null);
}}
>
Create User
</Button>
</SecureComponent>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
@@ -246,16 +295,24 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
{records.length > 0 && (
<Fragment>
<Grid item xs={12} className={classes.tableBlock}>
<TableWrapper
itemActions={tableActions}
columns={[{ label: "Access Key", elementKey: "accessKey" }]}
onSelect={selectionChanged}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
entityName="Users"
idField="accessKey"
/>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_LIST_USERS]}
resource={CONSOLE_UI_RESOURCE}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Access Key", elementKey: "accessKey" },
]}
onSelect={addUserToGroup ? selectionChanged : undefined}
selectedItems={checkedUsers}
isLoading={loading}
records={filteredRecords}
entityName="Users"
idField="accessKey"
/>
</SecureComponent>
</Grid>
<Grid item xs={12}>
<HelpBox
@@ -315,18 +372,30 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
explicitly list the actions and resources to which that
user has access. Users can also inherit policies from
the groups in which they have membership.
<br />
<br />
To get started,{" "}
<AButton
onClick={() => {
setAddScreenOpen(true);
setSelectedUser(null);
}}
<SecureComponent
scopes={[
IAM_SCOPES.ADMIN_CREATE_USER,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_GROUPS,
]}
matchAll
resource={CONSOLE_UI_RESOURCE}
>
Create a User
</AButton>
.
<Fragment>
<br />
<br />
To get started,{" "}
<AButton
onClick={() => {
setAddScreenOpen(true);
setSelectedUser(null);
}}
>
Create a User
</AButton>
.
</Fragment>
</SecureComponent>
</Fragment>
}
/>

View File

@@ -25,7 +25,6 @@ const initialState: ConsoleState = {
session: {
operator: false,
status: "",
pages: [],
features: [],
distributedMode: false,
permissions: {},

View File

@@ -20,7 +20,6 @@ export interface ISessionPermissions {
export interface ISessionResponse {
status: string;
pages: string[];
features: string[];
operator: boolean;
distributedMode: boolean;

View File

@@ -9612,10 +9612,10 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
prettier@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893"
integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==
pretty-bytes@^5.3.0:
version "5.6.0"

View File

@@ -5223,12 +5223,6 @@ func init() {
"operator": {
"type": "boolean"
},
"pages": {
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"type": "object",
"additionalProperties": {
@@ -11074,12 +11068,6 @@ func init() {
"operator": {
"type": "boolean"
},
"pages": {
"type": "array",
"items": {
"type": "string"
}
},
"permissions": {
"type": "object",
"additionalProperties": {

View File

@@ -31,7 +31,8 @@ import (
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
"github.com/minio/console/pkg/acl"
"github.com/minio/console/pkg/auth/idp/oauth2"
"github.com/minio/console/pkg/auth/ldap"
"github.com/minio/console/restapi/operations"
"github.com/minio/console/restapi/operations/user_api"
)
@@ -111,15 +112,6 @@ 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 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 val from there
if policy != nil {
actions = acl.GetActionsStringFromPolicy(policy)
}
currTime := time.Now().UTC()
// This actions will be global, meaning has to be attached to all resources
@@ -229,7 +221,6 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
return nil, prepareError(err)
}
sessionResp := &models.SessionResponse{
Pages: acl.GetAuthorizedEndpoints(actions),
Features: getListOfEnabledFeatures(),
Status: models.SessionResponseStatusOk,
Operator: false,
@@ -241,12 +232,20 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
// getListOfEnabledFeatures returns a list of features
func getListOfEnabledFeatures() []string {
var features []string
features := []string{}
logSearchURL := getLogSearchURL()
oidcEnabled := oauth2.IsIDPEnabled()
ldapEnabled := ldap.GetLDAPEnabled()
if logSearchURL != "" {
features = append(features, "log-search")
}
if oidcEnabled {
features = append(features, "oidc-idp", "external-idp")
}
if ldapEnabled {
features = append(features, "ldap-idp", "external-idp")
}
return features
}

View File

@@ -3128,10 +3128,6 @@ definitions:
sessionResponse:
type: object
properties:
pages:
type: array
items:
type: string
features:
type: array
items:

View File

@@ -978,14 +978,6 @@ definitions:
operatorSessionResponse:
type: object
properties:
pages:
type: array
items:
type: string
features:
type: array
items:
type: string
status:
type: string
enum: [ ok ]