Object Browser only mode (#2157)

- Added flag CONSOLE_OBJECT_BROWSER_ONLY=on to trigger between console mode & Object Browser only
- Hidden not necessary buttons for object browse
- STS Login

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Alex
2022-07-07 14:28:25 -05:00
committed by GitHub
parent e48958f5a0
commit cf0e326b82
22 changed files with 929 additions and 495 deletions

View File

@@ -276,7 +276,7 @@ jobs:
semgrep --config semgrep.yaml $(pwd)/portal-ui --error
no-warnings-and-make-assets:
name: "React Code Has No Warning & Prettified and then Make Assets"
name: "React Code Has No Warnings & is Prettified, then Make Assets"
runs-on: ${{ matrix.os }}
strategy:
matrix:

View File

@@ -28,7 +28,6 @@ import (
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
"github.com/go-openapi/validate"
)
// LoginRequest login request
@@ -37,48 +36,32 @@ import (
type LoginRequest struct {
// access key
// Required: true
AccessKey *string `json:"accessKey"`
AccessKey string `json:"accessKey,omitempty"`
// features
Features *LoginRequestFeatures `json:"features,omitempty"`
// secret key
// Required: true
SecretKey *string `json:"secretKey"`
SecretKey string `json:"secretKey,omitempty"`
// sts
Sts string `json:"sts,omitempty"`
}
// Validate validates this login request
func (m *LoginRequest) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateAccessKey(formats); err != nil {
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)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *LoginRequest) validateAccessKey(formats strfmt.Registry) error {
if err := validate.Required("accessKey", "body", m.AccessKey); err != nil {
return err
}
return nil
}
func (m *LoginRequest) validateFeatures(formats strfmt.Registry) error {
if swag.IsZero(m.Features) { // not required
return nil
@@ -98,15 +81,6 @@ func (m *LoginRequest) validateFeatures(formats strfmt.Registry) error {
return nil
}
func (m *LoginRequest) validateSecretKey(formats strfmt.Registry) error {
if err := validate.Required("secretKey", "body", m.SecretKey); err != nil {
return err
}
return nil
}
// 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

View File

@@ -48,6 +48,9 @@ type Principal struct {
// hm
Hm bool `json:"hm,omitempty"`
// ob
Ob bool `json:"ob,omitempty"`
}
// Validate validates this principal

View File

@@ -3288,10 +3288,6 @@ func init() {
},
"loginRequest": {
"type": "object",
"required": [
"accessKey",
"secretKey"
],
"properties": {
"accessKey": {
"type": "string"
@@ -3306,6 +3302,9 @@ func init() {
},
"secretKey": {
"type": "string"
},
"sts": {
"type": "string"
}
}
},
@@ -8801,10 +8800,6 @@ func init() {
},
"loginRequest": {
"type": "object",
"required": [
"accessKey",
"secretKey"
],
"properties": {
"accessKey": {
"type": "string"
@@ -8819,6 +8814,9 @@ func init() {
},
"secretKey": {
"type": "string"
},
"sts": {
"type": "string"
}
}
},

View File

@@ -67,11 +67,18 @@ type TokenClaims struct {
STSSessionToken string `json:"stsSessionToken,omitempty"`
AccountAccessKey string `json:"accountAccessKey,omitempty"`
HideMenu bool `json:"hm,omitempty"`
ObjectBrowser bool `json:"ob,omitempty"`
}
// STSClaims claims struct for STS Token
type STSClaims struct {
AccessKey string `json:"accessKey,omitempty"`
}
// SessionFeatures represents features stored in the session
type SessionFeatures struct {
HideMenu bool
HideMenu bool
ObjectBrowser bool
}
// SessionTokenAuthenticate takes a session token, decode it, extract claims and validate the signature
@@ -115,6 +122,7 @@ func NewEncryptedTokenForClient(credentials *credentials.Value, accountAccessKey
}
if features != nil {
tokenClaims.HideMenu = features.HideMenu
tokenClaims.ObjectBrowser = features.ObjectBrowser
}
encryptedClaims, err := encryptClaims(tokenClaims)
if err != nil {

View File

@@ -36,13 +36,15 @@ import {
IAM_ROLES,
IAM_SCOPES,
} from "../../../../common/SecureComponent/permissions";
import SearchBox from "../../Common/SearchBox";
import BackLink from "../../../../common/BackLink";
import {
setSearchObjects,
setSearchVersions,
setVersionsModeEnabled,
} from "../../ObjectBrowser/objectBrowserSlice";
import SearchBox from "../../Common/SearchBox";
import { selFeatures } from "../../consoleSlice";
import { LoginMinIOLogo } from "../../../../icons";
const styles = (theme: Theme) =>
createStyles({
@@ -67,9 +69,13 @@ const BrowserHandler = () => {
(state: AppState) => state.objectBrowser.searchVersions
);
const features = useSelector(selFeatures);
const bucketName = params.bucketName || "";
const internalPaths = get(params, "subpaths", "");
const obOnly = !!features?.includes("object-browser-only");
useEffect(() => {
dispatch(setVersionsModeEnabled({ status: false }));
}, [internalPaths, dispatch]);
@@ -78,59 +84,79 @@ const BrowserHandler = () => {
navigate(`/buckets/${bucketName}/admin`);
};
const searchBar = (
<Fragment>
{!versionsMode ? (
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<SearchBox
placeholder={"Start typing to filter objects in the bucket"}
onChange={(value) => {
dispatch(setSearchObjects(value));
}}
value={searchObjects}
/>
</SecureComponent>
) : (
<Fragment>
<SearchBox
placeholder={`Start typing to filter versions of ${versionedFile}`}
onChange={(value) => {
dispatch(setSearchVersions(value));
}}
value={searchVersions}
/>
</Fragment>
)}
</Fragment>
);
return (
<Fragment>
<PageHeader
label={<BackLink label={"Buckets"} to={IAM_PAGES.BUCKETS} />}
actions={
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Tooltip title={"Configure Bucket"}>
<IconButton
color="primary"
aria-label="Configure Bucket"
component="span"
onClick={openBucketConfiguration}
size="large"
>
<SettingsIcon />
</IconButton>
</Tooltip>
</SecureComponent>
}
middleComponent={
<Fragment>
{!versionsMode ? (
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<SearchBox
placeholder={"Start typing to filter objects in the bucket"}
onChange={(value) => {
dispatch(setSearchObjects(value));
}}
value={searchObjects}
/>
</SecureComponent>
) : (
<Fragment>
<SearchBox
placeholder={`Start typing to filter versions of ${versionedFile}`}
onChange={(value) => {
dispatch(setSearchVersions(value));
}}
value={searchVersions}
/>
</Fragment>
)}
</Fragment>
}
/>
{!obOnly ? (
<PageHeader
label={<BackLink label={"Buckets"} to={IAM_PAGES.BUCKETS} />}
actions={
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<Tooltip title={"Configure Bucket"}>
<IconButton
color="primary"
aria-label="Configure Bucket"
component="span"
onClick={openBucketConfiguration}
size="large"
>
<SettingsIcon />
</IconButton>
</Tooltip>
</SecureComponent>
}
middleComponent={searchBar}
/>
) : (
<Grid
container
sx={{
padding: "20px 32px 0",
}}
>
<Grid>
<LoginMinIOLogo
style={{ width: 105, marginRight: 30, marginTop: 10 }}
/>
</Grid>
<Grid item xs>
{searchBar}
</Grid>
</Grid>
)}
<Grid>
<ListObjects />
</Grid>

View File

@@ -163,6 +163,7 @@ interface IBucketListItem {
onSelect: (e: React.ChangeEvent<HTMLInputElement>) => void;
selected: boolean;
bulkSelect: boolean;
noManage?: boolean;
}
const BucketListItem = ({
@@ -171,6 +172,7 @@ const BucketListItem = ({
onSelect,
selected,
bulkSelect,
noManage = false,
}: IBucketListItem) => {
const usage = niceBytes(`${bucket.size}` || "0");
const usageScalar = usage.split(" ")[0];
@@ -236,24 +238,26 @@ const BucketListItem = ({
</Grid>
</Grid>
<Grid item xs={12} sm={5} className={classes.bucketActionButtons}>
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucket.name}
>
<Link
to={`/buckets/${bucket.name}/admin`}
style={{ textDecoration: "none" }}
{!noManage && (
<SecureComponent
scopes={IAM_PERMISSIONS[IAM_ROLES.BUCKET_ADMIN]}
resource={bucket.name}
>
<RBIconButton
tooltip={"Manage"}
onClick={() => {}}
text={"Manage"}
icon={<SettingsIcon />}
color={"primary"}
variant={"outlined"}
/>
</Link>
</SecureComponent>
<Link
to={`/buckets/${bucket.name}/admin`}
style={{ textDecoration: "none" }}
>
<RBIconButton
tooltip={"Manage"}
onClick={() => {}}
text={"Manage"}
icon={<SettingsIcon />}
color={"primary"}
variant={"outlined"}
/>
</Link>
</SecureComponent>
)}
<Link
to={`/buckets/${bucket.name}/browse`}
style={{ textDecoration: "none" }}

View File

@@ -27,6 +27,7 @@ import {
AddIcon,
BucketsIcon,
LifecycleConfigIcon,
LoginMinIOLogo,
SelectAllIcon,
} from "../../../../icons";
import {
@@ -57,6 +58,8 @@ import BulkLifecycleModal from "./BulkLifecycleModal";
import hasPermission from "../../../../common/SecureComponent/accessControl";
import { setErrorSnackMessage } from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import { useSelector } from "react-redux";
import { selFeatures } from "../../consoleSlice";
const styles = (theme: Theme) =>
createStyles({
@@ -98,9 +101,11 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
const [replicationModalOpen, setReplicationModalOpen] =
useState<boolean>(false);
const [lifecycleModalOpen, setLifecycleModalOpen] = useState<boolean>(false);
const [bulkSelect, setBulkSelect] = useState<boolean>(false);
const features = useSelector(selFeatures);
const obOnly = !!features?.includes("object-browser-only");
useEffect(() => {
if (loading) {
const fetchRecords = () => {
@@ -172,6 +177,7 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
onSelect={selectListBuckets}
selected={selectedBuckets.includes(bucket.name)}
bulkSelect={bulkSelect}
noManage={obOnly}
/>
);
}
@@ -209,9 +215,16 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
open={lifecycleModalOpen}
/>
)}
<PageHeader label={"Buckets"} />
{!obOnly && <PageHeader label={"Buckets"} />}
<PageLayout>
<Grid item xs={12} className={classes.actionsTray} display="flex">
{obOnly && (
<Grid item xs>
<LoginMinIOLogo
style={{ width: 105, marginRight: 15, marginTop: 10 }}
/>
</Grid>
)}
<SearchBox
onChange={setFilterBuckets}
placeholder="Search Buckets"
@@ -226,59 +239,63 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
alignItems={"center"}
justifyContent={"flex-end"}
>
<RBIconButton
tooltip={
bulkSelect ? "Unselect Buckets" : "Select Multiple Buckets"
}
onClick={() => {
setBulkSelect(!bulkSelect);
setSelectedBuckets([]);
}}
text={""}
icon={<SelectMultipleIcon />}
color={"primary"}
variant={bulkSelect ? "contained" : "outlined"}
/>
{!obOnly && (
<Fragment>
<RBIconButton
tooltip={
bulkSelect ? "Unselect Buckets" : "Select Multiple Buckets"
}
onClick={() => {
setBulkSelect(!bulkSelect);
setSelectedBuckets([]);
}}
text={""}
icon={<SelectMultipleIcon />}
color={"primary"}
variant={bulkSelect ? "contained" : "outlined"}
/>
{bulkSelect && (
<RBIconButton
tooltip={
selectedBuckets.length === filteredRecords.length
? "Unselect All Buckets"
: "Select All Buckets"
}
onClick={selectAllBuckets}
text={""}
icon={<SelectAllIcon />}
color={"primary"}
variant={"outlined"}
/>
{bulkSelect && (
<RBIconButton
tooltip={
selectedBuckets.length === filteredRecords.length
? "Unselect All Buckets"
: "Select All Buckets"
}
onClick={selectAllBuckets}
text={""}
icon={<SelectAllIcon />}
color={"primary"}
variant={"outlined"}
/>
)}
<RBIconButton
tooltip={"Set Lifecycle"}
onClick={() => {
setLifecycleModalOpen(true);
}}
text={""}
icon={<LifecycleConfigIcon />}
disabled={selectedBuckets.length === 0}
color={"primary"}
variant={"outlined"}
/>
<RBIconButton
tooltip={"Set Replication"}
onClick={() => {
setReplicationModalOpen(true);
}}
text={""}
icon={<MultipleBucketsIcon />}
disabled={selectedBuckets.length === 0}
color={"primary"}
variant={"outlined"}
/>
</Fragment>
)}
<RBIconButton
tooltip={"Set Lifecycle"}
onClick={() => {
setLifecycleModalOpen(true);
}}
text={""}
icon={<LifecycleConfigIcon />}
disabled={selectedBuckets.length === 0}
color={"primary"}
variant={"outlined"}
/>
<RBIconButton
tooltip={"Set Replication"}
onClick={() => {
setReplicationModalOpen(true);
}}
text={""}
icon={<MultipleBucketsIcon />}
disabled={selectedBuckets.length === 0}
color={"primary"}
variant={"outlined"}
/>
<RBIconButton
tooltip={"Refresh"}
onClick={() => {
@@ -290,17 +307,19 @@ const ListBuckets = ({ classes }: IListBucketsProps) => {
variant={"outlined"}
/>
<RBIconButton
tooltip={"Create Bucket"}
onClick={() => {
navigate(IAM_PAGES.ADD_BUCKETS);
}}
text={"Create Bucket"}
icon={<AddIcon />}
color={"primary"}
variant={"contained"}
disabled={!canCreateBucket}
/>
{!obOnly && (
<RBIconButton
tooltip={"Create Bucket"}
onClick={() => {
navigate(IAM_PAGES.ADD_BUCKETS);
}}
text={"Create Bucket"}
icon={<AddIcon />}
color={"primary"}
variant={"contained"}
disabled={!canCreateBucket}
/>
)}
</Grid>
</Grid>

View File

@@ -203,6 +203,8 @@ const Console = ({ classes }: IConsoleProps) => {
const [openSnackbar, setOpenSnackbar] = useState<boolean>(false);
const ldapIsEnabled = (features && features.includes("ldap-idp")) || false;
const obOnly = !!features?.includes("object-browser-only");
const restartServer = () => {
dispatch(serverIsLoading(true));
api
@@ -461,16 +463,17 @@ const Console = ({ classes }: IConsoleProps) => {
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
).filter((route: any) =>
obOnly
? route.path.includes("buckets")
: (route.forceDisplay ||
(route.customPermissionFnc
? route.customPermissionFnc()
: hasPermission(
CONSOLE_UI_RESOURCE,
IAM_PAGES_PERMISSIONS[route.path]
))) &&
!route.fsHidden
);
const closeSnackBar = () => {
@@ -494,6 +497,8 @@ const Console = ({ classes }: IConsoleProps) => {
hideMenu = true;
} else if (pathname.endsWith("/hop")) {
hideMenu = true;
} else if (obOnly) {
hideMenu = true;
}
return (

View File

@@ -0,0 +1,68 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 makeStyles from "@mui/styles/makeStyles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { TextFieldProps } from "@mui/material";
import TextField from "@mui/material/TextField";
import React from "react";
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
"& .MuiOutlinedInput-root": {
paddingLeft: 0,
"& svg": {
marginLeft: 4,
height: 14,
color: theme.palette.primary.main,
},
"& input": {
padding: 10,
fontSize: 14,
paddingLeft: 0,
"&::placeholder": {
fontSize: 12,
},
"@media (max-width: 900px)": {
padding: 10,
},
},
"& fieldset": {},
"& fieldset:hover": {
borderBottom: "2px solid #000000",
borderRadius: 0,
},
},
},
})
);
export const LoginField = (props: TextFieldProps) => {
const classes = inputStyles();
return (
<TextField
classes={{
root: classes.root,
}}
variant="standard"
{...props}
/>
);
};

View File

@@ -14,25 +14,16 @@
// 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, { useEffect, useState } from "react";
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
Box,
InputAdornment,
LinearProgress,
TextFieldProps,
} from "@mui/material";
import { Box, InputAdornment, LinearProgress } from "@mui/material";
import { Theme, useTheme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles";
import withStyles from "@mui/styles/withStyles";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Grid from "@mui/material/Grid";
import { ILoginDetails, loginStrategyType } from "./types";
import { ErrorResponseHandler } from "../../common/types";
import api from "../../common/api";
import { loginStrategyType } from "./types";
import RefreshIcon from "../../icons/RefreshIcon";
import MainError from "../Console/Common/MainError/MainError";
import {
@@ -45,20 +36,22 @@ import {
} from "../../icons";
import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary";
import CssBaseline from "@mui/material/CssBaseline";
import LockFilledIcon from "../../icons/LockFilledIcon";
import UserFilledIcon from "../../icons/UsersFilledIcon";
import { SupportMenuIcon } from "../../icons/SidebarMenus";
import GithubIcon from "../../icons/GithubIcon";
import clsx from "clsx";
import Loader from "../Console/Common/Loader/Loader";
import { AppState, useAppDispatch } from "../../store";
import { useSelector } from "react-redux";
import {
setErrorSnackMessage,
userLogged,
showMarketplace,
} from "../../systemSlice";
import { useAppDispatch } from "../../store";
doLoginAsync,
getFetchConfigurationAsync,
getVersionAsync,
} from "./loginThunks";
import { resetForm, setJwt } from "./loginSlice";
import StrategyForm from "./StrategyForm";
import { LoginField } from "./LoginField";
const styles = (theme: Theme) =>
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
position: "absolute",
@@ -78,18 +71,6 @@ const styles = (theme: Theme) =>
boxShadow: "none",
padding: "16px 30px",
},
learnMore: {
textAlign: "center",
fontSize: 10,
"& a": {
color: "#2781B0",
},
"& .min-icon": {
marginLeft: 12,
marginTop: 2,
width: 10,
},
},
separator: {
marginLeft: 8,
marginRight: 8,
@@ -227,298 +208,89 @@ const styles = (theme: Theme) =>
},
},
...spacingUtils,
});
const inputStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
"& .MuiOutlinedInput-root": {
paddingLeft: 0,
"& svg": {
marginLeft: 4,
height: 14,
color: theme.palette.primary.main,
},
"& input": {
padding: 10,
fontSize: 14,
paddingLeft: 0,
"&::placeholder": {
fontSize: 12,
},
"@media (max-width: 900px)": {
padding: 10,
},
},
"& fieldset": {},
"& fieldset:hover": {
borderBottom: "2px solid #000000",
borderRadius: 0,
},
},
},
})
);
function LoginField(props: TextFieldProps) {
const classes = inputStyles();
return (
<TextField
classes={{
root: classes.root,
}}
variant="standard"
{...props}
/>
);
}
// The inferred type will look like:
// {isOn: boolean, toggleOn: () => void}
interface ILoginProps {
classes: any;
}
interface LoginStrategyRoutes {
export interface LoginStrategyRoutes {
[key: string]: string;
}
interface LoginStrategyPayload {
export interface LoginStrategyPayload {
[key: string]: any;
}
const Login = ({ classes }: ILoginProps) => {
export const loginStrategyEndpoints: LoginStrategyRoutes = {
form: "/api/v1/login",
"service-account": "/api/v1/login/operator",
};
export const getTargetPath = () => {
let targetPath = "/";
if (
localStorage.getItem("redirect-path") &&
localStorage.getItem("redirect-path") !== ""
) {
targetPath = `${localStorage.getItem("redirect-path")}`;
localStorage.setItem("redirect-path", "");
}
return targetPath;
};
const Login = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const classes = useStyles();
const [accessKey, setAccessKey] = useState<string>("");
const [jwt, setJwt] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [loginStrategy, setLoginStrategy] = useState<ILoginDetails>({
loginStrategy: loginStrategyType.unknown,
redirect: "",
});
const [loginSending, setLoginSending] = useState<boolean>(false);
const [loadingFetchConfiguration, setLoadingFetchConfiguration] =
useState<boolean>(true);
const jwt = useSelector((state: AppState) => state.login.jwt);
const loginStrategy = useSelector(
(state: AppState) => state.login.loginStrategy
);
const loginSending = useSelector(
(state: AppState) => state.login.loginSending
);
const loadingFetchConfiguration = useSelector(
(state: AppState) => state.login.loadingFetchConfiguration
);
const [latestMinIOVersion, setLatestMinIOVersion] = useState<string>("");
const [loadingVersion, setLoadingVersion] = useState<boolean>(true);
const latestMinIOVersion = useSelector(
(state: AppState) => state.login.latestMinIOVersion
);
const loadingVersion = useSelector(
(state: AppState) => state.login.loadingVersion
);
const navigateTo = useSelector((state: AppState) => state.login.navigateTo);
const isOperator =
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
const loginStrategyEndpoints: LoginStrategyRoutes = {
form: "/api/v1/login",
"service-account": "/api/v1/login/operator",
};
const loginStrategyPayload: LoginStrategyPayload = {
form: { accessKey, secretKey },
"service-account": { jwt },
};
const fetchConfiguration = () => {
setLoadingFetchConfiguration(true);
};
const getTargetPath = () => {
let targetPath = "/";
if (
localStorage.getItem("redirect-path") &&
localStorage.getItem("redirect-path") !== ""
) {
targetPath = `${localStorage.getItem("redirect-path")}`;
localStorage.setItem("redirect-path", "");
}
return targetPath;
};
const redirectAfterLogin = () => {
navigate(getTargetPath());
};
const redirectToMarketplace = () => {
api
.invoke("GET", "/api/v1/mp-integration/")
.then((res: any) => {
redirectAfterLogin(); // Email already set, continue with normal flow
})
.catch((err: ErrorResponseHandler) => {
if (err.statusCode === 404) {
dispatch(showMarketplace(true));
navigate("/marketplace");
} else {
// Unexpected error, continue with normal flow
redirectAfterLogin();
}
});
};
if (navigateTo !== "") {
navigate(navigateTo);
dispatch(resetForm());
}
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoginSending(true);
api
.invoke(
"POST",
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login",
loginStrategyPayload[loginStrategy.loginStrategy]
)
.then(() => {
// We set the state in redux
dispatch(userLogged(true));
if (loginStrategy.loginStrategy === loginStrategyType.form) {
localStorage.setItem("userLoggedIn", accessKey);
}
if (isOperator) {
redirectToMarketplace();
} else {
redirectAfterLogin();
}
})
.catch((err) => {
setLoginSending(false);
dispatch(setErrorSnackMessage(err));
});
dispatch(doLoginAsync());
};
useEffect(() => {
if (loadingFetchConfiguration) {
api
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
setLoginStrategy(loginDetails);
setLoadingFetchConfiguration(false);
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
setLoadingFetchConfiguration(false);
});
dispatch(getFetchConfigurationAsync());
}
}, [loadingFetchConfiguration, dispatch]);
useEffect(() => {
if (loadingVersion) {
api
.invoke("GET", "/api/v1/check-version")
.then(
({
current_version,
latest_version,
}: {
current_version: string;
latest_version: string;
}) => {
setLatestMinIOVersion(latest_version);
setLoadingVersion(false);
}
)
.catch((err: ErrorResponseHandler) => {
// try the operator version
api
.invoke("GET", "/api/v1/check-operator-version")
.then(
({
current_version,
latest_version,
}: {
current_version: string;
latest_version: string;
}) => {
setLatestMinIOVersion(latest_version);
setLoadingVersion(false);
}
)
.catch((err: ErrorResponseHandler) => {
setLoadingVersion(false);
});
});
dispatch(getVersionAsync());
}
}, [loadingVersion, setLoadingVersion, setLatestMinIOVersion]);
}, [dispatch, loadingVersion]);
let loginComponent = null;
let loginComponent;
switch (loginStrategy.loginStrategy) {
case loginStrategyType.form: {
loginComponent = (
<React.Fragment>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
<Grid item xs={12} className={classes.spacerBottom}>
<LoginField
fullWidth
id="accessKey"
className={classes.inputField}
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAccessKey(e.target.value)
}
placeholder={"Username"}
name="accessKey"
autoComplete="username"
disabled={loginSending}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<UserFilledIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12}>
<LoginField
fullWidth
className={classes.inputField}
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSecretKey(e.target.value)
}
name="secretKey"
type="password"
id="secretKey"
autoComplete="current-password"
disabled={loginSending}
placeholder={"Password"}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<LockFilledIcon />
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.submitContainer}>
<Button
type="submit"
variant="contained"
color="primary"
id="do-login"
className={classes.submit}
disabled={secretKey === "" || accessKey === "" || loginSending}
>
Login
</Button>
</Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
</form>
</React.Fragment>
);
loginComponent = <StrategyForm />;
break;
}
case loginStrategyType.redirect:
@@ -553,7 +325,7 @@ const Login = ({ classes }: ILoginProps) => {
id="jwt"
value={jwt}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setJwt(e.target.value)
dispatch(setJwt(e.target.value))
}
name="jwt"
autoComplete="off"
@@ -607,7 +379,7 @@ const Login = ({ classes }: ILoginProps) => {
<div>
<Button
onClick={() => {
fetchConfiguration();
dispatch(getFetchConfigurationAsync());
}}
endIcon={<RefreshIcon />}
color={"primary"}
@@ -771,4 +543,4 @@ const Login = ({ classes }: ILoginProps) => {
);
};
export default withStyles(styles)(Login);
export default Login;

View File

@@ -0,0 +1,231 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 Grid from "@mui/material/Grid";
import React, { Fragment } from "react";
import { setAccessKey, setSecretKey, setSTS, setUseSTS } from "./loginSlice";
import { Box, InputAdornment, LinearProgress } from "@mui/material";
import UserFilledIcon from "../../icons/UsersFilledIcon";
import LockFilledIcon from "../../icons/LockFilledIcon";
import Button from "@mui/material/Button";
import { AppState, useAppDispatch } from "../../store";
import { useSelector } from "react-redux";
import { LoginField } from "./LoginField";
import makeStyles from "@mui/styles/makeStyles";
import { Theme, useTheme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary";
import { doLoginAsync } from "./loginThunks";
import { PasswordKeyIcon } from "../../icons";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
overflow: "auto",
},
form: {
width: "100%", // Fix IE 11 issue.
},
submit: {
margin: "30px 0px 8px",
height: 40,
width: "100%",
boxShadow: "none",
padding: "16px 30px",
},
submitContainer: {
textAlign: "right",
},
linearPredef: {
height: 10,
},
...spacingUtils,
})
);
const StrategyForm = () => {
const dispatch = useAppDispatch();
const classes = useStyles();
const theme = useTheme();
const accessKey = useSelector((state: AppState) => state.login.accessKey);
const secretKey = useSelector((state: AppState) => state.login.secretKey);
const sts = useSelector((state: AppState) => state.login.sts);
const useSTS = useSelector((state: AppState) => state.login.useSTS);
const loginSending = useSelector(
(state: AppState) => state.login.loginSending
);
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(doLoginAsync());
};
return (
<React.Fragment>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
<Grid item xs={12} className={classes.spacerBottom}>
<LoginField
fullWidth
id="accessKey"
className={classes.inputField}
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setAccessKey(e.target.value))
}
placeholder={useSTS ? "STS Username" : "Username"}
name="accessKey"
autoComplete="username"
disabled={loginSending}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<UserFilledIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={12} className={useSTS ? classes.spacerBottom : ""}>
<LoginField
fullWidth
className={classes.inputField}
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSecretKey(e.target.value))
}
name="secretKey"
type="password"
id="secretKey"
autoComplete="current-password"
disabled={loginSending}
placeholder={useSTS ? "STS Secret" : "Password"}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<LockFilledIcon />
</InputAdornment>
),
}}
/>
</Grid>
{useSTS && (
<Grid item xs={12} className={classes.spacerBottom}>
<LoginField
fullWidth
id="sts"
className={classes.inputField}
value={sts}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setSTS(e.target.value))
}
placeholder={"STS Token"}
name="STS"
autoComplete="sts"
disabled={loginSending}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment
position="start"
className={classes.iconColor}
>
<PasswordKeyIcon />
</InputAdornment>
),
}}
/>
</Grid>
)}
</Grid>
<Grid item xs={12} className={classes.submitContainer}>
<Button
type="submit"
variant="contained"
color="primary"
id="do-login"
className={classes.submit}
disabled={
(!useSTS && (accessKey === "" || secretKey === "")) ||
(useSTS && sts === "") ||
loginSending
}
>
Login
</Button>
</Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
<Grid item xs={12} className={classes.linearPredef}>
<Box
style={{
textAlign: "center",
marginTop: 20,
}}
>
<span
onClick={() => {
dispatch(setUseSTS(!useSTS));
}}
style={{
color: theme.colors.link,
font: "normal normal normal 12px/15px Lato",
textDecoration: "underline",
cursor: "pointer",
}}
>
{!useSTS && <Fragment>Use STS</Fragment>}
{useSTS && <Fragment>Use Credentials</Fragment>}
</span>
<span
onClick={() => {
dispatch(setUseSTS(!useSTS));
}}
style={{
color: theme.colors.link,
font: "normal normal normal 12px/15px Lato",
textDecoration: "none",
fontWeight: "bold",
paddingLeft: 4,
}}
>
</span>
</Box>
</Grid>
</form>
</React.Fragment>
);
};
export default StrategyForm;

