Add feature hide-menu for embedded screens on Operator UI (#1604)

* Add feature hide-menu for embedded screens on Operator UI

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-02-21 21:42:18 -08:00
committed by GitHub
parent 28dcd19dd9
commit 56c4311a6b
27 changed files with 381 additions and 166 deletions

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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())
}
}

View File

@@ -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,

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React 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 <Fragment />;
}
return (
<Grid
container
@@ -157,6 +162,7 @@ const mapState = (state: AppState) => ({
sidebarOpen: state.system.sidebarOpen,
operatorMode: state.system.operatorMode,
managerObjects: state.objectBrowser.objectManager.objectsToManage,
features: state.console.session.features,
});
const mapDispatchToProps = {

View File

@@ -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;

View File

@@ -84,6 +84,10 @@ const ConsoleKBar = ({
operatorMode: boolean;
features: string[] | null;
}) => {
if (features?.includes("hide-menu")) {
return <Console />;
}
const allowedMenuItems = validRoutes(features, operatorMode);
const initialActions = [];

View File

@@ -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 <http://www.gnu.org/licenses/>.
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<boolean>(true);
const [basicResult, setBasicResult] = useState<Usage | null>(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 (
<Fragment>
<Grid container>
{loading ? (
<Grid item xs={12}>
<LinearProgress />
</Grid>
) : (
<Fragment>
{widgets !== null ? (
<Grid container>
<PrDashboard />
</Grid>
) : (
<BasicDashboard usage={basicResult} />
)}
</Fragment>
)}
</Grid>
</Fragment>
);
};
const connector = connect(null, {
displayErrorMessage: setErrorSnackMessage,
});
export default withStyles(styles)(connector(Metrics));

View File

@@ -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}
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/trace"
component={TenantTrace}
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/security"
component={TenantSecurity}
@@ -442,7 +449,7 @@ const TenantDetails = ({
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/pvcs/:PVCName"
component={PVCDetails}
component={TenantVolumes}
/>
<Route
path="/namespaces/:tenantNamespace/tenants/:tenantName/volumes"

View File

@@ -28,6 +28,7 @@ import { ITenant } from "../ListTenants/types";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import { LinearProgress } from "@mui/material";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
interface ITenantMetrics {
classes: any;
@@ -66,7 +67,7 @@ const TenantMetrics = ({ classes, match }: ITenantMetrics) => {
<iframe
className={classes.iframeStyle}
title={"metrics"}
src={`/api/proxy/${tenantNamespace}/${tenantName}/metrics?cp=y`}
src={`/api/proxy/${tenantNamespace}/${tenantName}${IAM_PAGES.DASHBOARD}?cp=y`}
onLoad={() => {
setLoading(false);
}}

View File

@@ -0,0 +1,106 @@
// 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, { useState } from "react";
import { connect } from "react-redux";
import get from "lodash/get";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
containerForHeader,
tenantDetailsStyles,
} from "../../Common/FormComponents/common/styleLibrary";
import { ITenant } from "../ListTenants/types";
import { setErrorSnackMessage } from "../../../../actions";
import { AppState } from "../../../../store";
import { LinearProgress } from "@mui/material";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
interface ITenantTrace {
classes: any;
match: any;
tenant: ITenant | null;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
...tenantDetailsStyles,
iframeStyle: {
border: "0px",
flex: "1 1 auto",
minHeight: "800px",
width: "100%",
},
...containerForHeader(theme.spacing(4)),
});
const TenantTrace = ({ classes, match }: ITenantTrace) => {
const tenantName = match.params["tenantName"];
const tenantNamespace = match.params["tenantNamespace"];
const [loading, setLoading] = useState<boolean>(true);
return (
<React.Fragment>
<h1 className={classes.sectionTitle}>Metrics</h1>
{loading && (
<div style={{ marginTop: "80px" }}>
<LinearProgress />
</div>
)}
<iframe
className={classes.iframeStyle}
title={"metrics"}
src={`/api/proxy/${tenantNamespace}/${tenantName}${IAM_PAGES.TOOLS_TRACE}?cp=y`}
onLoad={() => {
setLoading(false);
}}
/>
</React.Fragment>
);
};
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
selectedTenant: state.tenants.tenantDetails.currentTenant,
tenant: state.tenants.tenantDetails.tenantInfo,
logEnabled: get(state.tenants.tenantDetails.tenantInfo, "logEnabled", false),
monitoringEnabled: get(
state.tenants.tenantDetails.tenantInfo,
"monitoringEnabled",
false
),
encryptionEnabled: get(
state.tenants.tenantDetails.tenantInfo,
"encryptionEnabled",
false
),
adEnabled: get(state.tenants.tenantDetails.tenantInfo, "idpAdEnabled", false),
oidcEnabled: get(
state.tenants.tenantDetails.tenantInfo,
"idpOidcEnabled",
false
),
});
const connector = connect(mapState, {
setErrorSnackMessage,
});
export default withStyles(styles)(connector(TenantTrace));

View File

@@ -43,7 +43,7 @@ const styles = (theme: Theme) =>
...containerForHeader(theme.spacing(4)),
});
const PVCDetails = ({
const TenantVolumes = ({
classes,
match,
setErrorSnackMessage,
@@ -98,4 +98,4 @@ const PVCDetails = ({
);
};
export default withStyles(styles)(PVCDetails);
export default withStyles(styles)(TenantVolumes);

View File

@@ -23,6 +23,6 @@ fixture("For user with default permissions").page("http://localhost:9090");
test("Login to Operator Web Page", async (t) => {
await t
.navigateTo("http://localhost:9090/login")
.typeText("#jwt","anyrandompasswordwillwork")
.click("button.MuiButton-root")
});
.typeText("#jwt", "anyrandompasswordwillwork")
.click("button.MuiButton-root");
});

View File

@@ -16,6 +16,7 @@
import * as constants from "./constants";
import { Selector } from "testcafe";
import { IAM_PAGES } from "../../src/common/SecureComponent/permissions";
//----------------------------------------------------
// General sidebar element
@@ -32,7 +33,7 @@ export const monitoringElement = Selector(".MuiPaper-root")
export const monitoringChildren = Selector("#tools-children");
export const dashboardElement = monitoringChildren
.find("a")
.withAttribute("href", "/tools/dashboard");
.withAttribute("href", IAM_PAGES.DASHBOARD);
export const logsElement = monitoringChildren
.find("a")
.withAttribute("href", "/tools/logs");

View File

@@ -79,6 +79,7 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
STSSecretAccessKey: claims.STSSecretAccessKey,
STSSessionToken: claims.STSSessionToken,
AccountAccessKey: claims.AccountAccessKey,
Hm: claims.HideMenu,
}, nil
}

