Skip to content

Commit

Permalink
Send cluster through to login form and actions. (#1879)
Browse files Browse the repository at this point in the history
* Update shared Auth with cluster arg.

* Update auth actions with cluster arg.

* Update the LoginForm container and components with cluster.
  • Loading branch information
absoludity committed Jul 22, 2020
1 parent 9de9e84 commit a19f5b8
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 42 deletions.
11 changes: 6 additions & 5 deletions dashboard/src/actions/auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getType } from "typesafe-actions";
import actions from ".";
import { Auth } from "../shared/Auth";

const defaultCluster = "default";
const mockStore = configureMockStore([thunk]);
const token = "abcd";
const validationErrorMsg = "Validation error";
Expand Down Expand Up @@ -54,7 +55,7 @@ describe("authenticate", () => {
},
];

return store.dispatch(actions.auth.authenticate(token, false)).then(() => {
return store.dispatch(actions.auth.authenticate("default", token, false)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
Expand All @@ -71,9 +72,9 @@ describe("authenticate", () => {
},
];

return store.dispatch(actions.auth.authenticate(token, false)).then(() => {
return store.dispatch(actions.auth.authenticate(defaultCluster, token, false)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(Auth.validateToken).toHaveBeenCalledWith(token);
expect(Auth.validateToken).toHaveBeenCalledWith(defaultCluster, token);
});
});

Expand All @@ -93,7 +94,7 @@ describe("authenticate", () => {
},
];

return store.dispatch(actions.auth.authenticate("ignored", true)).then(() => {
return store.dispatch(actions.auth.authenticate(defaultCluster, "ignored", true)).then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(Auth.validateToken).not.toHaveBeenCalled();
});
Expand All @@ -120,7 +121,7 @@ describe("OIDC authentication", () => {
},
];

return store.dispatch(actions.auth.checkCookieAuthentication()).then(() => {
return store.dispatch(actions.auth.checkCookieAuthentication("default")).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
Expand Down
16 changes: 7 additions & 9 deletions dashboard/src/actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,15 @@ const allActions = [setAuthenticated, authenticating, authenticationError, setSe
export type AuthAction = ActionType<typeof allActions[number]>;

export function authenticate(
cluster: string,
token: string,
oidc: boolean,
): ThunkAction<Promise<void>, IStoreState, null, AuthAction> {
return async dispatch => {
dispatch(authenticating());
try {
if (!oidc) {
await Auth.validateToken(token);
await Auth.validateToken(cluster, token);
}
Auth.setAuthToken(token, oidc);
dispatch(setAuthenticated(true, oidc, Auth.defaultNamespaceFromToken(token)));
Expand Down Expand Up @@ -74,20 +75,17 @@ export function expireSession(): ThunkAction<Promise<void>, IStoreState, null, A
};
}

export function checkCookieAuthentication(): ThunkAction<
Promise<void>,
IStoreState,
null,
AuthAction
> {
export function checkCookieAuthentication(
cluster: string,
): ThunkAction<Promise<void>, 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
// loading while we query isAuthenticatedWithCookie().
dispatch(authenticating());
const isAuthed = await Auth.isAuthenticatedWithCookie();
const isAuthed = await Auth.isAuthenticatedWithCookie(cluster);
if (isAuthed) {
dispatch(authenticate("", true));
dispatch(authenticate(cluster, "", true));
} else {
dispatch(setAuthenticated(false, false, ""));
}
Expand Down
5 changes: 4 additions & 1 deletion dashboard/src/components/LoginForm/LoginForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ const emptyLocation: Location = {
state: "",
};

const defaultCluster = "default";

const defaultProps = {
cluster: defaultCluster,
authenticate: jest.fn(),
authenticated: false,
authenticating: false,
Expand Down Expand Up @@ -95,7 +98,7 @@ describe("token login form", () => {
const wrapper = shallow(<LoginForm {...defaultProps} />);
wrapper.find("input#token").simulate("change", { currentTarget: { value: "f00b4r" } });
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() });
expect(defaultProps.authenticate).toBeCalledWith("f00b4r");
expect(defaultProps.authenticate).toBeCalledWith(defaultCluster, "f00b4r");
});

it("displays an error if the authentication error is passed", () => {
Expand Down
9 changes: 5 additions & 4 deletions dashboard/src/components/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import LoadingWrapper from "../../components/LoadingWrapper";
import "./LoginForm.css";

export interface ILoginFormProps {
cluster: string;
authenticated: boolean;
authenticating: boolean;
authenticationError: string | undefined;
oauthLoginURI: string;
authenticate: (token: string) => any;
checkCookieAuthentication: () => void;
authenticate: (cluster: string, token: string) => any;
checkCookieAuthentication: (cluster: string) => void;
location: Location;
}

Expand All @@ -25,7 +26,7 @@ class LoginForm extends React.Component<ILoginFormProps, ILoginFormState> {

public componentDidMount() {
if (this.props.oauthLoginURI) {
this.props.checkCookieAuthentication();
this.props.checkCookieAuthentication(this.props.cluster);
}
}

Expand Down Expand Up @@ -65,7 +66,7 @@ class LoginForm extends React.Component<ILoginFormProps, ILoginFormState> {
private handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const { token } = this.state;
return token && (await this.props.authenticate(token));
return token && (await this.props.authenticate(this.props.cluster, token));
};

private handleTokenChange = (e: React.FormEvent<HTMLInputElement>) => {
Expand Down
5 changes: 4 additions & 1 deletion dashboard/src/components/LoginForm/LoginForm.v2.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ const emptyLocation: Location = {
state: "",
};

const defaultCluster = "default";

const defaultProps = {
cluster: defaultCluster,
authenticate: jest.fn(),
authenticated: false,
authenticating: false,
Expand Down Expand Up @@ -111,7 +114,7 @@ describe("token login form", () => {
act(() => {
wrapper.find("form").simulate("submit", { preventDefault: jest.fn() });
});
expect(authenticate).toBeCalledWith("f00b4r");
expect(authenticate).toBeCalledWith(defaultCluster, "f00b4r");
});

it("displays an error if the authentication error is passed", () => {
Expand Down
9 changes: 5 additions & 4 deletions dashboard/src/components/LoginForm/LoginForm.v2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import TokenLogin from "./TokenLogin";
ClarityIcons.addIcons(infoCircleIcon);

export interface ILoginFormProps {
cluster: string;
authenticated: boolean;
authenticating: boolean;
authenticationError: string | undefined;
oauthLoginURI: string;
authenticate: (token: string) => any;
checkCookieAuthentication: () => void;
authenticate: (cluster: string, token: string) => any;
checkCookieAuthentication: (cluster: string) => void;
appVersion: string;
location: Location;
}
Expand All @@ -28,7 +29,7 @@ function LoginForm(props: ILoginFormProps) {
const { oauthLoginURI, checkCookieAuthentication } = props;
useEffect(() => {
if (oauthLoginURI) {
checkCookieAuthentication();
checkCookieAuthentication(props.cluster);
}
}, [oauthLoginURI, checkCookieAuthentication]);

Expand All @@ -42,7 +43,7 @@ function LoginForm(props: ILoginFormProps) {

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
return token && (await props.authenticate(token));
return token && (await props.authenticate(props.cluster, token));
};

const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ exports[`token login form displays an error if the authentication error is passe
authenticating={false}
authenticationError="it's a trap"
checkCookieAuthentication={[MockFunction]}
cluster="default"
location={
Object {
"hash": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { shallow } from "enzyme";
import { Location } from "history";
import * as React from "react";
import { IAuthState } from "reducers/auth";
import { IClustersState } from "reducers/cluster";
import { IConfigState } from "reducers/config";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
Expand Down Expand Up @@ -36,7 +37,16 @@ const makeStore = (
oauthLogoutURI: "",
featureFlags: { operators: false, additionalClusters: [], ui: "hex" },
};
return mockStore({ auth, config });
const clusters: IClustersState = {
currentCluster: "default",
clusters: {
default: {
currentNamespace: "default",
namespaces: [],
},
},
};
return mockStore({ auth, config, clusters });
};

const emptyLocation: Location = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { IStoreState } from "../../shared/types";
function mapStateToProps({
auth: { authenticated, authenticating, authenticationError },
config: { authProxyEnabled, oauthLoginURI, featureFlags, appVersion },
clusters,
}: IStoreState) {
return {
authenticated,
authenticating,
authenticationError,
cluster: clusters.currentCluster,
oauthLoginURI: authProxyEnabled ? oauthLoginURI : "",
UI: featureFlags.ui,
appVersion,
Expand All @@ -22,8 +24,10 @@ function mapStateToProps({

function mapDispatchToProps(dispatch: ThunkDispatch<IStoreState, null, Action>) {
return {
authenticate: (token: string) => dispatch(actions.auth.authenticate(token, false)),
checkCookieAuthentication: () => dispatch(actions.auth.checkCookieAuthentication()),
authenticate: (cluster: string, token: string) =>
dispatch(actions.auth.authenticate(cluster, token, false)),
checkCookieAuthentication: (cluster: string) =>
dispatch(actions.auth.checkCookieAuthentication(cluster)),
};
}

Expand Down
19 changes: 9 additions & 10 deletions dashboard/src/shared/Auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Axios, { AxiosResponse } from "axios";
import * as jwt from "jsonwebtoken";
import { Auth } from "./Auth";
import { APIBase } from "./Kube";

describe("Auth", () => {
beforeEach(() => {
Expand All @@ -13,9 +12,9 @@ describe("Auth", () => {
it("should get an URL with the given token", async () => {
const mock = jest.fn();
Axios.get = mock;
await Auth.validateToken("foo");
await Auth.validateToken("othercluster", "foo");
expect(mock.mock.calls[0]).toEqual([
"api/clusters/default/",
"api/clusters/othercluster/",
{ headers: { Authorization: "Bearer foo" } },
]);
});
Expand Down Expand Up @@ -52,7 +51,7 @@ describe("Auth", () => {
// to upgrade jest for `toThrow()` to work with async.
let err = null;
try {
await Auth.validateToken("foo");
await Auth.validateToken("default", "foo");
} catch (e) {
err = e;
} finally {
Expand All @@ -65,9 +64,9 @@ describe("Auth", () => {
describe("isAuthenticatedWithCookie", () => {
it("returns true if request to API root succeeds", async () => {
Axios.get = jest.fn().mockReturnValue(Promise.resolve({ headers: { status: 200 } }));
const isAuthed = await Auth.isAuthenticatedWithCookie();
const isAuthed = await Auth.isAuthenticatedWithCookie("somecluster");

expect(Axios.get).toBeCalledWith(APIBase + "/");
expect(Axios.get).toBeCalledWith("api/clusters/somecluster/");
expect(isAuthed).toBe(true);
});
it("returns false if the request to api root results in a 403 for an anonymous request", async () => {
Expand All @@ -79,7 +78,7 @@ describe("Auth", () => {
},
});
});
const isAuthed = await Auth.isAuthenticatedWithCookie();
const isAuthed = await Auth.isAuthenticatedWithCookie("somecluster");
expect(isAuthed).toBe(false);
});
it("returns false if the request to api root results in a non-json response (ie. without data.message)", async () => {
Expand All @@ -90,7 +89,7 @@ describe("Auth", () => {
},
});
});
const isAuthed = await Auth.isAuthenticatedWithCookie();
const isAuthed = await Auth.isAuthenticatedWithCookie("somecluster");
expect(isAuthed).toBe(false);
});
it("returns true if the request to api root results in a 403 (but not anonymous)", async () => {
Expand All @@ -102,14 +101,14 @@ describe("Auth", () => {
},
});
});
const isAuthed = await Auth.isAuthenticatedWithCookie();
const isAuthed = await Auth.isAuthenticatedWithCookie("somecluster");
expect(isAuthed).toBe(true);
});
it("should return false if the request results in a 401", async () => {
Axios.get = jest.fn(() => {
return Promise.reject({ response: { status: 401 } });
});
const isAuthed = await Auth.isAuthenticatedWithCookie();
const isAuthed = await Auth.isAuthenticatedWithCookie("somecluster");
expect(isAuthed).toBe(false);
});
});
Expand Down
12 changes: 7 additions & 5 deletions dashboard/src/shared/Auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Axios, { AxiosResponse } from "axios";
import * as jwt from "jsonwebtoken";
import * as url from "shared/url";
import { IConfig } from "./Config";
import { APIBase } from "./Kube";
import { definedNamespaces } from "./Namespace";

const AuthTokenKey = "kubeapps_auth_token";
Expand Down Expand Up @@ -57,9 +57,11 @@ export class Auth {
}

// Throws an error if the token is invalid
public static async validateToken(token: string) {
public static async validateToken(cluster: string, token: string) {
try {
await Axios.get(APIBase + "/", { headers: { Authorization: `Bearer ${token}` } });
await Axios.get(url.api.k8s.base(cluster) + "/", {
headers: { Authorization: `Bearer ${token}` },
});
} catch (e) {
const res = e.response as AxiosResponse;
if (res.status === 401) {
Expand Down Expand Up @@ -96,9 +98,9 @@ export class Auth {
// isAuthenticatedWithCookie() does an anonymous GET request to determine if
// the request is authenticated with an http-only cookie (there is, by design,
// no way to determine via client JS whether an http-only cookie is present).
public static async isAuthenticatedWithCookie(): Promise<boolean> {
public static async isAuthenticatedWithCookie(cluster: string): Promise<boolean> {
try {
await Axios.get(APIBase + "/");
await Axios.get(url.api.k8s.base(cluster) + "/");
} catch (e) {
const response = e.response as AxiosResponse;
// The only error response which can possibly mean we did authenticate is
Expand Down

0 comments on commit a19f5b8

Please sign in to comment.