Skip to content

Commit

Permalink
[ROCK-12171] SSO through KEOS oauth2proxy (#308)
Browse files Browse the repository at this point in the history
  • Loading branch information
grios-stratio authored Sep 27, 2024
1 parent ab79bb0 commit 6205bfb
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 75 deletions.
3 changes: 2 additions & 1 deletion .clj-kondo/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,8 @@
metabase.shared.util.namespaces
metabase.shared.util.time} ; TODO -- consolidate these into a real API namespace.
metabase.stratio #{metbase.stratio
metabase.stratio.middleware}
metabase.stratio.middleware
metabase.stratio.config}
metabase.sync #{metabase.sync
metabase.sync.analyze
metabase.sync.concurrent
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## 0.50.21-0.1.0 (upcoming)

* [ROCK-11848] Update metabase to 0.50
* [ROCK-12171] SSO via KEOS oauth2proxy
* [ROCK-12227] Fix: X-ray suggestions do not appear on first app load
* [ROCK-11267] Fix: Improper tenant validation
* [ROCK-11122] Fix: Failure to invalidate session on logout

## 0.43.4-0.2.0 (2024-02-01)

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/metabase-types/api/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,10 @@ interface PublicSettings {
"token-features": TokenFeatures;
version: Version;
"version-info-last-checked": string | null;
// < STRATIO - auto login from headers/jwt
"gosec-sso-enabled": boolean;
"stratio-logout-url": string;
// STRATIO >
}