View File

@@ -0,0 +1,135 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { ILoginDetails, loginStrategyType } from "./types";
import {
doLoginAsync,
getFetchConfigurationAsync,
getVersionAsync,
} from "./loginThunks";
export interface LoginState {
accessKey: string;
secretKey: string;
sts: string;
useSTS: boolean;
jwt: string;
loginStrategy: ILoginDetails;
loginSending: boolean;
loadingFetchConfiguration: boolean;
latestMinIOVersion: string;
loadingVersion: boolean;
navigateTo: string;
}
const initialState: LoginState = {
accessKey: "",
secretKey: "",
sts: "",
useSTS: false,
jwt: "",
loginStrategy: {
loginStrategy: loginStrategyType.unknown,
redirect: "",
},
loginSending: false,
loadingFetchConfiguration: true,
latestMinIOVersion: "",
loadingVersion: true,
navigateTo: "",
};
export const loginSlice = createSlice({
name: "login",
initialState,
reducers: {
setAccessKey: (state, action: PayloadAction<string>) => {
state.accessKey = action.payload;
},
setSecretKey: (state, action: PayloadAction<string>) => {
state.secretKey = action.payload;
},
setUseSTS: (state, action: PayloadAction<boolean>) => {
state.useSTS = action.payload;
},
setSTS: (state, action: PayloadAction<string>) => {
state.sts = action.payload;
},
setJwt: (state, action: PayloadAction<string>) => {
state.jwt = action.payload;
},
setNavigateTo: (state, action: PayloadAction<string>) => {
state.navigateTo = action.payload;
},
resetForm: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(getVersionAsync.pending, (state, action) => {
state.loadingVersion = true;
})
.addCase(getVersionAsync.rejected, (state, action) => {
state.loadingVersion = false;
})
.addCase(getVersionAsync.fulfilled, (state, action) => {
state.loadingVersion = false;
if (action.payload) {
state.latestMinIOVersion = action.payload;
}
})
.addCase(getFetchConfigurationAsync.pending, (state, action) => {
state.loadingFetchConfiguration = true;
})
.addCase(getFetchConfigurationAsync.rejected, (state, action) => {
state.loadingFetchConfiguration = false;
})
.addCase(getFetchConfigurationAsync.fulfilled, (state, action) => {
state.loadingFetchConfiguration = false;
if (action.payload) {
state.loginStrategy = action.payload;
}
})
.addCase(doLoginAsync.pending, (state, action) => {
state.loginSending = true;
})
.addCase(doLoginAsync.rejected, (state, action) => {
state.loginSending = false;
})
.addCase(doLoginAsync.fulfilled, (state, action) => {
state.loginSending = false;
});
},
});
// Action creators are generated for each case reducer function
export const {
setAccessKey,
setSecretKey,
setUseSTS,
setSTS,
setJwt,
setNavigateTo,
resetForm,
} = loginSlice.actions;
export default loginSlice.reducer;

