Changed IDP login support in console (#2695)

- Allowed to use External IDP + Built-in IDP at the same time
- Added IDP type to IDP listing
- Added IDP name when no display name is configured
- Changed STS login link into new menu
- Cleanup of Operator login strategies

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2023-03-07 16:10:21 -06:00
committed by GitHub
parent 1953a98968
commit 1fc9a40273
9 changed files with 129 additions and 279 deletions

View File

@@ -39,6 +39,9 @@ type RedirectRule struct {
// redirect // redirect
Redirect string `json:"redirect,omitempty"` Redirect string `json:"redirect,omitempty"`
// service type
ServiceType string `json:"serviceType,omitempty"`
} }
// Validate validates this redirect rule // Validate validates this redirect rule

View File

@@ -16,38 +16,18 @@
import React, { Fragment, useEffect } from "react"; import React, { Fragment, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import { Button, Loader, LoginWrapper, RefreshIcon } from "mds";
InputAdornment,
LinearProgress,
MenuItem,
Select,
} from "@mui/material";
import {
Button,
Loader,
LockIcon,
LoginWrapper,
LogoutIcon,
RefreshIcon,
} from "mds";
import { Theme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import Grid from "@mui/material/Grid";
import { loginStrategyType, redirectRule } from "./types"; import { loginStrategyType, redirectRule } from "./types";
import MainError from "../Console/Common/MainError/MainError"; import MainError from "../Console/Common/MainError/MainError";
import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary"; import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary";
import clsx from "clsx";
import { AppState, useAppDispatch } from "../../store"; import { AppState, useAppDispatch } from "../../store";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { import { getFetchConfigurationAsync, getVersionAsync } from "./loginThunks";
doLoginAsync, import { resetForm } from "./loginSlice";
getFetchConfigurationAsync,
getVersionAsync,
} from "./loginThunks";
import { resetForm, setJwt } from "./loginSlice";
import StrategyForm from "./StrategyForm"; import StrategyForm from "./StrategyForm";
import { LoginField } from "./LoginField";
import { redirectRules } from "../../utils/sortFunctions"; import { redirectRules } from "../../utils/sortFunctions";
import { getLogoVar } from "../../config"; import { getLogoVar } from "../../config";
@@ -250,19 +230,12 @@ const useStyles = makeStyles((theme: Theme) =>
}) })
); );
export interface LoginStrategyRoutes {
[key: string]: string;
}
export interface LoginStrategyPayload { export interface LoginStrategyPayload {
[key: string]: any; accessKey: string;
secretKey: string;
sts?: string;
} }
export const loginStrategyEndpoints: LoginStrategyRoutes = {
form: "/api/v1/login",
"service-account": "/api/v1/login/operator",
};
export const getTargetPath = () => { export const getTargetPath = () => {
let targetPath = "/"; let targetPath = "/";
if ( if (
@@ -280,13 +253,9 @@ const Login = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const classes = useStyles(); const classes = useStyles();
const jwt = useSelector((state: AppState) => state.login.jwt);
const loginStrategy = useSelector( const loginStrategy = useSelector(
(state: AppState) => state.login.loginStrategy (state: AppState) => state.login.loginStrategy
); );
const loginSending = useSelector(
(state: AppState) => state.login.loginSending
);
const loadingFetchConfiguration = useSelector( const loadingFetchConfiguration = useSelector(
(state: AppState) => state.login.loadingFetchConfiguration (state: AppState) => state.login.loadingFetchConfiguration
); );
@@ -295,13 +264,8 @@ const Login = () => {
); );
const navigateTo = useSelector((state: AppState) => state.login.navigateTo); const navigateTo = useSelector((state: AppState) => state.login.navigateTo);
const isDirectPV = useSelector((state: AppState) => state.login.isDirectPV);
const isK8S = useSelector((state: AppState) => state.login.isK8S); const isK8S = useSelector((state: AppState) => state.login.isK8S);
const isOperator =
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
useEffect(() => { useEffect(() => {
if (navigateTo !== "") { if (navigateTo !== "") {
dispatch(resetForm()); dispatch(resetForm());
@@ -309,11 +273,6 @@ const Login = () => {
} }
}, [navigateTo, dispatch, navigate]); }, [navigateTo, dispatch, navigate]);
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(doLoginAsync());
};
useEffect(() => { useEffect(() => {
if (loadingFetchConfiguration) { if (loadingFetchConfiguration) {
dispatch(getFetchConfigurationAsync()); dispatch(getFetchConfigurationAsync());
@@ -329,12 +288,8 @@ const Login = () => {
let loginComponent; let loginComponent;
switch (loginStrategy.loginStrategy) { switch (loginStrategy.loginStrategy) {
case loginStrategyType.form: {
loginComponent = <StrategyForm />;
break;
}
case loginStrategyType.redirect: case loginStrategyType.redirect:
case loginStrategyType.redirectServiceAccount: { case loginStrategyType.form: {
let redirectItems: redirectRule[] = []; let redirectItems: redirectRule[] = [];
if ( if (
@@ -344,111 +299,7 @@ const Login = () => {
redirectItems = [...loginStrategy.redirectRules].sort(redirectRules); redirectItems = [...loginStrategy.redirectRules].sort(redirectRules);
} }
if ( loginComponent = <StrategyForm redirectRules={redirectItems} />;
loginStrategy.redirectRules &&
loginStrategy.redirectRules.length > 1
) {
loginComponent = (
<Fragment>
<div className={classes.loginSsoText}>Login with SSO:</div>
<Select
id="ssoLogin"
name="ssoLogin"
data-test-id="sso-login"
onChange={(e) => {
if (e.target.value) {
window.location.href = e.target.value as string;
}
}}
displayEmpty
className={classes.ssoSelect}
renderValue={() => "Select Provider"}
>
{redirectItems.map((r, idx) => (
<MenuItem
value={r.redirect}
key={`sso-login-option-${idx}`}
className={classes.ssoMenuItem}
divider={true}
>
<LogoutIcon className={classes.ssoLoginIcon} />
{r.displayName}
</MenuItem>
))}
</Select>
</Fragment>
);
} else if (redirectItems.length === 1) {
loginComponent = (
<div className={clsx(classes.submit, classes.ssoSubmit)}>
<Button
key={`login-button`}
variant="callAction"
id="sso-login"
label={
redirectItems[0].displayName === ""
? "Login with SSO"
: redirectItems[0].displayName
}
onClick={() => (window.location.href = redirectItems[0].redirect)}
fullWidth
/>
</div>
);
} else {
loginComponent = (
<div className={classes.loginStrategyMessage}>
Cannot retrieve redirect from login strategy
</div>
);
}
break;
}
case loginStrategyType.serviceAccount: {
loginComponent = (
<Fragment>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
<Grid item xs={12}>
<LoginField
required
className={classes.inputField}
fullWidth
id="jwt"
value={jwt}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(setJwt(e.target.value))
}
name="jwt"
autoComplete="off"
disabled={loginSending}
placeholder={"Enter JWT"}
variant={"outlined"}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<LockIcon />
</InputAdornment>
),
}}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.submitContainer}>
<Button
variant="callAction"
id="do-login"
disabled={jwt === "" || loginSending}
label={"Login"}
fullWidth
/>
</Grid>
<Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />}
</Grid>
</form>
</Fragment>
);
break; break;
} }
default: default:
@@ -483,16 +334,6 @@ const Login = () => {
); );
} }
let modeLogo: "console" | "directpv" | "operator" | "kes" | "subnet" =
"console";
const logoVar = getLogoVar();
if (isDirectPV) {
modeLogo = "directpv";
} else if (isOperator) {
modeLogo = "operator";
}
let docsURL = "https://min.io/docs/minio/linux/index.html?ref=con"; let docsURL = "https://min.io/docs/minio/linux/index.html?ref=con";
if (isK8S) { if (isK8S) {
docsURL = docsURL =
@@ -503,7 +344,7 @@ const Login = () => {
<Fragment> <Fragment>
<MainError /> <MainError />
<LoginWrapper <LoginWrapper
logoProps={{ applicationName: modeLogo, subVariant: logoVar }} logoProps={{ applicationName: "console", subVariant: getLogoVar() }}
form={loginComponent} form={loginComponent}
formFooter={ formFooter={
<Fragment> <Fragment>

View File

@@ -15,18 +15,31 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import React, { Fragment } from "react"; import React from "react";
import { Button, LockFilledIcon, PasswordKeyIcon, UserFilledIcon } from "mds"; import {
Button,
LockFilledIcon,
LogoutIcon,
PasswordKeyIcon,
UserFilledIcon,
} from "mds";
import { setAccessKey, setSecretKey, setSTS, setUseSTS } from "./loginSlice"; import { setAccessKey, setSecretKey, setSTS, setUseSTS } from "./loginSlice";
import { Box, InputAdornment, LinearProgress } from "@mui/material"; import {
InputAdornment,
LinearProgress,
MenuItem,
Select,
SelectChangeEvent,
} from "@mui/material";
import { AppState, useAppDispatch } from "../../store"; import { AppState, useAppDispatch } from "../../store";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { LoginField } from "./LoginField"; import { LoginField } from "./LoginField";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import { Theme, useTheme } from "@mui/material/styles"; import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary"; import { spacingUtils } from "../Console/Common/FormComponents/common/styleLibrary";
import { doLoginAsync } from "./loginThunks"; import { doLoginAsync } from "./loginThunks";
import { IStrategyForm } from "./types";
const useStyles = makeStyles((theme: Theme) => const useStyles = makeStyles((theme: Theme) =>
createStyles({ createStyles({
@@ -59,10 +72,9 @@ const useStyles = makeStyles((theme: Theme) =>
}) })
); );
const StrategyForm = () => { const StrategyForm = ({ redirectRules }: IStrategyForm) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const classes = useStyles(); const classes = useStyles();
const theme = useTheme();
const accessKey = useSelector((state: AppState) => state.login.accessKey); const accessKey = useSelector((state: AppState) => state.login.accessKey);
const secretKey = useSelector((state: AppState) => state.login.secretKey); const secretKey = useSelector((state: AppState) => state.login.secretKey);
@@ -78,6 +90,42 @@ const StrategyForm = () => {
dispatch(doLoginAsync()); dispatch(doLoginAsync());
}; };
let ssoOptions: React.ReactNode = null;
if (redirectRules.length > 0) {
ssoOptions = redirectRules.map((r, idx) => (
<MenuItem
value={r.redirect}
key={`sso-login-option-${idx}`}
className={classes.ssoMenuItem}
divider={true}
>
<LogoutIcon
className={classes.ssoLoginIcon}
style={{ width: 16, height: 16, marginRight: 8 }}
/>
{r.displayName}
{r.serviceType ? ` - ${r.serviceType}` : ""}
</MenuItem>
));
}
const extraActionSelector = (e: SelectChangeEvent) => {
const value = e.target.value;
if (value) {
console.log(value);
if (value.includes("use-sts")) {
console.log("si");
dispatch(setUseSTS(!useSTS));
return;
}
window.location.href = e.target.value as string;
}
};
return ( return (
<React.Fragment> <React.Fragment>
<form className={classes.form} noValidate onSubmit={formSubmit}> <form className={classes.form} noValidate onSubmit={formSubmit}>
@@ -184,42 +232,32 @@ const StrategyForm = () => {
<Grid item xs={12} className={classes.linearPredef}> <Grid item xs={12} className={classes.linearPredef}>
{loginSending && <LinearProgress />} {loginSending && <LinearProgress />}
</Grid> </Grid>
<Grid item xs={12} className={classes.linearPredef}> <Grid
<Box item
style={{ xs={12}
textAlign: "center", className={classes.linearPredef}
marginTop: 20, sx={{ marginTop: "16px" }}
>
<Select
id="alternativeMethods"
name="alternativeMethods"
onChange={extraActionSelector}
displayEmpty
className={classes.ssoSelect}
renderValue={() => "Other Authentication Methods"}
sx={{
width: "100%",
}} }}
> >
<span <MenuItem
onClick={() => { value={useSTS ? "use-sts-cred" : "use-sts"}
dispatch(setUseSTS(!useSTS)); className={classes.ssoMenuItem}
}} divider={redirectRules.length > 0}
style={{
color: theme.colors.link,
font: "normal normal normal 14px Inter",
textDecoration: "underline",
cursor: "pointer",
}}
> >
{!useSTS && <Fragment>Use STS</Fragment>} {useSTS ? "Use Credentials" : "Use STS"}
{useSTS && <Fragment>Use Credentials</Fragment>} </MenuItem>
</span> {ssoOptions}
<span </Select>
onClick={() => {
dispatch(setUseSTS(!useSTS));
}}
style={{
color: theme.colors.link,
font: "normal normal normal 12px/15px Inter",
textDecoration: "none",
fontWeight: "bold",
paddingLeft: 4,
}}
>
</span>
</Box>
</Grid> </Grid>
</form> </form>
</React.Fragment> </React.Fragment>

View File

@@ -28,8 +28,6 @@ export interface LoginState {
sts: string; sts: string;
useSTS: boolean; useSTS: boolean;
jwt: string;
loginStrategy: ILoginDetails; loginStrategy: ILoginDetails;
loginSending: boolean; loginSending: boolean;
@@ -48,7 +46,6 @@ const initialState: LoginState = {
secretKey: "", secretKey: "",
sts: "", sts: "",
useSTS: false, useSTS: false,
jwt: "",
loginStrategy: { loginStrategy: {
loginStrategy: loginStrategyType.unknown, loginStrategy: loginStrategyType.unknown,
redirectRules: [], redirectRules: [],
@@ -79,9 +76,6 @@ export const loginSlice = createSlice({
setSTS: (state, action: PayloadAction<string>) => { setSTS: (state, action: PayloadAction<string>) => {
state.sts = action.payload; state.sts = action.payload;
}, },
setJwt: (state, action: PayloadAction<string>) => {
state.jwt = action.payload;
},
setNavigateTo: (state, action: PayloadAction<string>) => { setNavigateTo: (state, action: PayloadAction<string>) => {
state.navigateTo = action.payload; state.navigateTo = action.payload;
}, },
@@ -133,7 +127,6 @@ export const {
setSecretKey, setSecretKey,
setUseSTS, setUseSTS,
setSTS, setSTS,
setJwt,
setNavigateTo, setNavigateTo,
resetForm, resetForm,
} = loginSlice.actions; } = loginSlice.actions;

View File

@@ -18,75 +18,41 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../store"; import { AppState } from "../../store";
import api from "../../common/api"; import api from "../../common/api";
import { ErrorResponseHandler } from "../../common/types"; import { ErrorResponseHandler } from "../../common/types";
import { import { setErrorSnackMessage, userLogged } from "../../systemSlice";
setErrorSnackMessage, import { ILoginDetails } from "./types";
showMarketplace,
userLogged,
} from "../../systemSlice";
import { ILoginDetails, loginStrategyType } from "./types";
import { setNavigateTo } from "./loginSlice"; import { setNavigateTo } from "./loginSlice";
import { import { getTargetPath, LoginStrategyPayload } from "./LoginPage";
getTargetPath,
loginStrategyEndpoints,
LoginStrategyPayload,
} from "./LoginPage";
export const doLoginAsync = createAsyncThunk( export const doLoginAsync = createAsyncThunk(
"login/doLoginAsync", "login/doLoginAsync",
async (_, { getState, rejectWithValue, dispatch }) => { async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState; const state = getState() as AppState;
const loginStrategy = state.login.loginStrategy;
const accessKey = state.login.accessKey; const accessKey = state.login.accessKey;
const secretKey = state.login.secretKey; const secretKey = state.login.secretKey;
const jwt = state.login.jwt;
const sts = state.login.sts; const sts = state.login.sts;
const useSTS = state.login.useSTS; const useSTS = state.login.useSTS;
const isOperator =
loginStrategy.loginStrategy === loginStrategyType.serviceAccount ||
loginStrategy.loginStrategy === loginStrategyType.redirectServiceAccount;
let loginStrategyPayload: LoginStrategyPayload = { let loginStrategyPayload: LoginStrategyPayload = {
form: { accessKey, secretKey }, accessKey,
"service-account": { jwt }, secretKey,
}; };
if (useSTS) { if (useSTS) {
loginStrategyPayload = { loginStrategyPayload = {
form: { accessKey, secretKey, sts }, accessKey,
secretKey,
sts,
}; };
} }
console.log("PAYLOAD:", loginStrategyPayload);
return api return api
.invoke( .invoke("POST", "/api/v1/login", loginStrategyPayload)
"POST",
loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login",
loginStrategyPayload[loginStrategy.loginStrategy]
)
.then((res) => { .then((res) => {
// We set the state in redux // We set the state in redux
dispatch(userLogged(true)); dispatch(userLogged(true));
if (loginStrategy.loginStrategy === loginStrategyType.form) { localStorage.setItem("userLoggedIn", accessKey);
localStorage.setItem("userLoggedIn", accessKey); dispatch(setNavigateTo(getTargetPath()));
}
// if it's in operator mode, check the Marketplace integration
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) => { .catch((err) => {
dispatch(setErrorSnackMessage(err)); dispatch(setErrorSnackMessage(err));
@@ -124,23 +90,7 @@ export const getVersionAsync = createAsyncThunk(
} }
) )
.catch((err: ErrorResponseHandler) => { .catch((err: ErrorResponseHandler) => {
// try the operator version return err.errorMessage;
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

@@ -24,6 +24,11 @@ export interface ILoginDetails {
export interface redirectRule { export interface redirectRule {
redirect: string; redirect: string;
displayName: string; displayName: string;
serviceType?: string;
}
export interface IStrategyForm {
redirectRules: redirectRule[];
} }
export enum loginStrategyType { export enum loginStrategyType {

View File

@@ -7592,6 +7592,9 @@ func init() {
}, },
"redirect": { "redirect": {
"type": "string" "type": "string"
},
"serviceType": {
"type": "string"
} }
} }
}, },
@@ -16573,6 +16576,9 @@ func init() {
}, },
"redirect": { "redirect": {
"type": "string" "type": "string"
},
"serviceType": {
"type": "string"
} }
} }
}, },

View File

@@ -20,7 +20,9 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"strings"
"github.com/go-openapi/runtime" "github.com/go-openapi/runtime"
"github.com/go-openapi/runtime/middleware" "github.com/go-openapi/runtime/middleware"
@@ -186,15 +188,25 @@ func getLoginDetailsResponse(params authApi.LoginDetailParams, openIDProviders o
Client: oauth2Client, Client: oauth2Client,
} }
displayName := "Login with SSO" displayName := fmt.Sprintf("Login with SSO (%s)", name)
serviceType := ""
if provider.DisplayName != "" { if provider.DisplayName != "" {
displayName = provider.DisplayName displayName = provider.DisplayName
} }
if provider.RoleArn != "" {
splitRoleArn := strings.Split(provider.RoleArn, ":")
if len(splitRoleArn) > 2 {
serviceType = splitRoleArn[2]
}
}
redirectRule := models.RedirectRule{ redirectRule := models.RedirectRule{
Redirect: identityProvider.GenerateLoginURL(), Redirect: identityProvider.GenerateLoginURL(),
DisplayName: displayName, DisplayName: displayName,
ServiceType: serviceType,
} }
redirectRules = append(redirectRules, &redirectRule) redirectRules = append(redirectRules, &redirectRule)

View File

@@ -5891,6 +5891,8 @@ definitions:
type: string type: string
displayName: displayName:
type: string type: string
serviceType:
type: string
idpServerConfiguration: idpServerConfiguration:
type: object type: object