Skip to content

Commit

Permalink
Allow to bypass the oauth loading page (#2149)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor committed Nov 9, 2020
1 parent bcb0139 commit a4d79be
Show file tree
Hide file tree
Showing 14 changed files with 64 additions and 93 deletions.
2 changes: 1 addition & 1 deletion chart/kubeapps/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
apiVersion: v1
name: kubeapps
version: 4.0.4
version: 4.0.5
appVersion: DEVEL
description: Kubeapps is a dashboard for your Kubernetes cluster that makes it easy to deploy and manage applications in your cluster using Helm
icon: https://raw.githubusercontent.com/kubeapps/kubeapps/master/docs/img/logo.png
Expand Down
1 change: 1 addition & 0 deletions chart/kubeapps/templates/dashboard-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ data:
"authProxyEnabled": {{ .Values.authProxy.enabled }},
"oauthLoginURI": {{ .Values.authProxy.oauthLoginURI | quote }},
"oauthLogoutURI": {{ .Values.authProxy.oauthLogoutURI | quote }},
"authProxySkipLoginPage": {{ .Values.authProxy.skipKubeappsLoginPage }},
"featureFlags": {{ .Values.featureFlags | toJson }},
"clusters": {{ template "kubeapps.clusterNames" . }}
}
5 changes: 5 additions & 0 deletions chart/kubeapps/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,11 @@ authProxy:
##
oauthLoginURI: /oauth2/start
oauthLogoutURI: /oauth2/sign_out

## Skip the Kubeapps login page when using OIDC and directly redirect to the IdP
##
skipKubeappsLoginPage: false

## The remaining auth proxy values are relevant only if an internal auth-proxy is
## being configured by Kubeapps.
## Bitnami OAuth2 Proxy image
Expand Down
1 change: 1 addition & 0 deletions dashboard/public/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"kubeappsNamespace": "kubeapps",
"appVersion": "DEVEL",
"authProxyEnabled": false,
"authProxySkipLoginPage": false,
"oauthLoginURI": "/oauth2/start",
"oauthLogoutURI": "/oauth2/sign_out",
"clusters": ["default"]
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function expireSession(): ThunkAction<Promise<void>, IStoreState, null, A

export function checkCookieAuthentication(
cluster: string,
): ThunkAction<Promise<void>, IStoreState, null, AuthAction> {
): ThunkAction<Promise<boolean>, IStoreState, null, AuthAction> {
return async dispatch => {
// The call to authenticate below will also dispatch authenticating,
// but we dispatch it early so that the login screen is shown as
Expand All @@ -102,5 +102,6 @@ export function checkCookieAuthentication(
} else {
dispatch(setAuthenticated(false, false, ""));
}
return isAuthed;
};
}
61 changes: 33 additions & 28 deletions dashboard/src/components/LoginForm/LoginForm.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import LoadingWrapper from "components/LoadingWrapper";
import { mount, shallow } from "enzyme";
import { Location } from "history";
import context from "jest-plugin-context";
import * as React from "react";
import { act } from "react-dom/test-utils";
import { Redirect } from "react-router";
import { defaultStore, mountWrapper } from "shared/specs/mountWrapper";
import { wait } from "shared/utils";
import itBehavesLike from "../../shared/specs";
import LoginForm from "./LoginForm";
import OAuthLogin from "./OauthLogin";
Expand All @@ -25,31 +28,16 @@ const defaultProps = {
authenticating: false,
authenticationError: undefined,
location: emptyLocation,
checkCookieAuthentication: jest.fn(),
checkCookieAuthentication: jest.fn().mockReturnValue({
then: jest.fn(f => f()),
}),
oauthLoginURI: "",
appVersion: "devel",
authProxySkipLoginPage: false,
};

const authenticationError = "it's a trap";

describe("componentDidMount", () => {
it("calls checkCookieAuthentication when oauthLoginURI provided", () => {
const props = {
...defaultProps,
oauthLoginURI: "/sign/in",
};
const checkCookieAuthentication = jest.fn();
mount(<LoginForm {...props} checkCookieAuthentication={checkCookieAuthentication} />);
expect(checkCookieAuthentication).toHaveBeenCalled();
});

it("does not call checkCookieAuthentication when oauthLoginURI not provided", () => {
const checkCookieAuthentication = jest.fn();
mount(<LoginForm {...defaultProps} checkCookieAuthentication={checkCookieAuthentication} />);
expect(checkCookieAuthentication).not.toHaveBeenCalled();
});
});