View File

@@ -0,0 +1,145 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../store";
import api from "../../common/api";
import { ErrorResponseHandler } from "../../common/types";
import {
setErrorSnackMessage,
showMarketplace,
userLogged,
} from "../../systemSlice";
import { ILoginDetails, loginStrategyType } from "./types";
import { setNavigateTo } from "./loginSlice";
import {
getTargetPath,
loginStrategyEndpoints,
LoginStrategyPayload,
} from "./LoginPage";
export const doLoginAsync = createAsyncThunk(
"login/doLoginAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const loginStrategy = state.login.loginStrategy;
const accessKey = state.login.accessKey;
const secretKey = state.login.secretKey;
const jwt = state.login.jwt;
const sts = state.login.sts;
const useSTS = state.login.useSTS;
const isOperator =
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
let loginStrategyPayload: LoginStrategyPayload = {
form: { accessKey, secretKey },
"service-account": { jwt },
};
if (useSTS) {
loginStrategyPayload = {
form: { accessKey, secretKey, sts },
};
}
return api
.invoke(
"POST",
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login",
loginStrategyPayload[loginStrategy.loginStrategy]
)
.then(() => {
// We set the state in redux
dispatch(userLogged(true));
if (loginStrategy.loginStrategy === loginStrategyType.form) {
localStorage.setItem("userLoggedIn", accessKey);
}
if (isOperator) {
api
.invoke("GET", "/api/v1/mp-integration/")
.then((res: any) => {
dispatch(setNavigateTo(getTargetPath())); // Email already set, continue with normal flow
})
.catch((err: ErrorResponseHandler) => {
if (err.statusCode === 404) {
dispatch(showMarketplace(true));
dispatch(setNavigateTo("/marketplace"));
} else {
// Unexpected error, continue with normal flow
dispatch(setNavigateTo(getTargetPath()));
}
});
} else {
dispatch(setNavigateTo(getTargetPath()));
}
})
.catch((err) => {
dispatch(setErrorSnackMessage(err));
});
}
);
export const getFetchConfigurationAsync = createAsyncThunk(
"login/getFetchConfigurationAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
return api
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
return loginDetails;
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
});
}
);
export const getVersionAsync = createAsyncThunk(
"login/getVersionAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
return api
.invoke("GET", "/api/v1/check-version")
.then(
({
current_version,
latest_version,
}: {
current_version: string;
latest_version: string;
}) => {
return latest_version;
}
)
.catch((err: ErrorResponseHandler) => {
// try the operator version
api
.invoke("GET", "/api/v1/check-operator-version")
.then(
({
current_version,
latest_version,
}: {
current_version: string;
latest_version: string;
}) => {
return latest_version;
}
)
.catch((err: ErrorResponseHandler) => {
return err;
});
});
}
);

