MCS service account authentication with Mkube (#166)

`MCS` will authenticate against `Mkube`using bearer tokens via HTTP
`Authorization` header. The user will provide this token once
in the login form, MCS will validate it against Mkube (list tenants) and
if valid will generate and return a new MCS sessions
with encrypted claims (the user Service account token will be inside the
JWT in the data field)

Kubernetes

The provided `JWT token` corresponds to the `Kubernetes service account`
that `Mkube` will use to run tasks on behalf of the
user, ie: list, create, edit, delete tenants, storage class, etc.

Development

If you are running mcs in your local environment and wish to make
request to `Mkube` you can set `MCS_M3_HOSTNAME`, if
the environment variable is not present by default `MCS` will use
`"http://m3:8787"`, additionally you will need to set the
`MCS_MKUBE_ADMIN_ONLY=on` variable to make MCS display the Mkube UI

Extract the Service account token and use it with MCS

For local development you can use the jwt associated to the `m3-sa`
service account, you can get the token running
the following command in your terminal:

```
kubectl get secret $(kubectl get serviceaccount m3-sa -o
jsonpath="{.secrets[0].name}") -o jsonpath="{.data.token}" | base64
--decode
```

Then run the mcs server

```
MCS_M3_HOSTNAME=http://localhost:8787 MCS_MKUBE_ADMIN_ONLY=on ./mcs
server
```

Self-signed certificates and Custom certificate authority for Mkube

If Mkube uses TLS with a self-signed certificate, or a certificate
issued by a custom certificate authority you can add those
certificates usinng the `MCS_M3_SERVER_TLS_CA_CERTIFICATE` env variable

````
MCS_M3_SERVER_TLS_CA_CERTIFICATE=cert1.pem,cert2.pem,cert3.pem ./mcs
server
````
This commit is contained in:
Lenin Alevski
2020-06-23 11:37:46 -07:00
committed by GitHub
parent 1aec2d879e
commit 1e7f272a67
36 changed files with 1532 additions and 387 deletions

File diff suppressed because one or more lines are too long

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, { useEffect, useState } from "react";
import request from "superagent";
import storage from "local-storage-fallback";
import { connect, ConnectedProps } from "react-redux";
@@ -26,9 +26,8 @@ import { CircularProgress, Paper } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { SystemState } from "../../types";
import { userLoggedIn } from "../../actions";
import history from "../../history";
import api from "../../common/api";
import { ILoginDetails } from "./types";
import { ILoginDetails, loginStrategyType } from "./types";
import { setCookie } from "../../common/utils";
const styles = (theme: Theme) =>
@@ -101,57 +100,57 @@ interface ILoginProps {
classes: any;
}
interface ILoginState {
accessKey: string;
secretKey: string;
error: string;
loading: boolean;
loginStrategy: ILoginDetails;
interface LoginStrategyRoutes {
[key: string]: string;
}
class Login extends React.Component<ILoginProps, ILoginState> {
state: ILoginState = {
accessKey: "",
secretKey: "",
error: "",
loading: false,
loginStrategy: {
loginStrategy: "",
redirect: "",
},
};
interface LoginStrategyPayload {
[key: string]: any;
}
fetchConfiguration() {
this.setState({ loading: true }, () => {
api
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
this.setState({
loading: false,
});
this.setState({
loading: false,
loginStrategy: loginDetails,
error: "",
});
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
const Login = ({ classes, userLoggedIn }: ILoginProps) => {
const [accessKey, setAccessKey] = useState<string>("");
const [jwt, setJwt] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [error, setError] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [loginStrategy, setLoginStrategy] = useState<ILoginDetails>({
loginStrategy: loginStrategyType.unknown,
redirect: "",
});
const loginStrategyEndpoints: LoginStrategyRoutes = {
"form": "/api/v1/login",
"service-account": "/api/v1/login/mkube",
}
const loginStrategyPayload: LoginStrategyPayload = {
"form": { accessKey, secretKey },
"service-account": { jwt },
}
formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const url = "/api/v1/login";
const { accessKey, secretKey } = this.state;
const fetchConfiguration = () => {
setLoading(true);
api
.invoke("GET", "/api/v1/login")
.then((loginDetails: ILoginDetails) => {
setLoading(false);
setLoginStrategy(loginDetails);
setError("");
})
.catch((err: any) => {
setLoading(false);
setError(err);
});
};
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
request
.post(url)
.send({ accessKey, secretKey })
.post(loginStrategyEndpoints[loginStrategy.loginStrategy] || "/api/v1/login")
.send(loginStrategyPayload[loginStrategy.loginStrategy])
.then((res: any) => {
const bodyResponse = res.body;
if (bodyResponse.sessionId) {
// store the jwt token
setCookie("token", bodyResponse.sessionId);
@@ -164,134 +163,173 @@ class Login extends React.Component<ILoginProps, ILoginState> {
})
.then(() => {
// We set the state in redux
this.props.userLoggedIn(true);
userLoggedIn(true);
// There is a browser cache issue if we change the policy associated to an account and then logout and history.push("/") after login
// therefore after login we need to use window.location redirect
window.location.href = "/";
})
.catch((err) => {
this.setState({ error: `${err}` });
setError(err.message);
});
};
componentDidMount(): void {
this.fetchConfiguration();
}
useEffect(() => {
fetchConfiguration();
}, []);
render() {
const { error, accessKey, secretKey, loginStrategy } = this.state;
const { classes } = this.props;
let loginComponent = null;
let loginComponent = null;
switch (loginStrategy.loginStrategy) {
case "form": {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
</Typography>
<form
className={classes.form}
noValidate
onSubmit={this.formSubmit}
>
<Grid container spacing={2}>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
switch (loginStrategy.loginStrategy) {
case loginStrategyType.form: {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
</Typography>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
{error !== "" && (
<Grid item xs={12}>
<TextField
required
fullWidth
id="accessKey"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ accessKey: e.target.value })
}
label="Access Key"
name="accessKey"
autoComplete="username"
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
this.setState({ secretKey: e.target.value })
}
name="secretKey"
label="Secret Key"
type="password"
id="secretKey"
autoComplete="current-password"
/>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TextField
required
fullWidth
id="accessKey"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setAccessKey(e.target.value)
}
label="Access Key"
name="accessKey"
autoComplete="username"
/>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Login
</Button>
</form>
</React.Fragment>
);
break;
}
case "redirect": {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
</Typography>
<Grid item xs={12}>
<TextField
required
fullWidth
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setSecretKey(e.target.value)
}
name="secretKey"
label="Secret Key"
type="password"
id="secretKey"
autoComplete="current-password"
/>
</Grid>
</Grid>
<Button
component={"a"}
href={loginStrategy.redirect}
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Welcome
Login
</Button>
</React.Fragment>
);
break;
}
default:
loginComponent = (
<CircularProgress className={classes.loadingLoginStrategy} />
);
</form>
</React.Fragment>
);
break;
}
return (
<Paper className={classes.paper}>
<Grid container className={classes.mainContainer}>
<Grid item xs={7} className={classes.theOcean}>
<div className={classes.oceanBg} />
</Grid>
<Grid item xs={5} className={classes.theLogin}>
{loginComponent}
</Grid>
</Grid>
</Paper>
);
case loginStrategyType.redirect: {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
</Typography>
<Button
component={"a"}
href={loginStrategy.redirect}
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Welcome
</Button>
</React.Fragment>
);
break;
}
case loginStrategyType.serviceAccount: {
loginComponent = (
<React.Fragment>
<Typography component="h1" variant="h6">
Login
</Typography>
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{error}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TextField
required
fullWidth
id="jwt"
value={jwt}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setJwt(e.target.value)
}
label="JWT"
name="jwt"
autoComplete="Service Account JWT Token"
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Login
</Button>
</form>
</React.Fragment>
);
break;
}
default:
loginComponent = (
<CircularProgress className={classes.loadingLoginStrategy} />
);
}
}
return (
<Paper className={classes.paper}>
<Grid container className={classes.mainContainer}>
<Grid item xs={7} className={classes.theOcean}>
<div className={classes.oceanBg} />
</Grid>
<Grid item xs={5} className={classes.theLogin}>
{loginComponent}
</Grid>
</Grid>
</Paper>
);
};
export default connector(withStyles(styles)(Login));

View File

@@ -15,6 +15,13 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface ILoginDetails {
loginStrategy: string;
loginStrategy: loginStrategyType;
redirect: string;
}
export enum loginStrategyType {
unknown = "unknown",
form = "form",
redirect = "redirect",
serviceAccount = "service-account",
}