context("while authenticating", () => {
itBehavesLike("aLoadingComponent", {
component: LoginForm,
Expand All @@ -59,14 +47,13 @@ context("while authenticating", () => {

describe("token login form", () => {
it("renders a token login form", () => {
const wrapper = shallow(<LoginForm {...defaultProps} />);
const wrapper = mount(<LoginForm {...defaultProps} />);
expect(wrapper.find(TokenLogin)).toExist();
expect(wrapper.find(OAuthLogin)).not.toExist();
expect(wrapper).toMatchSnapshot();
});

it("renders a link to the access control documentation", () => {
const wrapper = shallow(<LoginForm {...defaultProps} />);
const wrapper = mount(<LoginForm {...defaultProps} />);
expect(wrapper.find("a").props()).toMatchObject({
href: "https://github.com/kubeapps/kubeapps/blob/devel/docs/user/access-control.md",
target: "_blank",
Expand All @@ -89,15 +76,19 @@ describe("token login form", () => {

describe("redirect if authenticated", () => {
it("redirects to / if no current location", () => {
const wrapper = shallow(<LoginForm {...defaultProps} authenticated={true} />);
const wrapper = mountWrapper(
defaultStore,
<LoginForm {...defaultProps} authenticated={true} />,
);
const redirect = wrapper.find(Redirect);
expect(redirect.props()).toEqual({ to: { pathname: "/" } });
});

it("redirects to previous location", () => {
const location = Object.assign({}, emptyLocation);
location.state = { from: "/test" };
const wrapper = shallow(
const wrapper = mountWrapper(
defaultStore,
<LoginForm {...defaultProps} authenticated={true} location={location} />,
);
const redirect = wrapper.find(Redirect);
Expand Down Expand Up @@ -151,12 +142,26 @@ describe("oauth login form", () => {

it("displays the oauth login if oauthLoginURI provided", () => {
const wrapper = mount(<LoginForm {...props} />);

expect(props.checkCookieAuthentication).toHaveBeenCalled();
expect(wrapper.find(OAuthLogin)).toExist();
expect(wrapper.find("a").findWhere(a => a.prop("href") === props.oauthLoginURI)).toExist();
});

it("renders a login button link", () => {
const wrapper = shallow(<LoginForm {...props} />);
expect(wrapper).toMatchSnapshot();
it("doesn't render the login form if the cookie has not been checked yet", () => {
const checkCookieAuthentication = jest.fn(async () => {
await wait(1);
return true;
});
const wrapper = mount(
<LoginForm {...props} checkCookieAuthentication={checkCookieAuthentication} />,
);
expect(wrapper.find(LoadingWrapper)).toExist();
expect(wrapper.find(OAuthLogin)).not.toExist();
});

it("changes window location when skipping oauth login page", () => {
window.location.replace = jest.fn();
mount(<LoginForm {...props} authProxySkipLoginPage={true} />);
expect(window.location.replace).toHaveBeenCalledWith(props.oauthLoginURI);
});
});
15 changes: 12 additions & 3 deletions dashboard/src/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,26 @@ export interface ILoginFormProps {
authenticating: boolean;
authenticationError: string | undefined;
oauthLoginURI: string;
authProxySkipLoginPage: boolean;
authenticate: (cluster: string, token: string) => any;
checkCookieAuthentication: (cluster: string) => void;
checkCookieAuthentication: (cluster: string) => Promise<boolean>;
appVersion: string;
location: Location;
}

function LoginForm(props: ILoginFormProps) {
const [token, setToken] = useState("");
const [cookieChecked, setCookieChecked] = useState(false);
const { oauthLoginURI, checkCookieAuthentication } = props;
useEffect(() => {
if (oauthLoginURI) {
checkCookieAuthentication(props.cluster);
checkCookieAuthentication(props.cluster).then(() => setCookieChecked(true));
} else {
setCookieChecked(true);
}
}, [oauthLoginURI, checkCookieAuthentication, props.cluster]);

if (props.authenticating) {
if (props.authenticating || !cookieChecked) {
return <LoadingWrapper loaded={false} />;
}
if (props.authenticated) {
Expand All @@ -47,6 +51,11 @@ function LoginForm(props: ILoginFormProps) {
setToken(e.target.value);
};

if (props.oauthLoginURI && props.authProxySkipLoginPage) {
// If the oauth login page should be skipped, simply redirect to the login URI.
window.location.replace(props.oauthLoginURI);
}

return (
<div className="login-wrapper">
<form className="login clr-form" onSubmit={handleSubmit}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`oauth login form renders a login button link 1`] = `
<div
className="login-wrapper"
>
<form
className="login clr-form"
onSubmit={[Function]}
>
<OAuthLogin
oauthLoginURI="/sign/in"
/>
<div
className="login-moreinfo"
>
<a
href="https://github.com/kubeapps/kubeapps/blob/devel/docs/user/access-control.md"
rel="noopener noreferrer"
target="_blank"
>
<CdsIcon
shape="info-circle"
/>
More Info
</a>
</div>
</form>
</div>
`;

exports[`token login form displays an error if the authentication error is passed 1`] = `
<LoginForm
appVersion="devel"
authProxySkipLoginPage={false}
authenticate={[MockFunction]}
authenticated={false}
authenticating={false}
Expand Down Expand Up @@ -152,36 +124,6 @@ exports[`token login form displays an error if the authentication error is passe
</LoginForm>
`;

exports[`token login form renders a token login form 1`] = `
<div
className="login-wrapper"
>
<form
className="login clr-form"
onSubmit={[Function]}
>
<TokenLogin
handleTokenChange={[Function]}
token=""
/>
<div
className="login-moreinfo"
>
<a
href="https://github.com/kubeapps/kubeapps/blob/devel/docs/user/access-control.md"
rel="noopener noreferrer"
target="_blank"
>
<CdsIcon
shape="info-circle"
/>
More Info
</a>
</div>
</form>
</div>
`;

exports[`while authenticating loading spinner matches the snapshot 1`] = `
<LoadingWrapper
loaded={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const makeStore = (
appVersion: "",
oauthLogoutURI: "",
clusters: [],
authProxySkipLoginPage: false,
};
const clusters: IClustersState = {
currentCluster: "default",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { IStoreState } from "../../shared/types";

function mapStateToProps({
auth: { authenticated, authenticating, authenticationError },
config: { authProxyEnabled, oauthLoginURI, appVersion },
config: { authProxyEnabled, oauthLoginURI, appVersion, authProxySkipLoginPage },
clusters,
}: IStoreState) {
return {
Expand All @@ -17,6 +17,7 @@ function mapStateToProps({
authenticationError,
cluster: clusters.currentCluster,
oauthLoginURI: authProxyEnabled ? oauthLoginURI : "",
authProxySkipLoginPage,
appVersion,
};
}
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/reducers/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ describe("clusterReducer", () => {
ui: "hex",
},
clusters: ["additionalCluster1", "additionalCluster2"],
authProxySkipLoginPage: false,
} as IConfig;
it("re-writes the clusters to match the config.clusters state", () => {
expect(
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/reducers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const initialState: IConfigState = {
authProxyEnabled: false,
oauthLoginURI: "",
oauthLogoutURI: "",
authProxySkipLoginPage: false,
clusters: [],
};

Expand Down
2 changes: 2 additions & 0 deletions dashboard/src/shared/Auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ describe("Auth", () => {
kubeappsNamespace: "ns",
appVersion: "2",
clusters: [],
authProxySkipLoginPage: false,
});

expect(mockedAssign).toBeCalledWith(oauthLogoutURI);
Expand All @@ -185,6 +186,7 @@ describe("Auth", () => {
kubeappsNamespace: "ns",
appVersion: "2",
clusters: [],
authProxySkipLoginPage: false,
});

expect(mockedAssign).toBeCalledWith("/oauth2/sign_out");
Expand Down
1 change: 1 addition & 0 deletions dashboard/src/shared/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface IConfig {
authProxyEnabled: boolean;
oauthLoginURI: string;
oauthLogoutURI: string;
authProxySkipLoginPage: boolean;
error?: Error;
clusters: string[];
}
Expand Down

0 comments on commit a4d79be

Please sign in to comment.