View File

@@ -17,6 +17,7 @@ import { useDispatch } from "react-redux";
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import systemReducer from "./systemSlice";
import loginReducer from "./screens/LoginPage/loginSlice";
import traceReducer from "./screens/Console/Trace/traceSlice";
import logReducer from "./screens/Console/Logs/logsSlice";
import healthInfoReducer from "./screens/Console/HealthInfo/healthInfoSlice";
@@ -36,6 +37,7 @@ import editTenantAuditLoggingReducer from "./screens/Console/Tenants/TenantDetai
const rootReducer = combineReducers({
system: systemReducer,
login: loginReducer,
trace: traceReducer,
logs: logReducer,
watch: watchReducer,

View File

@@ -37,6 +37,7 @@ import (
"github.com/minio/console/pkg/logger"
"github.com/minio/console/pkg/utils"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/klauspost/compress/gzhttp"
@@ -93,6 +94,7 @@ func configureAPI(api *operations.ConsoleAPI) http.Handler {
STSSessionToken: claims.STSSessionToken,
AccountAccessKey: claims.AccountAccessKey,
Hm: claims.HideMenu,
Ob: claims.ObjectBrowser,
}, nil
}
@@ -335,6 +337,7 @@ func (w *notFoundRedirectRespWr) Write(p []byte) (int, error) {
return len(p), nil // Lie that we successfully wrote it
}
// handleSPA handles the serving of the React Single Page Application
func handleSPA(w http.ResponseWriter, r *http.Request) {
basePath := "/"
// For SPA mode we will replace root base with a sub path if configured unless we received cp=y and cpb=/NEW/BASE
@@ -354,6 +357,29 @@ func handleSPA(w http.ResponseWriter, r *http.Request) {
return
}
sts := r.URL.Query().Get("sts")
stsAccessKey := r.URL.Query().Get("sts_a")
stsSecretKey := r.URL.Query().Get("sts_s")
// if these three parameters are present we are being asked to issue a session with these values
if sts != "" && stsAccessKey != "" && stsSecretKey != "" {
creds := credentials.NewStaticV4(stsAccessKey, stsSecretKey, sts)
consoleCreds := &ConsoleCredentials{
ConsoleCredentials: creds,
AccountAccessKey: stsAccessKey,
}
sf := &auth.SessionFeatures{}
sf.HideMenu = true
sf.ObjectBrowser = true
sessionID, err := login(consoleCreds, sf)
if err != nil {
log.Println(err)
} else {
cookie := NewSessionCookieForConsole(*sessionID)
http.SetCookie(w, &cookie)
}
}
indexPageBytes, err := io.ReadAll(indexPage)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@@ -51,6 +51,7 @@ const (
PrometheusExtraLabels = "CONSOLE_PROMETHEUS_EXTRA_LABELS"
ConsoleLogQueryURL = "CONSOLE_LOG_QUERY_URL"
ConsoleLogQueryAuthToken = "CONSOLE_LOG_QUERY_AUTH_TOKEN"
ConsoleObjectBrowserOnly = "CONSOLE_OBJECT_BROWSER_ONLY"
LogSearchQueryAuthToken = "LOGSEARCH_QUERY_AUTH_TOKEN"
SlashSeparator = "/"
)

View File

@@ -5412,10 +5412,6 @@ func init() {
},
"loginRequest": {
"type": "object",
"required": [
"accessKey",
"secretKey"
],
"properties": {
"accessKey": {
"type": "string"
@@ -5430,6 +5426,9 @@ func init() {
},
"secretKey": {
"type": "string"
},
"sts": {
"type": "string"
}
}
},
@@ -5970,6 +5969,9 @@ func init() {
},
"hm": {
"type": "boolean"
},
"ob": {
"type": "boolean"
}
}
},
@@ -12631,10 +12633,6 @@ func init() {
},
"loginRequest": {
"type": "object",
"required": [
"accessKey",
"secretKey"
],
"properties": {
"accessKey": {
"type": "string"
@@ -12649,6 +12647,9 @@ func init() {
},
"secretKey": {
"type": "string"
},
"sts": {
"type": "string"
}
}
},
@@ -13189,6 +13190,9 @@ func init() {
},
"hm": {
"type": "boolean"
},
"ob": {
"type": "boolean"
}
}
},

View File

@@ -21,7 +21,6 @@ import (
"net/http"
"github.com/minio/madmin-go"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/go-openapi/runtime"
@@ -113,11 +112,23 @@ func getLoginResponse(params authApi.LoginParams) (*models.LoginResponse, *model
ctx, cancel := context.WithCancel(params.HTTPRequest.Context())
defer cancel()
lr := params.Body
// prepare console credentials
consoleCreds, err := getConsoleCredentials(*lr.AccessKey, *lr.SecretKey)
if err != nil {
return nil, ErrorWithContext(ctx, err, ErrInvalidLogin, err)
var err error
var consoleCreds *ConsoleCredentials
// if we receive an STS we use that instead of the credentials
if lr.Sts != "" {
creds := credentials.NewStaticV4(lr.AccessKey, lr.SecretKey, lr.Sts)
consoleCreds = &ConsoleCredentials{
ConsoleCredentials: creds,
AccountAccessKey: lr.AccessKey,
}
} else {
// prepare console credentials
consoleCreds, err = getConsoleCredentials(lr.AccessKey, lr.SecretKey)
if err != nil {
return nil, ErrorWithContext(ctx, err, ErrInvalidLogin, err)
}
}
sf := &auth.SessionFeatures{}
if lr.Features != nil {
sf.HideMenu = lr.Features.HideMenu

View File

@@ -268,6 +268,9 @@ func getListOfEnabledFeatures(session *models.Principal) []string {
if session.Hm {
features = append(features, "hide-menu")
}
if session.Ob {
features = append(features, "object-browser-only")
}
return features
}

View File

@@ -1,4 +1,3 @@
swagger: "2.0"
info:
title: MinIO Console Server
@@ -3677,14 +3676,13 @@ definitions:
type: string
loginRequest:
type: object
required:
- accessKey
- secretKey
properties:
accessKey:
type: string
secretKey:
type: string
sts:
type: string
features:
type: object
properties:
@@ -3709,6 +3707,8 @@ definitions:
type: string
hm:
type: boolean
ob:
type: boolean
startProfilingItem:
type: object
properties:

View File

@@ -1394,14 +1394,13 @@ definitions:
type: string
loginRequest:
type: object
required:
- accessKey
- secretKey
properties:
accessKey:
type: string
secretKey:
type: string
sts:
type: string
features:
type: object
properties: