From 2fee5a8db7cd42f962dfb2dd0c786f18aec39996 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Sun, 5 Nov 2023 20:49:56 -0500 Subject: [PATCH] Initial add auth to homepage --- environment.yml | 14 +++++++++++ src/pages/api/bookmarks.js | 4 ++- src/pages/api/services/index.js | 5 +++- src/pages/api/widgets/index.js | 5 +++- src/pages/index.jsx | 18 ++++++++------ src/utils/auth/auth-helpers.js | 42 ++++++++++++++++++++++++++++++++ src/utils/auth/proxy.js | 42 ++++++++++++++++++++++++++++++++ src/utils/config/api-response.js | 39 +++++++++++++++++------------ 8 files changed, 144 insertions(+), 25 deletions(-) create mode 100644 environment.yml create mode 100644 src/utils/auth/auth-helpers.js create mode 100644 src/utils/auth/proxy.js diff --git a/environment.yml b/environment.yml new file mode 100644 index 00000000..6d271a9f --- /dev/null +++ b/environment.yml @@ -0,0 +1,14 @@ +name: node18 +channels: + - conda-forge + - defaults +dependencies: + - ca-certificates=2023.7.22=hf0a4a13_0 + - icu=73.2=hc8870d7_0 + - libcxx=16.0.6=h4653b0c_0 + - libuv=1.46.0=hb547adb_0 + - libzlib=1.2.13=h53f4e23_5 + - nodejs=18.17.1=h7ed3092_1 + - openssl=3.1.4=h0d3ecfb_0 + - pnpm=8.10.0=h992f1b1_0 + - zlib=1.2.13=h53f4e23_5 diff --git a/src/pages/api/bookmarks.js b/src/pages/api/bookmarks.js index 63d1e29e..ae50c279 100644 --- a/src/pages/api/bookmarks.js +++ b/src/pages/api/bookmarks.js @@ -1,5 +1,7 @@ +import { createAuthFromSettings } from "utils/auth/auth-helpers"; import { bookmarksResponse } from "utils/config/api-response"; export default async function handler(req, res) { - res.send(await bookmarksResponse()); + const auth = createAuthFromSettings() + res.send(await bookmarksResponse(auth.permissions(req))); } diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js index 46d0a721..3140d6eb 100644 --- a/src/pages/api/services/index.js +++ b/src/pages/api/services/index.js @@ -1,5 +1,8 @@ +import { createAuthFromSettings } from "utils/auth/auth-helpers"; import { servicesResponse } from "utils/config/api-response"; export default async function handler(req, res) { - res.send(await servicesResponse()); + const auth = createAuthFromSettings() + + res.send(await servicesResponse(auth.permissions(req))); } diff --git a/src/pages/api/widgets/index.js b/src/pages/api/widgets/index.js index 513e02e9..eae40397 100644 --- a/src/pages/api/widgets/index.js +++ b/src/pages/api/widgets/index.js @@ -1,5 +1,8 @@ +import { createAuthFromSettings } from "utils/auth/auth-helpers"; import { widgetsResponse } from "utils/config/api-response"; export default async function handler(req, res) { - res.send(await widgetsResponse()); + const auth = createAuthFromSettings(); + + res.send(await widgetsResponse(auth.permissions(req))); } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 4b922ccf..590d39bb 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,6 +1,7 @@ /* eslint-disable react/no-array-index-key */ import useSWR, { SWRConfig } from "swr"; import Head from "next/head"; +import {headers} from "next/header"; import dynamic from "next/dynamic"; import classNames from "classnames"; import { useTranslation } from "next-i18next"; @@ -27,6 +28,7 @@ import ErrorBoundary from "components/errorboundry"; import themes from "utils/styles/themes"; import QuickLaunch from "components/quicklaunch"; import { getStoredProvider, searchProviders } from "components/widgets/search/search"; +import { NullPermissions, createAuthFromSettings } from "utils/auth/auth-helpers"; const ThemeToggle = dynamic(() => import("components/toggles/theme"), { ssr: false, @@ -46,11 +48,11 @@ export async function getStaticProps() { let logger; try { logger = createLogger("index"); - const { providers, ...settings } = getSettings(); + const { providers, auth, ...settings } = getSettings(); - const services = await servicesResponse(); - const bookmarks = await bookmarksResponse(); - const widgets = await widgetsResponse(); + const services = await servicesResponse(NullPermissions); + const bookmarks = await bookmarksResponse(NullPermissions); + const widgets = await widgetsResponse(NullPermissions); return { props: { @@ -179,9 +181,11 @@ function Home({ initialSettings }) { setSettings(initialSettings); }, [initialSettings, setSettings]); - const { data: services } = useSWR("/api/services"); - const { data: bookmarks } = useSWR("/api/bookmarks"); - const { data: widgets } = useSWR("/api/widgets"); + const auth = createAuthFromSettings(); + + const { data: services } = useSWR(auth.cacheContext("/api/services"), auth.fetcher); + const { data: bookmarks } = useSWR(auth.cacheContext("/api/bookmarks"), auth.fetcher); + const { data: widgets } = useSWR(auth.cacheContext("/api/widgets"), auth.fetcher); const servicesAndBookmarks = [ ...services.map((sg) => sg.services).flat(), diff --git a/src/utils/auth/auth-helpers.js b/src/utils/auth/auth-helpers.js new file mode 100644 index 00000000..bf3ae242 --- /dev/null +++ b/src/utils/auth/auth-helpers.js @@ -0,0 +1,42 @@ +import { getSettings } from "utils/config/config"; +import { ProxyAuthKey, createProxyAuth } from "./proxy"; + +export const NullPermissions = { user: null, groups:[]} + +export const NullAuth = { + permissions: (request) => NullPermissions, + cacheContext: (key) => key, + fetcher: (key) => fetch(key).then((res) => res.json()) +} + +export function createAuthFromSettings() { + const {auth} = getSettings(); + if (auth) { + switch (Object.keys(auth)[0]) { + case ProxyAuthKey: + return createProxyAuth(auth[ProxyAuthKey]); + default: + return NullAuth; + } + } + return NullAuth +} + +export const filterAllowedServices = (perms, services) => filterAllowedItems(perms, services, 'services'); +export const filterAllowedBookmarks = (perms, bookmarks) => filterAllowedItems(perms, bookmarks, 'bookmarks'); +export const filterAllowedWidgets = (perms, widgets) => filterAllowedItems(perms, widgets, 'widgets') + +function filterAllowedItems({user, groups}, itemGroups, groupKey) { + return itemGroups.map((group) => ({ + name: group.name, + [groupKey]: group[groupKey].filter((item) => authItemFilter({user, groups}, item)) + })).filter((group) => !group[groupKey].length) +} + +function authItemFilter({user, groups}, item) { + const groupAllow = (!(allowGroups in item)) || groups.some(group => item.allowGroups.includes(group)); + const userAllow = (!(allowUsers in item)) || item.allowUsers.includes(user); + + return userAllow || groupAllow; +} + diff --git a/src/utils/auth/proxy.js b/src/utils/auth/proxy.js new file mode 100644 index 00000000..17c51328 --- /dev/null +++ b/src/utils/auth/proxy.js @@ -0,0 +1,42 @@ +// Proxy auth is meant to be used by a reverse proxy that injects permission headers into the origin +// request. In this case we are relying on our proxy to authenitcate our users and validate. +import {createLogger} from "utils/logger"; +import { headers } from 'next/headers'; + +export const ProxyAuthKey="proxy_auth" + + +function getProxyPermissions(userHeader, groupHeader, request) { + const logger = createLogger("proxyAuth") + const user = (userHeader)?request.headers.get(userHeader):None; + if (!user) { + logger.debug("unable to retreive user. User header doesn't exist or unspecified.") + } + const groupsString = (groupHeader)?request.headers.get(groupHeader):""; + if (!groupsString) { + logger.debug("unable to retrieve groups. Groups header doesn't exist or unspecified") + } + + return {user: user, groups: (groupsString)?groupsString.split(",").map((v) => v.trimStart()):[]} +} + +export function createProxyAuth({groupHeader, userHeader}) { + const logger = createLogger("proxyAuth") + + if (!userHeader) { + logger.debug("'userHeader' value not specified"); + } + if (!groupHeader) { + logger.debug("'groupHeader' value not specified") + } + return { + permissions : (request) => getProxyPermissions(userHeader, groupHeader, request), + cacheContext: (key) => [ key, { + ...userHeader && {[userHeader]: headers.get(userHeader) }, + ...groupHeader && {[groupHeader]: headers.get(groupHeader)} + }], + fetcher: ([key, context]) => { + fetch(key, {headers: context}).then((res) => res.json()) + } + } +} \ No newline at end of file diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 97f61ea0..bd86670e 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -12,6 +12,13 @@ import { servicesFromKubernetes, } from "utils/config/service-helpers"; import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers"; +import { filterAuthBookmarks } from "utils/auth/auth-helpers"; + +import { + filterAllowedBookmarks, + filterAllowedServices, + filterAllowedWidgets +} from "utils/auth/auth-helpers"; /** * Compares services by weight then by name. @@ -24,13 +31,13 @@ function compareServices(service1, service2) { return service1.name.localeCompare(service2.name); } -export async function bookmarksResponse() { +export async function bookmarksResponse(perms) { checkAndCopyConfig("bookmarks.yaml"); const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml"); const rawFileContents = await fs.readFile(bookmarksYaml, "utf8"); const fileContents = substituteEnvironmentVars(rawFileContents); - const bookmarks = yaml.load(fileContents); + const bookmarks = yaml.load(fileContents); if (!bookmarks) return []; @@ -45,13 +52,15 @@ export async function bookmarksResponse() { } // map easy to write YAML objects into easy to consume JS arrays - const bookmarksArray = bookmarks.map((group) => ({ - name: Object.keys(group)[0], - bookmarks: group[Object.keys(group)[0]].map((entries) => ({ - name: Object.keys(entries)[0], - ...entries[Object.keys(entries)[0]][0], - })), - })); + const bookmarksArray = filterAllowedBookmarks(perms, + bookmarks.map((group) => ({ + name: Object.keys(group)[0], + bookmarks: group[Object.keys(group)[0]].map((entries) => ({ + name: Object.keys(entries)[0], + ...entries[Object.keys(entries)[0]][0], + })), + })) + ); const sortedGroups = []; const unsortedGroups = []; @@ -70,11 +79,11 @@ export async function bookmarksResponse() { return [...sortedGroups.filter((g) => g), ...unsortedGroups]; } -export async function widgetsResponse() { +export async function widgetsResponse(perms) { let configuredWidgets; try { - configuredWidgets = cleanWidgetGroups(await widgetsFromConfig()); + configuredWidgets = filterAllowedWidgets(perms, cleanWidgetGroups(await widgetsFromConfig())); } catch (e) { console.error("Failed to load widgets, please check widgets.yaml for errors or remove example entries."); if (e) console.error(e); @@ -84,14 +93,14 @@ export async function widgetsResponse() { return configuredWidgets; } -export async function servicesResponse() { +export async function servicesResponse(perms) { let discoveredDockerServices; let discoveredKubernetesServices; let configuredServices; let initialSettings; try { - discoveredDockerServices = cleanServiceGroups(await servicesFromDocker()); + discoveredDockerServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromDocker())); if (discoveredDockerServices?.length === 0) { console.debug("No containers were found with homepage labels."); } @@ -102,7 +111,7 @@ export async function servicesResponse() { } try { - discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes()); + discoveredKubernetesServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromKubernetes())); } catch (e) { console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries."); if (e) console.error(e.toString()); @@ -110,7 +119,7 @@ export async function servicesResponse() { } try { - configuredServices = cleanServiceGroups(await servicesFromConfig()); + configuredServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromConfig())); } catch (e) { console.error("Failed to load services.yaml, please check for errors"); if (e) console.error(e.toString());