diff --git a/Dockerfile b/Dockerfile index c5321e662..315c950d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ WORKDIR /go/src/github.com/minio/console/ ENV CGO_ENABLED=0 COPY --from=uilayer /app/build /go/src/github.com/minio/console/portal-ui/build -RUN go build -ldflags "-w -s" -a -o console ./cmd/console +RUN go build --tags=kqueue,operator -ldflags "-w -s" -a -o console ./cmd/console FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5 MAINTAINER MinIO Development "dev@min.io" diff --git a/models/login_request.go b/models/login_request.go index efd9eaa25..85793e977 100644 --- a/models/login_request.go +++ b/models/login_request.go @@ -40,6 +40,9 @@ type LoginRequest struct { // Required: true AccessKey *string `json:"accessKey"` + // features + Features *LoginRequestFeatures `json:"features,omitempty"` + // secret key // Required: true SecretKey *string `json:"secretKey"` @@ -53,6 +56,10 @@ func (m *LoginRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateFeatures(formats); err != nil { + res = append(res, err) + } + if err := m.validateSecretKey(formats); err != nil { res = append(res, err) } @@ -72,6 +79,25 @@ func (m *LoginRequest) validateAccessKey(formats strfmt.Registry) error { return nil } +func (m *LoginRequest) validateFeatures(formats strfmt.Registry) error { + if swag.IsZero(m.Features) { // not required + return nil + } + + if m.Features != nil { + if err := m.Features.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("features") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("features") + } + return err + } + } + + return nil +} + func (m *LoginRequest) validateSecretKey(formats strfmt.Registry) error { if err := validate.Required("secretKey", "body", m.SecretKey); err != nil { @@ -81,8 +107,33 @@ func (m *LoginRequest) validateSecretKey(formats strfmt.Registry) error { return nil } -// ContextValidate validates this login request based on context it is used +// ContextValidate validate this login request based on the context it is used func (m *LoginRequest) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + var res []error + + if err := m.contextValidateFeatures(ctx, formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *LoginRequest) contextValidateFeatures(ctx context.Context, formats strfmt.Registry) error { + + if m.Features != nil { + if err := m.Features.ContextValidate(ctx, formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("features") + } else if ce, ok := err.(*errors.CompositeError); ok { + return ce.ValidateName("features") + } + return err + } + } + return nil } @@ -103,3 +154,40 @@ func (m *LoginRequest) UnmarshalBinary(b []byte) error { *m = res return nil } + +// LoginRequestFeatures login request features +// +// swagger:model LoginRequestFeatures +type LoginRequestFeatures struct { + + // hide menu + HideMenu bool `json:"hide_menu,omitempty"` +} + +// Validate validates this login request features +func (m *LoginRequestFeatures) Validate(formats strfmt.Registry) error { + return nil +} + +// ContextValidate validates this login request features based on context it is used +func (m *LoginRequestFeatures) ContextValidate(ctx context.Context, formats strfmt.Registry) error { + return nil +} + +// MarshalBinary interface implementation +func (m *LoginRequestFeatures) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *LoginRequestFeatures) UnmarshalBinary(b []byte) error { + var res LoginRequestFeatures + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/models/principal.go b/models/principal.go index d9b2c1687..231e24d23 100644 --- a/models/principal.go +++ b/models/principal.go @@ -45,6 +45,9 @@ type Principal struct { // account access key AccountAccessKey string `json:"accountAccessKey,omitempty"` + + // Hide Console Menu + Hm bool `json:"hm,omitempty"` } // Validate validates this principal diff --git a/operatorapi/embedded_spec.go b/operatorapi/embedded_spec.go index ce03ce2ea..6f65909ee 100644 --- a/operatorapi/embedded_spec.go +++ b/operatorapi/embedded_spec.go @@ -2605,6 +2605,14 @@ func init() { "accessKey": { "type": "string" }, + "features": { + "type": "object", + "properties": { + "hide_menu": { + "type": "boolean" + } + } + }, "secretKey": { "type": "string" } @@ -5812,6 +5820,14 @@ func init() { } } }, + "LoginRequestFeatures": { + "type": "object", + "properties": { + "hide_menu": { + "type": "boolean" + } + } + }, "NodeSelectorTermMatchExpressionsItems0": { "description": "A node selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", "type": "object", @@ -7043,6 +7059,14 @@ func init() { "accessKey": { "type": "string" }, + "features": { + "type": "object", + "properties": { + "hide_menu": { + "type": "boolean" + } + } + }, "secretKey": { "type": "string" } diff --git a/operatorapi/operator_login.go b/operatorapi/operator_login.go index 413df05d0..25fc5bc0d 100644 --- a/operatorapi/operator_login.go +++ b/operatorapi/operator_login.go @@ -83,7 +83,7 @@ func login(credentials restapi.ConsoleCredentialsI) (*string, error) { return nil, err } // if we made it here, the consoleCredentials work, generate a jwt with claims - token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey()) + token, err := auth.NewEncryptedTokenForClient(&tokens, credentials.GetAccountAccessKey(), nil) if err != nil { LogError("error authenticating user: %v", err) return nil, errInvalidCredentials diff --git a/operatorapi/proxy.go b/operatorapi/proxy.go index 835a9dc89..80dbd50cf 100644 --- a/operatorapi/proxy.go +++ b/operatorapi/proxy.go @@ -123,9 +123,12 @@ func serveProxy(responseWriter http.ResponseWriter, req *http.Request) { return } - data := map[string]string{ - "accessKey": string(tenantConfiguration["accesskey"]), - "secretKey": string(tenantConfiguration["secretkey"]), + data := map[string]interface{}{ + "accessKey": tenantConfiguration["accesskey"], + "secretKey": tenantConfiguration["secretkey"], + "features": map[string]bool{ + "hide_menu": true, + }, } payload, _ := json.Marshal(data) diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 1a9baf58c..1cda95da1 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -66,6 +66,12 @@ type TokenClaims struct { STSSecretAccessKey string `json:"stsSecretAccessKey,omitempty"` STSSessionToken string `json:"stsSessionToken,omitempty"` AccountAccessKey string `json:"accountAccessKey,omitempty"` + HideMenu bool `json:"hm,omitempty"` +} + +// SessionFeatures represents features stored in the session +type SessionFeatures struct { + HideMenu bool } // SessionTokenAuthenticate takes a session token, decode it, extract claims and validate the signature @@ -96,14 +102,18 @@ func SessionTokenAuthenticate(token string) (*TokenClaims, error) { // NewEncryptedTokenForClient generates a new session token with claims based on the provided STS credentials, first // encrypts the claims and the sign them -func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey string) (string, error) { +func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey string, features *SessionFeatures) (string, error) { if credentials != nil { - encryptedClaims, err := encryptClaims(&TokenClaims{ + tokenClaims := &TokenClaims{ STSAccessKeyID: credentials.AccessKeyID, STSSecretAccessKey: credentials.SecretAccessKey, STSSessionToken: credentials.SessionToken, AccountAccessKey: accountAccessKey, - }) + } + if features != nil { + tokenClaims.HideMenu = features.HideMenu + } + encryptedClaims, err := encryptClaims(tokenClaims) if err != nil { return "", err } diff --git a/pkg/auth/token_test.go b/pkg/auth/token_test.go index d0263042c..58cd077bd 100644 --- a/pkg/auth/token_test.go +++ b/pkg/auth/token_test.go @@ -36,14 +36,14 @@ func TestNewJWTWithClaimsForClient(t *testing.T) { funcAssert := assert.New(t) // Test-1 : NewEncryptedTokenForClient() is generated correctly without errors function := "NewEncryptedTokenForClient()" - token, err := NewEncryptedTokenForClient(creds, "") + token, err := NewEncryptedTokenForClient(creds, "", nil) if err != nil || token == "" { t.Errorf("Failed on %s:, error occurred: %s", function, err) } // saving token for future tests goodToken = token // Test-2 : NewEncryptedTokenForClient() throws error because of empty credentials - if _, err = NewEncryptedTokenForClient(nil, ""); err != nil { + if _, err = NewEncryptedTokenForClient(nil, "", nil); err != nil { funcAssert.Equal("provided credentials are empty", err.Error()) } } diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index 8b2254a11..63986fcf9 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -129,8 +129,7 @@ export const IAM_PAGES = { TOOLS_LOGS: "/tools/logs", TOOLS_AUDITLOGS: "/tools/audit-logs", TOOLS_TRACE: "/tools/trace", - METRICS: "/tools/metrics", - DASHBOARD: "/tools/dashboard", + DASHBOARD: "/tools/metrics", TOOLS_HEAL: "/tools/heal", TOOLS_WATCH: "/tools/watch", /* Health */ @@ -178,6 +177,8 @@ export const IAM_PAGES = { "/namespaces/:tenantNamespace/tenants/:tenantName/summary", NAMESPACE_TENANT_METRICS: "/namespaces/:tenantNamespace/tenants/:tenantName/metrics", + NAMESPACE_TENANT_TRACE: + "/namespaces/:tenantNamespace/tenants/:tenantName/trace", NAMESPACE_TENANT_POOLS: "/namespaces/:tenantNamespace/tenants/:tenantName/pools", NAMESPACE_TENANT_VOLUMES: @@ -297,9 +298,6 @@ export const IAM_PAGES_PERMISSIONS = { [IAM_PAGES.DASHBOARD]: [ IAM_SCOPES.ADMIN_SERVER_INFO, // displays dashboard information ], - [IAM_PAGES.METRICS]: [ - IAM_SCOPES.ADMIN_SERVER_INFO, // displays dashboard information - ], [IAM_PAGES.POLICIES_VIEW]: [ IAM_SCOPES.ADMIN_DELETE_POLICY, IAM_SCOPES.ADMIN_LIST_GROUPS, diff --git a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx index e8f3a1840..099df0175 100644 --- a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx +++ b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React from "react"; +import React, { Fragment } from "react"; import { Theme } from "@mui/material/styles"; import { connect } from "react-redux"; import Grid from "@mui/material/Grid"; @@ -38,6 +38,7 @@ interface IPageHeader { managerObjects?: IFileItem[]; toggleList: typeof toggleList; middleComponent?: React.ReactNode; + features: string[]; } const styles = (theme: Theme) => @@ -88,7 +89,11 @@ const PageHeader = ({ managerObjects, toggleList, middleComponent, + features, }: IPageHeader) => { + if (features.includes("hide-menu")) { + return ; + } return ( ({ sidebarOpen: state.system.sidebarOpen, operatorMode: state.system.operatorMode, managerObjects: state.objectBrowser.objectManager.objectsToManage, + features: state.console.session.features, }); const mapDispatchToProps = { diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 81803e8bc..4dcd35e83 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -56,7 +56,6 @@ const Heal = React.lazy(() => import("./Heal/Heal")); const Watch = React.lazy(() => import("./Watch/Watch")); const HealthInfo = React.lazy(() => import("./HealthInfo/HealthInfo")); const Storage = React.lazy(() => import("./Storage/Storage")); -const Metrics = React.lazy(() => import("./Dashboard/Metrics")); const Hop = React.lazy(() => import("./Tenants/TenantDetails/hop/Hop")); const AddTenant = React.lazy(() => import("./Tenants/AddTenant/AddTenant")); @@ -211,10 +210,6 @@ const Console = ({ component: Dashboard, path: IAM_PAGES.DASHBOARD, }, - { - component: Metrics, - path: IAM_PAGES.METRICS, - }, { component: Buckets, path: IAM_PAGES.ADD_BUCKETS, @@ -433,6 +428,11 @@ const Console = ({ path: IAM_PAGES.NAMESPACE_TENANT_METRICS, forceDisplay: true, }, + { + component: TenantDetails, + path: IAM_PAGES.NAMESPACE_TENANT_TRACE, + forceDisplay: true, + }, { component: TenantDetails, path: IAM_PAGES.NAMESPACE_TENANT_PODS_LIST, @@ -513,7 +513,7 @@ const Console = ({ const location = useLocation(); let hideMenu = false; - if (location.pathname === IAM_PAGES.METRICS) { + if (features?.includes("hide-menu")) { hideMenu = true; } else if (location.pathname.endsWith("/hop")) { hideMenu = true; diff --git a/portal-ui/src/screens/Console/ConsoleKBar.tsx b/portal-ui/src/screens/Console/ConsoleKBar.tsx index 6e3745d96..51db80ac1 100644 --- a/portal-ui/src/screens/Console/ConsoleKBar.tsx +++ b/portal-ui/src/screens/Console/ConsoleKBar.tsx @@ -84,6 +84,10 @@ const ConsoleKBar = ({ operatorMode: boolean; features: string[] | null; }) => { + if (features?.includes("hide-menu")) { + return ; + } + const allowedMenuItems = validRoutes(features, operatorMode); const initialActions = []; diff --git a/portal-ui/src/screens/Console/Dashboard/Metrics.tsx b/portal-ui/src/screens/Console/Dashboard/Metrics.tsx deleted file mode 100644 index 39c1809fd..000000000 --- a/portal-ui/src/screens/Console/Dashboard/Metrics.tsx +++ /dev/null @@ -1,91 +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 React, { Fragment, useCallback, useEffect, useState } from "react"; -import { connect } from "react-redux"; -import get from "lodash/get"; -import Grid from "@mui/material/Grid"; -import { Theme } from "@mui/material/styles"; -import createStyles from "@mui/styles/createStyles"; -import withStyles from "@mui/styles/withStyles"; -import { LinearProgress } from "@mui/material"; -import api from "../../../common/api"; -import { Usage } from "./types"; -import { setErrorSnackMessage } from "../../../actions"; -import { ErrorResponseHandler } from "../../../common/types"; -import PrDashboard from "./Prometheus/PrDashboard"; -import BasicDashboard from "./BasicDashboard/BasicDashboard"; - -interface IMetricsSimple { - classes: any; - displayErrorMessage: typeof setErrorSnackMessage; -} - -const styles = (theme: Theme) => createStyles({}); - -const Metrics = ({ classes, displayErrorMessage }: IMetricsSimple) => { - const [loading, setLoading] = useState(true); - const [basicResult, setBasicResult] = useState(null); - - const fetchUsage = useCallback(() => { - api - .invoke("GET", `/api/v1/admin/info`) - .then((res: Usage) => { - setBasicResult(res); - setLoading(false); - }) - .catch((err: ErrorResponseHandler) => { - displayErrorMessage(err); - setLoading(false); - }); - }, [setBasicResult, setLoading, displayErrorMessage]); - - useEffect(() => { - if (loading) { - fetchUsage(); - } - }, [loading, fetchUsage]); - - const widgets = get(basicResult, "widgets", null); - - return ( - - - {loading ? ( - - - - ) : ( - - {widgets !== null ? ( - - - - ) : ( - - )} - - )} - - - ); -}; - -const connector = connect(null, { - displayErrorMessage: setErrorSnackMessage, -}); - -export default withStyles(styles)(connector(Metrics)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx index b36c5ac50..ab2307a3c 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/TenantDetails.tsx @@ -50,7 +50,6 @@ import BackLink from "../../../../common/BackLink"; import VerticalTabs from "../../Common/VerticalTabs/VerticalTabs"; import BoxIconButton from "../../Common/BoxIconButton/BoxIconButton"; import withSuspense from "../../Common/Components/withSuspense"; -import PVCDetails from "./pvcs/PVCDetails"; const TenantYAML = withSuspense(React.lazy(() => import("./TenantYAML"))); const TenantSummary = withSuspense(React.lazy(() => import("./TenantSummary"))); @@ -63,6 +62,10 @@ const VolumesSummary = withSuspense( React.lazy(() => import("./VolumesSummary")) ); const TenantMetrics = withSuspense(React.lazy(() => import("./TenantMetrics"))); +const TenantTrace = withSuspense(React.lazy(() => import("./TenantTrace"))); +const TenantVolumes = withSuspense( + React.lazy(() => import("./pvcs/TenantVolumes")) +); const TenantSecurity = withSuspense( React.lazy(() => import("./TenantSecurity")) ); @@ -424,6 +427,10 @@ const TenantDetails = ({ path="/namespaces/:tenantNamespace/tenants/:tenantName/metrics" component={TenantMetrics} /> + {