export type UserSettings = {
Expand Down
7 changes: 4 additions & 3 deletions frontend/src/metabase/auth/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,11 @@ export const logout = createAsyncThunk(
dispatch(clearCurrentUser());
await dispatch(refreshLocale()).unwrap();
trackLogout();
// < STRATIO - auto login from headers info (sso proxy integration)
// < STRATIO - auto login from headers/jwt
// we must redirect to the sso proxy logout
if (MetabaseSettings.get("gosec-sso-enabled", false)) {
dispatch(push("logout"));
if (getSetting(state, "gosec-sso-enabled")) {
window.location.href = getSetting(state, "stratio-logout-url");
return
} else {
dispatch(push(Urls.login()));
}
Expand Down
91 changes: 64 additions & 27 deletions frontend/src/metabase/auth/components/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import type { AuthProvider } from "metabase/plugins/types";
import { getApplicationName } from "metabase/selectors/whitelabel";
import { Box } from "metabase/ui";

import { getAuthProviders } from "../../selectors";
// < STRATIO import getSSOEnabled selector and AuthButton
import {
getAuthProviders,
getSSOEnabled,
getStratioLogoutUrl,
} from "../../selectors";
import { AuthButton } from "../AuthButton";
// STRATIO >
import { AuthLayout } from "../AuthLayout";

interface LoginQueryString {
Expand All @@ -27,34 +34,64 @@ export const Login = ({ params, location }: LoginProps): JSX.Element => {
const selection = getSelectedProvider(providers, params?.provider);
const redirectUrl = location?.query?.redirect;
const applicationName = useSelector(getApplicationName);
return (
<AuthLayout>
<Box
role="heading"
c="text-dark"
fz="1.25rem"
fw="bold"
lh="1.5rem"
ta="center"
>
{t`Sign in to ${applicationName}`}
</Box>
{selection && selection.Panel && (
<Box mt="2.5rem">
<selection.Panel redirectUrl={redirectUrl} />
const gosecSSOEnabled = useSelector(getSSOEnabled);
const stratioLogoutUrl = useSelector(getStratioLogoutUrl);

// < STRATIO - login via headers/jwt - do not show login form
// if we reach here it means we have jwt but we are not allowed so no session => 401 => routed to auth/login
if (gosecSSOEnabled) {
return (
<AuthLayout>
<Box
role="heading"
c="text-dark"
fz="1.25rem"
fw="bold"
lh="1.5rem"
ta="center"
>
{t`You are not allowed to access ${applicationName}`}
</Box>
)}
{!selection && (
<Box mt="3.5rem">
{providers.map(provider => (
<Box key={provider.name} mt="2rem" ta="center">
<provider.Button isCard={true} redirectUrl={redirectUrl} />
</Box>
))}
<AuthButton
isCard={true}
onClick={() => (window.location.href = stratioLogoutUrl)}
>
{"Logout"}
</AuthButton>
</AuthLayout>
);
}
// STRATIO >
else {
return (
<AuthLayout>
<Box
role="heading"
c="text-dark"
fz="1.25rem"
fw="bold"
lh="1.5rem"
ta="center"
>
{t`Sign in to ${applicationName}`}
</Box>
)}
</AuthLayout>
);
{selection && selection.Panel && (
<Box mt="2.5rem">
<selection.Panel redirectUrl={redirectUrl} />
</Box>
)}
{!selection && (
<Box mt="3.5rem">
{providers.map(provider => (
<Box key={provider.name} mt="2rem" ta="center">
<provider.Button isCard={true} redirectUrl={redirectUrl} />
</Box>
))}
</Box>
)}
</AuthLayout>
);
}
};

const getSelectedProvider = (
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/metabase/auth/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,13 @@ export const getSiteLocale = (state: State) => {
export const getGoogleClientId = (state: State) => {
return getSetting(state, "google-auth-client-id");
};

// < STRATIO login via headers/jwt - selector to use in login page
export const getSSOEnabled = (state: State) => {
return getSetting(state, "gosec-sso-enabled");
};

export const getStratioLogoutUrl = (state: State) => {
return getSetting(state, "stratio-logout-url");
};
// STRATIO >
23 changes: 22 additions & 1 deletion frontend/src/metabase/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ import {
IsAuthenticated,
IsNotAuthenticated,
} from "./route-guards";
// < STRATIO - login via headers/jwt - we need this below to get our gosec-sso-enabled setting
import { getSetting } from "./selectors/settings";
// < STRATIO
import { getApplicationName } from "./selectors/whitelabel";

export const getRoutes = store => {
Expand Down Expand Up @@ -115,7 +118,25 @@ export const getRoutes = store => {
<Route path="/auth">
<IndexRedirect to="/auth/login" />
<Route component={IsNotAuthenticated}>
<Route path="login" title={t`Login`} component={Login} />
<Route
path="login"
title={t`Login`}
component={Login}
// < STRATIO - login via headers/jwt - reload page when sent to auth/login so that oauthredirect or autologin kicks in
onEnter={(nextState, replace) => {
const gosecSSOEnabled = getSetting(
store.getState(),
"gosec-sso-enabled",
);
const hasBeenRedirected =
nextState.location.action === "REPLACE" ||
nextState.location.action === "PUSH";
if (gosecSSOEnabled && hasBeenRedirected) {
window.location.reload();
}
}}
// STRATIO >
/>
<Route path="login/:provider" title={t`Login`} component={Login} />
</Route>
<Route path="logout" component={Logout} />
Expand Down
9 changes: 8 additions & 1 deletion src/metabase/setup.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
[metabase.config :as config]
[metabase.db :as mdb]
[metabase.models.setting :as setting :refer [defsetting Setting]]
;; < STATIO - login via headers/jwt - nevet go to setup page
[metabase.stratio.config :as st.config]
;; STRATIO >
[metabase.util.i18n :refer [deferred-tru tru]]
[toucan2.core :as t2]))

Expand Down Expand Up @@ -62,6 +65,10 @@
(or (get @app-db-id->user-exists? (mdb/unique-identifier))
(let [exists? (boolean (seq (t2/select :model/User {:where [:not= :id config/internal-mb-user-id]})))]
(swap! app-db-id->user-exists? assoc (mdb/unique-identifier) exists?)
exists?))))))
exists?)
;; < STRATIO - login via headers/jwt - never go to setup page
st.config/should-auto-login?
;; STRATIO >
)))))
:doc false
:audit :never)
19 changes: 14 additions & 5 deletions src/metabase/stratio/auth.clj
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@
;; a set can act as a predicate!
(some whitelist groups)))

(defn- tenant-allowed?
[user-tenants]
(let [app-tenant (st.config/config-str :tenant)]
(or (not (seq user-tenants))
(not app-tenant)
(contains? (set user-tenants) app-tenant))))

(defn- admin?
[groups]
(contains? (set groups) admin-group))
Expand All @@ -41,14 +48,16 @@
superuser? (conj perms-group/admin-group-name)))

(defn- allowed-user
[{:keys [user groups error]}]
[{:keys [user groups email tenants error]}]
(if error
{:error error}
(if (allowed? groups)
(if (and (allowed? groups) (tenant-allowed? tenants))
{:first_name user
:last_name ""
:is_superuser (admin? groups)
:email (if (u/email? user) user (u/lower-case-en (str user dummy-email-domain)))
:email (cond email email
(u/email? user) user
:else (u/lower-case-en (str user dummy-email-domain)))
:login_attributes {:groups groups}}
{:error (str "User " user " not allowed")})))

Expand Down Expand Up @@ -100,8 +109,8 @@
"Reads the SSO user info in the request (either as jwt or as plain headers) and returs a 'user' (a map with some
user-related keys, including a valid Metbase session in :session. If the user does not exists in the Metabse DB,
it is created, and optionally, their groups are also created and synced."
[{headers :headers, :as request}]
(let [user-info (http-headers->user-info headers)
[request]
(let [user-info (http-headers->user-info request)
allowed-user (allowed-user user-info)]
(log/debug "received user info " user-info)
(if (:error allowed-user)
Expand Down
47 changes: 41 additions & 6 deletions src/metabase/stratio/config.clj
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(ns metabase.stratio.config
(:require
[buddy.core.keys :as keys]
[clj-http.client :as http]
[metabase.config :as config]
[metabase.models.setting :refer [defsetting]]
[metabase.stratio.util :as st.util]))
Expand All @@ -13,8 +15,14 @@
:jwt-header-name "X-USER-TOKEN"
:jwt-username-claim "sub"
:jwt-groups-claim "groups"
:jwt-tenants-claim "tenants"
:jwt-email-claim "mail"
:jwt-public-key-location "url"
:jwt-public-key-endpoint ""
:jwt-public-key-file "/etc/pki/jwt-public-key.pem"
:jwt-insecure-request-pkey "false"
:jwt-cookie-name "stratio-cookie"
:oauth2proxy-logout-url "localhost:3000/stratio-logout"

;; settings for authentication via headers
:mb-user-header ""
Expand All @@ -33,17 +41,44 @@
(defn config-kw [k] (some-> k config-str keyword))
(defn config-vector [k] (st.util/make-vector (config-str k)))

(defn- ssl-config []
(if (config-bool :jwt-insecure-request-pkey)
{:insecure? true}
{:trust-store (config-str :mb-jetty-ssl-truststore)
:trust-store-pass (config-str :mb-jetty-ssl-truststore-password)}))

(def auto-login-authenticators #{:jwt :headers :gosec-sso})
(def authenticator (config-kw :authenticator))
(def should-auto-login? (contains? auto-login-authenticators authenticator))
(def jwt? (= authenticator :jwt))
(def gosec-sso? (= authenticator :gosec-sso))
(def authenticator (config-kw :authenticator))
(def jwt-public-key-location (config-kw :jwt-public-key-location))
(def should-auto-login? (contains? auto-login-authenticators authenticator))
(def jwt? (= authenticator :jwt))
(def gosec-sso? (= authenticator :gosec-sso))
(def oauth2proxy-logout-url (config-str :oauth2proxy-logout-url))
(def headers? (= authenticator :headers))
(def jwt-cookie-name (config-str :jwt-cookie-name))
(def jwt-public-key (delay
(if (= jwt-public-key-location :file)
(-> (config-str :jwt-public-key-file)
(slurp)
(keys/str->public-key))
(-> (config-str :jwt-public-key-endpoint)
(http/get (ssl-config))
(:body)
(keys/str->public-key)))))

;; We need to define a setting so it can reach frontend via MetabaseSettings object
;; We need to define these as settings so they can reach frontend via MetabaseSettings object
(defsetting gosec-sso-enabled
"flag to tell the front end if we are behing the sso proxy so when logout redirect to proxy logout"
"flag to tell the front end if we are behind the sso proxy so when logout redirect to proxy logout"
:type :boolean
:default gosec-sso?
:visibility :public
:setter :none
:export? false)

(defsetting stratio-logout-url
"url to send the users to to finish sso logout"
:type :string
:default oauth2proxy-logout-url
:visibility :public
:setter :none
:export? false)
Loading

0 comments on commit 6205bfb

Please sign in to comment.