View File

@@ -5018,6 +5018,14 @@ func init() {
"accessKey": {
"type": "string"
},
"features": {
"type": "object",
"properties": {
"hide_menu": {
"type": "boolean"
}
}
},
"secretKey": {
"type": "string"
}
@@ -5467,6 +5475,10 @@ func init() {
},
"accountAccessKey": {
"type": "string"
},
"hm": {
"description": "Hide Console Menu",
"type": "boolean"
}
}
},
@@ -10422,6 +10434,14 @@ func init() {
}
}
},
"LoginRequestFeatures": {
"type": "object",
"properties": {
"hide_menu": {
"type": "boolean"
}
}
},
"SubnetRegTokenResponse": {
"type": "object",
"properties": {
@@ -11583,6 +11603,14 @@ func init() {
"accessKey": {
"type": "string"
},
"features": {
"type": "object",
"properties": {
"hide_menu": {
"type": "boolean"
}
}
},
"secretKey": {
"type": "string"
}
@@ -12032,6 +12060,10 @@ func init() {
},
"accountAccessKey": {
"type": "string"
},
"hm": {
"description": "Hide Console Menu",
"type": "boolean"
}
}
},

View File

@@ -21,6 +21,8 @@ import (
"net/http"
"time"
"github.com/minio/console/pkg/auth"
"github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware"
"github.com/minio/console/models"
@@ -80,7 +82,7 @@ func getChangePasswordResponse(session *models.Principal, params user_api.Accoun
return nil, prepareError(errInvalidCredentials, nil, err)
}
// authenticate user and generate new session token
sessionID, err := login(credentials)
sessionID, err := login(credentials, &auth.SessionFeatures{HideMenu: session.Hm})
if err != nil {
return nil, prepareError(errInvalidCredentials, nil, err)
}

View File

@@ -74,14 +74,14 @@ func registerLoginHandlers(api *operations.ConsoleAPI) {
// login performs a check of ConsoleCredentials against MinIO, generates some claims and returns the jwt
// for subsequent authentication
func login(credentials ConsoleCredentialsI) (*string, error) {
func login(credentials ConsoleCredentialsI, sessionFeatures *auth.SessionFeatures) (*string, error) {
// try to obtain consoleCredentials,
tokens, err := credentials.Get()
if err != nil {
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(), sessionFeatures)
if err != nil {
LogError("error authenticating user: %v", err)
return nil, errInvalidCredentials
@@ -115,11 +115,15 @@ func getConsoleCredentials(accessKey, secretKey string) (*ConsoleCredentials, er
// getLoginResponse performs login() and serializes it to the handler's output
func getLoginResponse(lr *models.LoginRequest) (*models.LoginResponse, *models.Error) {
// prepare console credentials
consolCreds, err := getConsoleCredentials(*lr.AccessKey, *lr.SecretKey)
consoleCreds, err := getConsoleCredentials(*lr.AccessKey, *lr.SecretKey)
if err != nil {
return nil, prepareError(err, errInvalidCredentials, err)
}
sessionID, err := login(consolCreds)
sf := &auth.SessionFeatures{}
if lr.Features != nil {
sf.HideMenu = lr.Features.HideMenu
}
sessionID, err := login(consoleCreds, sf)
if err != nil {
return nil, prepareError(err, errInvalidCredentials, err)
}
@@ -185,7 +189,7 @@ func getLoginOauth2AuthResponse(r *http.Request, lr *models.LoginOauth2AuthReque
token, err := login(&ConsoleCredentials{
ConsoleCredentials: userCredentials,
AccountAccessKey: "",
})
}, nil)
if err != nil {
return nil, prepareError(err)
}

View File

@@ -65,7 +65,7 @@ func TestLogin(t *testing.T) {
SignerType: 0,
}, nil
}
token, err := login(consoleCredentials)
token, err := login(consoleCredentials, nil)
funcAssert.NotEmpty(token, "Token was returned empty")
funcAssert.Nil(err, "error creating a session")
@@ -73,7 +73,7 @@ func TestLogin(t *testing.T) {
consoleCredentialsGetMock = func() (credentials.Value, error) {
return credentials.Value{}, errors.New("")
}
_, err = login(consoleCredentials)
_, err = login(consoleCredentials, nil)
funcAssert.NotNil(err, "not error returned creating a session")
}

View File

@@ -220,7 +220,7 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
return nil, prepareError(err)
}
sessionResp := &models.SessionResponse{
Features: getListOfEnabledFeatures(),
Features: getListOfEnabledFeatures(session),
Status: models.SessionResponseStatusOk,
Operator: false,
DistributedMode: isErasureMode(),
@@ -230,7 +230,7 @@ func getSessionResponse(session *models.Principal) (*models.SessionResponse, *mo
}
// getListOfEnabledFeatures returns a list of features
func getListOfEnabledFeatures() []string {
func getListOfEnabledFeatures(session *models.Principal) []string {
features := []string{}
logSearchURL := getLogSearchURL()
oidcEnabled := oauth2.IsIDPEnabled()
@@ -246,5 +246,9 @@ func getListOfEnabledFeatures() []string {
features = append(features, "ldap-idp", "external-idp")
}
if session.Hm {
features = append(features, "hide-menu")
}
return features
}

View File

@@ -19,7 +19,7 @@ securityDefinitions:
tokenUrl: http://min.io
# Apply the key security definition to all APIs
security:
- key: []
- key: [ ]
paths:
/login:
get:
@@ -35,7 +35,7 @@ paths:
schema:
$ref: "#/definitions/error"
# Exclude this API from the authentication requirement
security: []
security: [ ]
tags:
- UserAPI
post:
@@ -55,7 +55,7 @@ paths:
schema:
$ref: "#/definitions/error"
# Exclude this API from the authentication requirement
security: []
security: [ ]
tags:
- UserAPI
/login/oauth2/auth:
@@ -75,7 +75,7 @@ paths:
description: Generic error response.
schema:
$ref: "#/definitions/error"
security: []
security: [ ]
tags:
- UserAPI
@@ -2579,7 +2579,7 @@ paths:
- name: order
in: query
type: string
enum: [timeDesc, timeAsc]
enum: [ timeDesc, timeAsc ]
default: timeDesc
- name: timeStart
in: query
@@ -3383,7 +3383,7 @@ definitions:
properties:
loginStrategy:
type: string
enum: [form, redirect, service-account]
enum: [ form, redirect, service-account ]
redirect:
type: string
loginOauth2AuthRequest:
@@ -3406,6 +3406,11 @@ definitions:
type: string
secretKey:
type: string
features:
type: object
properties:
hide_menu:
type: boolean
loginResponse:
type: object
properties:
@@ -3423,6 +3428,8 @@ definitions:
type: string
accountAccessKey:
type: string
hm:
type: boolean
startProfilingItem:
type: object
properties:
@@ -3469,7 +3476,7 @@ definitions:
type: string
status:
type: string
enum: [ok]
enum: [ ok ]
operator:
type: boolean
distributedMode:
@@ -3490,7 +3497,7 @@ definitions:
type: string
values:
type: array
items: {}
items: { }
resultTarget:
type: object
properties:
@@ -3746,7 +3753,7 @@ definitions:
secretKey:
type: string
url:
type: string
type: string
remoteBucket:
type: object
required:
@@ -3772,7 +3779,7 @@ definitions:
type: string
service:
type: string
enum: [replication]
enum: [ replication ]
syncMode:
type: string
bandwidth:

View File

@@ -19,7 +19,7 @@ securityDefinitions:
tokenUrl: http://min.io
# Apply the key security definition to all APIs
security:
- key: []
- key: [ ]
paths:
/login:
get:
@@ -35,7 +35,7 @@ paths:
schema:
$ref: "#/definitions/error"
# Exclude this API from the authentication requirement
security: []
security: [ ]
tags:
- UserAPI
/login/operator:
@@ -55,7 +55,7 @@ paths:
description: Generic error response.
schema:
$ref: "#/definitions/error"
security: []
security: [ ]
tags:
- UserAPI
@@ -76,7 +76,7 @@ paths:
description: Generic error response.
schema:
$ref: "#/definitions/error"
security: []
security: [ ]
tags:
- UserAPI
@@ -781,13 +781,13 @@ paths:
200:
description: A successful response.
schema:
$ref: "#/definitions/tenantMonitoringInfo"
$ref: "#/definitions/tenantMonitoringInfo"
default:
description: Generic error response.
schema:
$ref: "#/definitions/error"
tags:
- OperatorAPI
- OperatorAPI
put:
summary: Set Prometheus monitoring fields for tenant
operationId: SetTenantMonitoring
@@ -813,7 +813,7 @@ paths:
schema:
$ref: "#/definitions/error"
tags:
- OperatorAPI
- OperatorAPI
/namespaces/{namespace}/tenants/{tenant}/certificates:
put:
@@ -1199,7 +1199,7 @@ definitions:
properties:
loginStrategy:
type: string
enum: [form, redirect, service-account]
enum: [ form, redirect, service-account ]
redirect:
type: string
loginRequest:
@@ -1212,6 +1212,11 @@ definitions:
type: string
secretKey:
type: string
features:
type: object
properties:
hide_menu:
type: boolean
loginOauth2AuthRequest:
type: object
required:
@@ -1238,7 +1243,7 @@ definitions:
type: string
status:
type: string
enum: [ok]
enum: [ ok ]
operator:
type: boolean
permissions:
@@ -1895,7 +1900,7 @@ definitions:
type: string
url:
type: string
tenantPod:
type: object
required:
@@ -2551,12 +2556,12 @@ definitions:
logCPURequest:
type: string
logMemRequest:
type: string
type: string
logDBCPURequest:
type: string
logDBMemRequest:
type: string
type: string
listPVCsResponse:
type: object
@@ -2622,14 +2627,14 @@ definitions:
type: string
message:
type: string
tenantMonitoringInfo:
type: object
properties:
prometheusEnabled:
type: boolean
type: boolean
toggle:
type: boolean
type: boolean
image:
type: string
sidecarImage:
@@ -2651,14 +2656,14 @@ definitions:
items:
$ref: "#/definitions/nodeSelector"
serviceAccountName:
type: string
type: string
storageClassName:
type: string
type: string
monitoringCPURequest:
type: string
type: string
monitoringMemRequest:
type: string
type: string
label:
type: object
properties:
@@ -2674,7 +2679,7 @@ definitions:
type: string
value:
type: string
nodeSelector:
type: object
properties:
@@ -2687,7 +2692,7 @@ definitions:
type: object
properties:
prometheusEnabled:
type: boolean
type: boolean
formatConfiguration:
type: object