From 2fee5a8db7cd42f962dfb2dd0c786f18aec39996 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Sun, 5 Nov 2023 20:49:56 -0500 Subject: [PATCH 1/8] 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()); From a8bedc24a427e1d5abf328ed474b37285c3bdf76 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Tue, 7 Nov 2023 07:41:11 -0500 Subject: [PATCH 2/8] First somewhat working image --- src/pages/api/bookmarks.js | 5 ++-- src/pages/api/services/index.js | 14 ++++++--- src/pages/api/widgets/index.js | 5 ++-- src/pages/index.jsx | 50 ++++++++++++++++--------------- src/utils/auth/auth-helpers.js | 41 ++++++++++++------------- src/utils/auth/null.js | 21 +++++++++++++ src/utils/auth/proxy.js | 51 ++++++++++++++------------------ src/utils/config/api-response.js | 2 +- 8 files changed, 107 insertions(+), 82 deletions(-) create mode 100644 src/utils/auth/null.js diff --git a/src/pages/api/bookmarks.js b/src/pages/api/bookmarks.js index ae50c279..a7b0ae17 100644 --- a/src/pages/api/bookmarks.js +++ b/src/pages/api/bookmarks.js @@ -1,7 +1,8 @@ -import { createAuthFromSettings } from "utils/auth/auth-helpers"; +import { createAuthProvider } from "utils/auth/auth-helpers"; import { bookmarksResponse } from "utils/config/api-response"; +import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const auth = createAuthFromSettings() + const auth = createAuthProvider(getSettings()) res.send(await bookmarksResponse(auth.permissions(req))); } diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js index 3140d6eb..749ee420 100644 --- a/src/pages/api/services/index.js +++ b/src/pages/api/services/index.js @@ -1,8 +1,14 @@ -import { createAuthFromSettings } from "utils/auth/auth-helpers"; +import { createAuthProvider } from "utils/auth/auth-helpers"; import { servicesResponse } from "utils/config/api-response"; +import { getSettings } from "utils/config/config"; +import createLogger from "utils/logger"; + +let logger = createLogger("services_index") export default async function handler(req, res) { - const auth = createAuthFromSettings() - - res.send(await servicesResponse(auth.permissions(req))); + logger.log("Call services"); + const auth = createAuthProvider(getSettings) + const result = await servicesResponse(auth.permissions(req)) + logger.log(result); + res.send(result); } diff --git a/src/pages/api/widgets/index.js b/src/pages/api/widgets/index.js index eae40397..beb642f3 100644 --- a/src/pages/api/widgets/index.js +++ b/src/pages/api/widgets/index.js @@ -1,8 +1,9 @@ -import { createAuthFromSettings } from "utils/auth/auth-helpers"; +import { createAuthProvider } from "utils/auth/auth-helpers"; import { widgetsResponse } from "utils/config/api-response"; +import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const auth = createAuthFromSettings(); + const auth = createAuthProvider(getSettings()); res.send(await widgetsResponse(auth.permissions(req))); } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 590d39bb..6701b3a8 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,7 +1,6 @@ /* eslint-disable react/no-array-index-key */ -import useSWR, { SWRConfig } from "swr"; +import useSWR, { unstable_serialize, 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"; @@ -28,8 +27,8 @@ 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"; - +import { createAuthorizer, fetchWithAuth } from "utils/auth/auth-helpers"; +import { NullAuthProvider } from "utils/auth/null"; const ThemeToggle = dynamic(() => import("components/toggles/theme"), { ssr: false, }); @@ -44,25 +43,28 @@ const Version = dynamic(() => import("components/version"), { const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "openmeteo", "search", "datetime"]; -export async function getStaticProps() { +export async function getServerSideProps({req}) { let logger; try { logger = createLogger("index"); const { providers, auth, ...settings } = getSettings(); + const authProvider = createAuthorizer({auth: auth}); - const services = await servicesResponse(NullPermissions); - const bookmarks = await bookmarksResponse(NullPermissions); - const widgets = await widgetsResponse(NullPermissions); + const services = await servicesResponse(authProvider.authorize(req)); + const bookmarks = await bookmarksResponse(authProvider.authorize(req)); + const widgets = await widgetsResponse(authProvider.authorize(req)); + const authContext = authProvider.getContext(req); return { props: { initialSettings: settings, fallback: { - "/api/services": services, - "/api/bookmarks": bookmarks, - "/api/widgets": widgets, + [unstable_serialize(["/api/services", authContext])]: services, + [unstable_serialize(["/api/bookmarks", authContext])]: bookmarks, + [unstable_serialize(["/api/widgets", authContext])]: widgets, "/api/hash": false, }, + authContext: authContext, ...(await serverSideTranslations(settings.language ?? "en")), }, }; @@ -70,22 +72,24 @@ export async function getStaticProps() { if (logger) { logger.error(e); } + const authContext = NullAuthProvider.create().getContext(req); return { props: { initialSettings: {}, fallback: { - "/api/services": [], - "/api/bookmarks": [], - "/api/widgets": [], + [unstable_serialize(["/api/services", authContext])]: [], + [unstable_serialize(["/api/bookmarks", authContext])]: [], + [unstable_serialize(["/api/widgets", authContext])]: [], "/api/hash": false, }, + authContext: authContext, ...(await serverSideTranslations("en")), }, }; } } -function Index({ initialSettings, fallback }) { +function Index({ initialSettings, fallback, authContext }) { const windowFocused = useWindowFocus(); const [stale, setStale] = useState(false); const { data: errorsData } = useSWR("/api/validate"); @@ -155,7 +159,7 @@ function Index({ initialSettings, fallback }) { return ( fetch(resource, init).then((res) => res.json()) }}> - + ); @@ -169,7 +173,7 @@ const headerStyles = { boxedWidgets: "m-6 mb-0 sm:m-9 sm:mb-0 sm:mt-1", }; -function Home({ initialSettings }) { +function Home({ initialSettings, authContext }) { const { i18n } = useTranslation(); const { theme, setTheme } = useContext(ThemeContext); const { color, setColor } = useContext(ColorContext); @@ -181,11 +185,9 @@ function Home({ initialSettings }) { setSettings(initialSettings); }, [initialSettings, setSettings]); - 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 { data: services } = useSWR(["/api/services", authContext], fetchWithAuth); + const { data: bookmarks } = useSWR(["/api/bookmarks", authContext], fetchWithAuth); + const { data: widgets } = useSWR(["/api/widgets", authContext], fetchWithAuth); const servicesAndBookmarks = [ ...services.map((sg) => sg.services).flat(), @@ -470,7 +472,7 @@ function Home({ initialSettings }) { ); } -export default function Wrapper({ initialSettings, fallback }) { +export default function Wrapper({ initialSettings, fallback, authContext }) { const wrappedStyle = {}; let backgroundBlur = false; let backgroundSaturate = false; @@ -521,7 +523,7 @@ export default function Wrapper({ initialSettings, fallback }) { backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`, )} > - + diff --git a/src/utils/auth/auth-helpers.js b/src/utils/auth/auth-helpers.js index bf3ae242..a6c2ccbb 100644 --- a/src/utils/auth/auth-helpers.js +++ b/src/utils/auth/auth-helpers.js @@ -1,41 +1,42 @@ -import { getSettings } from "utils/config/config"; -import { ProxyAuthKey, createProxyAuth } from "./proxy"; +import { ProxyAuthProvider} from "./proxy"; +import { NullAuthProvider} from "./null"; -export const NullPermissions = { user: null, groups:[]} +const AuthProviders = { + NullAuthProvider, + ProxyAuthProvider +}; -export const NullAuth = { - permissions: (request) => NullPermissions, - cacheContext: (key) => key, - fetcher: (key) => fetch(key).then((res) => res.json()) +function getProviderByKey(key) { + return AuthProviders.find((provider) => provider.key == key) ?? NullAuthProvider; } -export function createAuthFromSettings() { - const {auth} = getSettings(); +export function createAuthorizer({auth}) { if (auth) { - switch (Object.keys(auth)[0]) { - case ProxyAuthKey: - return createProxyAuth(auth[ProxyAuthKey]); - default: - return NullAuth; - } + getProviderByKey(Object.keys(auth)[0]).create(auth[ProxyAuthKey]); } - return NullAuth + return NullAuthProvider.create(); +} + +export async function fetchWithAuth(key, context) { + return getProviderByKey(context.provider).fetch([key, context]); } 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') +export const filterAllowedWidgets = (perms, widgets) => { + return widgets.filter((widget) => authItemFilter(perms, widget.options) ) +} 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) + })).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); + 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/null.js b/src/utils/auth/null.js new file mode 100644 index 00000000..f1656823 --- /dev/null +++ b/src/utils/auth/null.js @@ -0,0 +1,21 @@ +const NullPermissions = { user: null, groups:[]} +const NullAuthKey = "none" + +function createNullAuth() { + return { + authorize: (request) => NullPermissions, + getContext: (request) => { return { + provider: NullAuthKey + } }, + } +} + +async function fetchNullAuth([key, context]) { + return fetch(key).then((res) => res.json()) +} + +export const NullAuthProvider = { + key: NullAuthKey, + create: createNullAuth, + fetch: fetchNullAuth +} diff --git a/src/utils/auth/proxy.js b/src/utils/auth/proxy.js index 17c51328..7e824cb0 100644 --- a/src/utils/auth/proxy.js +++ b/src/utils/auth/proxy.js @@ -1,42 +1,35 @@ // 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" - +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") - } +function createProxyAuth({groupHeader, userHeader}) { 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()) - } + getContext : (request) => { + return { + provider: ProxyAuthKey, + headers: { + ...userHeader && {[userHeader]: request.headers.get(userHeader) }, + ...groupHeader && {[groupHeader]: request.headers.get(groupHeader)} + } + } + }, + authorize : (request) => getProxyPermissions(userHeader, groupHeader, request) } +} + +async function fetchProxyAuth([key, context]) { + return fetch(key, {headers: context.headers}).then((res) => res.json()) +} + +export const ProxyAuthProvider = { + key: ProxyAuthKey, + create: createProxyAuth, + fetch: fetchProxyAuth } \ No newline at end of file diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index bd86670e..62937e8a 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -83,7 +83,7 @@ export async function widgetsResponse(perms) { let configuredWidgets; try { - configuredWidgets = filterAllowedWidgets(perms, cleanWidgetGroups(await widgetsFromConfig())); + configuredWidgets = filterAllowedWidgets(perms, await 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); From 37f25a2ceb8ba1bb1e7ed7e743e723725d4f1862 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Tue, 7 Nov 2023 08:32:36 -0500 Subject: [PATCH 3/8] Fix allow logic --- src/utils/auth/auth-helpers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/utils/auth/auth-helpers.js b/src/utils/auth/auth-helpers.js index a6c2ccbb..e04b5365 100644 --- a/src/utils/auth/auth-helpers.js +++ b/src/utils/auth/auth-helpers.js @@ -31,13 +31,14 @@ 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); + })).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); + const groupAllow = (('allowGroups' in item)) && groups.some(group => item.allowGroups.includes(group)); + const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user); + const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item)); - return userAllow || groupAllow; + return userAllow || groupAllow || allowAll; } From 1c6d13b34bd8e559d0eabcebb8da22e6284a25de Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Sat, 18 Nov 2023 23:18:05 -0500 Subject: [PATCH 4/8] Add group authentication and documentation --- README.md | 5 +++++ docs/configs/settings.md | 36 +++++++++++++++++++++++++++++++ src/pages/api/auth.js | 17 +++++++++++++++ src/pages/api/bookmarks.js | 6 +++--- src/pages/api/services/index.js | 12 +++-------- src/pages/api/widgets/index.js | 7 +++--- src/pages/index.jsx | 12 +++++------ src/utils/auth/auth-helpers.js | 37 +++++++++++++++++++++----------- src/utils/auth/proxy.js | 12 +++++------ src/utils/config/api-response.js | 13 ++++++----- 10 files changed, 109 insertions(+), 48 deletions(-) create mode 100644 src/pages/api/auth.js diff --git a/README.md b/README.md index 7fc05672..b80dd71b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@

+!!! Note: + +This is a custom fork of the original homepage that integrates some per-user and per-group configuration settings +via proxy auth. Use at your own risk. +

A modern, fully static, fast, secure fully proxied, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages. Easily configured via YAML files or through docker label discovery.

diff --git a/docs/configs/settings.md b/docs/configs/settings.md index 3bc843c1..29e3537e 100644 --- a/docs/configs/settings.md +++ b/docs/configs/settings.md @@ -423,3 +423,39 @@ or per service widget (`services.yaml`) with: ``` If either value is set to true, the error message will be hidden. + +## Authentication + +Basic auth integration is implemeted via an `auth` section. An auth provider can be configured using the `provider` section with the given type. Currently the only provider supported is `proxy`, where the users identification and group membership are passed via HTTP Request headers (in plaintext). The expectation is that the application will be accessed only via an authenticating proxy (i.e treafik ). + +The group and user headers are both configurable like so: +```yaml +auth: + provider: + type: proxy + groupHeader: "X-group-header" + userHeader: "X-user-header" +``` + +Auth can be configured on the service, bookmark, and widget level using the `allowUsers` and `allowGroups` list. + +```yaml +- Example Servie: + allowGroups: + - Group1 + - Group2 + - Group3 + allowUsers: + - User1 + - User2 + - User3 +``` + +Auth for groups can be set in the `groups` under `auth`. + +```yaml +auth: + groups: + My Service Group: + allowGroups: ['Group1', 'Group2'] +``` \ No newline at end of file diff --git a/src/pages/api/auth.js b/src/pages/api/auth.js new file mode 100644 index 00000000..6d0d6033 --- /dev/null +++ b/src/pages/api/auth.js @@ -0,0 +1,17 @@ +import { checkAllowedGroup, readAuthSettings } from "utils/auth/auth-helpers"; +import { getSettings } from "utils/config/config"; + +export default async function handler(req, res) { + const { group } = req.query; + const { provider, groups } = readAuthSettings(getSettings().auth) + + try { + if (checkAllowedGroup(provider.permissions(req), groups, group)) { + res.json({ group: group}) + } else { + res.status(401).json({message:"Group unathorized"}) + } + } catch (err) { + res.status(500).send("Error authenticating"); + } +} diff --git a/src/pages/api/bookmarks.js b/src/pages/api/bookmarks.js index a7b0ae17..d8c4e456 100644 --- a/src/pages/api/bookmarks.js +++ b/src/pages/api/bookmarks.js @@ -1,8 +1,8 @@ -import { createAuthProvider } from "utils/auth/auth-helpers"; +import { readAuthSettings } from "utils/auth/auth-helpers"; import { bookmarksResponse } from "utils/config/api-response"; import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const auth = createAuthProvider(getSettings()) - res.send(await bookmarksResponse(auth.permissions(req))); + const { provider, groups } = readAuthSettings(getSettings().auth) + res.send(await bookmarksResponse(provider.permissions(req), groups)); } diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js index 749ee420..77573ead 100644 --- a/src/pages/api/services/index.js +++ b/src/pages/api/services/index.js @@ -1,14 +1,8 @@ -import { createAuthProvider } from "utils/auth/auth-helpers"; +import { readAuthSettings } from "utils/auth/auth-helpers"; import { servicesResponse } from "utils/config/api-response"; import { getSettings } from "utils/config/config"; -import createLogger from "utils/logger"; - -let logger = createLogger("services_index") export default async function handler(req, res) { - logger.log("Call services"); - const auth = createAuthProvider(getSettings) - const result = await servicesResponse(auth.permissions(req)) - logger.log(result); - res.send(result); + const { provider, groups } = readAuthSettings(getSettings().auth) + res.send(await servicesResponse(provider.permissions(req), groups)); } diff --git a/src/pages/api/widgets/index.js b/src/pages/api/widgets/index.js index beb642f3..a0aa3d1c 100644 --- a/src/pages/api/widgets/index.js +++ b/src/pages/api/widgets/index.js @@ -1,9 +1,8 @@ -import { createAuthProvider } from "utils/auth/auth-helpers"; +import { readAuthSettings } from "utils/auth/auth-helpers"; import { widgetsResponse } from "utils/config/api-response"; import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const auth = createAuthProvider(getSettings()); - - res.send(await widgetsResponse(auth.permissions(req))); + const { provider } = readAuthSettings(getSettings().auth) + res.send(await widgetsResponse(provider.permissions(req))); } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 6701b3a8..55724010 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -27,7 +27,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 { createAuthorizer, fetchWithAuth } from "utils/auth/auth-helpers"; +import { fetchWithAuth, readAuthSettings } from "utils/auth/auth-helpers"; import { NullAuthProvider } from "utils/auth/null"; const ThemeToggle = dynamic(() => import("components/toggles/theme"), { ssr: false, @@ -48,12 +48,12 @@ export async function getServerSideProps({req}) { try { logger = createLogger("index"); const { providers, auth, ...settings } = getSettings(); - const authProvider = createAuthorizer({auth: auth}); + const { provider, groups } = readAuthSettings(auth); - const services = await servicesResponse(authProvider.authorize(req)); - const bookmarks = await bookmarksResponse(authProvider.authorize(req)); - const widgets = await widgetsResponse(authProvider.authorize(req)); - const authContext = authProvider.getContext(req); + const services = await servicesResponse(provider.authorize(req), groups); + const bookmarks = await bookmarksResponse(provider.authorize(req), groups); + const widgets = await widgetsResponse(provider.authorize(req)); + const authContext = provider.getContext(req); return { props: { diff --git a/src/utils/auth/auth-helpers.js b/src/utils/auth/auth-helpers.js index e04b5365..49b9e494 100644 --- a/src/utils/auth/auth-helpers.js +++ b/src/utils/auth/auth-helpers.js @@ -10,31 +10,44 @@ function getProviderByKey(key) { return AuthProviders.find((provider) => provider.key == key) ?? NullAuthProvider; } -export function createAuthorizer({auth}) { - if (auth) { - getProviderByKey(Object.keys(auth)[0]).create(auth[ProxyAuthKey]); - } - return NullAuthProvider.create(); +export function readAuthSettings({provider, groups} = {}) { + return { + provider: provider ? getProviderByKey(provider.type).create(provider) : NullAuthProvider.create(), + groups: groups ? groups.map((group) => ({ + name: Object.keys(group)[0], + allowUsers: group[Object.keys(group)[0]].allowUsers, + allowGroups: group[Object.keys(group)[0]].allowGroups + })) : [] + } } export async function fetchWithAuth(key, context) { return getProviderByKey(context.provider).fetch([key, context]); } -export const filterAllowedServices = (perms, services) => filterAllowedItems(perms, services, 'services'); -export const filterAllowedBookmarks = (perms, bookmarks) => filterAllowedItems(perms, bookmarks, 'bookmarks'); +export function checkAllowedGroup(perms, authGroups, groupName) { + testGroup = authGroups.find((group) => group.name == groupName ) + return testGroup ? authAllow(perms, testGroup) : true +} + +export const filterAllowedServices = (perms, authGroups, services) => filterAllowedItems(perms, authGroups, services, 'services'); +export const filterAllowedBookmarks = (perms, authGroups, bookmarks) => filterAllowedItems(perms, authGroups, bookmarks, 'bookmarks'); export const filterAllowedWidgets = (perms, widgets) => { return widgets.filter((widget) => authItemFilter(perms, widget.options) ) } -function filterAllowedItems({user, groups}, itemGroups, groupKey) { - return itemGroups.map((group) => ({ +function filterAllowedItems(perms, authGroups, groups, groupKey) { + return groups.filter((group) => checkAllowedGroup(perms, authGroups, group.name)) + .map((group) => ({ name: group.name, - [groupKey]: group[groupKey].filter((item) => authItemFilter({user, groups}, item)) - })).filter((group) => group[groupKey].length); + [groupKey]: group[groupKey].filter((item) => authAllow(perms, item)) + })) + .filter((group) => group[groupKey].length); } -function authItemFilter({user, groups}, item) { + + +function authAllow({user, groups}, item) { const groupAllow = (('allowGroups' in item)) && groups.some(group => item.allowGroups.includes(group)); const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user); const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item)); diff --git a/src/utils/auth/proxy.js b/src/utils/auth/proxy.js index 7e824cb0..3369e62f 100644 --- a/src/utils/auth/proxy.js +++ b/src/utils/auth/proxy.js @@ -1,6 +1,6 @@ -// Proxy auth is meant to be used by a reverse proxy that injects permission headers into the origin +// 'proxy' auth provider 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. -const ProxyAuthKey="proxy_auth" +const ProxyAuthKey="proxy" function getProxyPermissions(userHeader, groupHeader, request) { const user = (userHeader)?request.headers.get(userHeader):None; @@ -13,11 +13,9 @@ function createProxyAuth({groupHeader, userHeader}) { return { getContext : (request) => { return { - provider: ProxyAuthKey, - headers: { - ...userHeader && {[userHeader]: request.headers.get(userHeader) }, - ...groupHeader && {[groupHeader]: request.headers.get(groupHeader)} - } + type: ProxyAuthKey, + ...userHeader && {[userHeader]: request.headers.get(userHeader) }, + ...groupHeader && {[groupHeader]: request.headers.get(groupHeader)} } }, authorize : (request) => getProxyPermissions(userHeader, groupHeader, request) diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 62937e8a..280a8268 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -12,7 +12,6 @@ import { servicesFromKubernetes, } from "utils/config/service-helpers"; import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers"; -import { filterAuthBookmarks } from "utils/auth/auth-helpers"; import { filterAllowedBookmarks, @@ -31,7 +30,7 @@ function compareServices(service1, service2) { return service1.name.localeCompare(service2.name); } -export async function bookmarksResponse(perms) { +export async function bookmarksResponse(perms, authGroups) { checkAndCopyConfig("bookmarks.yaml"); const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml"); @@ -52,7 +51,7 @@ export async function bookmarksResponse(perms) { } // map easy to write YAML objects into easy to consume JS arrays - const bookmarksArray = filterAllowedBookmarks(perms, + const bookmarksArray = filterAllowedBookmarks(perms, authGroups, bookmarks.map((group) => ({ name: Object.keys(group)[0], bookmarks: group[Object.keys(group)[0]].map((entries) => ({ @@ -93,14 +92,14 @@ export async function widgetsResponse(perms) { return configuredWidgets; } -export async function servicesResponse(perms) { +export async function servicesResponse(perms, authGroups) { let discoveredDockerServices; let discoveredKubernetesServices; let configuredServices; let initialSettings; try { - discoveredDockerServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromDocker())); + discoveredDockerServices = filterAllowedServices(perms, authGroups, cleanServiceGroups(await servicesFromDocker())); if (discoveredDockerServices?.length === 0) { console.debug("No containers were found with homepage labels."); } @@ -111,7 +110,7 @@ export async function servicesResponse(perms) { } try { - discoveredKubernetesServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromKubernetes())); + discoveredKubernetesServices = filterAllowedServices(perms, authGroups, 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()); @@ -119,7 +118,7 @@ export async function servicesResponse(perms) { } try { - configuredServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromConfig())); + configuredServices = filterAllowedServices(perms, authGroups, cleanServiceGroups(await servicesFromConfig())); } catch (e) { console.error("Failed to load services.yaml, please check for errors"); if (e) console.error(e.toString()); From aecb22d7d55b94896da444690be9a25bc345bb1e Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Wed, 22 Nov 2023 16:18:49 -0500 Subject: [PATCH 5/8] Fix eslint errors and update documentation --- docs/configs/settings.md | 7 ++-- src/pages/api/auth.js | 2 +- src/pages/index.jsx | 21 ++++++------ src/utils/auth/auth-helpers.js | 58 +++++++++++++++----------------- src/utils/auth/null.js | 12 ++++--- src/utils/auth/proxy.js | 16 ++++----- src/utils/config/api-response.js | 1 - 7 files changed, 59 insertions(+), 58 deletions(-) diff --git a/docs/configs/settings.md b/docs/configs/settings.md index 29e3537e..80a16447 100644 --- a/docs/configs/settings.md +++ b/docs/configs/settings.md @@ -451,11 +451,14 @@ Auth can be configured on the service, bookmark, and widget level using the `all - User3 ``` -Auth for groups can be set in the `groups` under `auth`. +Auth for groups can be set in the `groups` under `auth`. In general the `groups` tag follows the format of the `layout` +section. For example: ```yaml auth: groups: - My Service Group: + My Service Group: allowGroups: ['Group1', 'Group2'] + My Other Group: + allowGroups: ['Group1'] ``` \ No newline at end of file diff --git a/src/pages/api/auth.js b/src/pages/api/auth.js index 6d0d6033..bdc547db 100644 --- a/src/pages/api/auth.js +++ b/src/pages/api/auth.js @@ -7,7 +7,7 @@ export default async function handler(req, res) { try { if (checkAllowedGroup(provider.permissions(req), groups, group)) { - res.json({ group: group}) + res.json({group}) } else { res.status(401).json({message:"Group unathorized"}) } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 55724010..6721d6af 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,5 +1,5 @@ /* eslint-disable react/no-array-index-key */ -import useSWR, { unstable_serialize, SWRConfig } from "swr"; +import useSWR, { unstable_serialize as unstableSerialize, SWRConfig } from "swr"; import Head from "next/head"; import dynamic from "next/dynamic"; import classNames from "classnames"; @@ -9,6 +9,7 @@ import { BiError } from "react-icons/bi"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import { useRouter } from "next/router"; +import NullAuthProvider from "utils/auth/null"; import Tab, { slugify } from "components/tab"; import FileContent from "components/filecontent"; import ServicesGroup from "components/services/group"; @@ -28,7 +29,7 @@ import themes from "utils/styles/themes"; import QuickLaunch from "components/quicklaunch"; import { getStoredProvider, searchProviders } from "components/widgets/search/search"; import { fetchWithAuth, readAuthSettings } from "utils/auth/auth-helpers"; -import { NullAuthProvider } from "utils/auth/null"; + const ThemeToggle = dynamic(() => import("components/toggles/theme"), { ssr: false, }); @@ -59,12 +60,12 @@ export async function getServerSideProps({req}) { props: { initialSettings: settings, fallback: { - [unstable_serialize(["/api/services", authContext])]: services, - [unstable_serialize(["/api/bookmarks", authContext])]: bookmarks, - [unstable_serialize(["/api/widgets", authContext])]: widgets, + [unstableSerialize(["/api/services", authContext])]: services, + [unstableSerialize(["/api/bookmarks", authContext])]: bookmarks, + [unstableSerialize(["/api/widgets", authContext])]: widgets, "/api/hash": false, }, - authContext: authContext, + authContext, ...(await serverSideTranslations(settings.language ?? "en")), }, }; @@ -77,12 +78,12 @@ export async function getServerSideProps({req}) { props: { initialSettings: {}, fallback: { - [unstable_serialize(["/api/services", authContext])]: [], - [unstable_serialize(["/api/bookmarks", authContext])]: [], - [unstable_serialize(["/api/widgets", authContext])]: [], + [unstableSerialize(["/api/services", authContext])]: [], + [unstableSerialize(["/api/bookmarks", authContext])]: [], + [unstableSerialize(["/api/widgets", authContext])]: [], "/api/hash": false, }, - authContext: authContext, + authContext, ...(await serverSideTranslations("en")), }, }; diff --git a/src/utils/auth/auth-helpers.js b/src/utils/auth/auth-helpers.js index 49b9e494..38d86708 100644 --- a/src/utils/auth/auth-helpers.js +++ b/src/utils/auth/auth-helpers.js @@ -1,5 +1,5 @@ -import { ProxyAuthProvider} from "./proxy"; -import { NullAuthProvider} from "./null"; +import ProxyAuthProvider from "./proxy"; +import NullAuthProvider from "./null"; const AuthProviders = { NullAuthProvider, @@ -7,7 +7,30 @@ const AuthProviders = { }; function getProviderByKey(key) { - return AuthProviders.find((provider) => provider.key == key) ?? NullAuthProvider; + return AuthProviders.find((provider) => provider.key === key) ?? NullAuthProvider; +} + +function authAllow({user, groups}, item) { + const groupAllow = (('allowGroups' in item)) && groups.some(group => item.allowGroups.includes(group)); + const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user); + const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item)); + + return userAllow || groupAllow || allowAll; +} + +export function checkAllowedGroup(perms, authGroups, groupName) { + const testGroup = authGroups.find((group) => group.name === groupName ) + return testGroup ? authAllow(perms, testGroup) : true +} + + +function filterAllowedItems(perms, authGroups, groups, groupKey) { + return groups.filter((group) => checkAllowedGroup(perms, authGroups, group.name)) + .map((group) => ({ + name: group.name, + [groupKey]: group[groupKey].filter((item) => authAllow(perms, item)) + })) + .filter((group) => group[groupKey].length); } export function readAuthSettings({provider, groups} = {}) { @@ -25,33 +48,6 @@ export async function fetchWithAuth(key, context) { return getProviderByKey(context.provider).fetch([key, context]); } -export function checkAllowedGroup(perms, authGroups, groupName) { - testGroup = authGroups.find((group) => group.name == groupName ) - return testGroup ? authAllow(perms, testGroup) : true -} - export const filterAllowedServices = (perms, authGroups, services) => filterAllowedItems(perms, authGroups, services, 'services'); export const filterAllowedBookmarks = (perms, authGroups, bookmarks) => filterAllowedItems(perms, authGroups, bookmarks, 'bookmarks'); -export const filterAllowedWidgets = (perms, widgets) => { - return widgets.filter((widget) => authItemFilter(perms, widget.options) ) -} - -function filterAllowedItems(perms, authGroups, groups, groupKey) { - return groups.filter((group) => checkAllowedGroup(perms, authGroups, group.name)) - .map((group) => ({ - name: group.name, - [groupKey]: group[groupKey].filter((item) => authAllow(perms, item)) - })) - .filter((group) => group[groupKey].length); -} - - - -function authAllow({user, groups}, item) { - const groupAllow = (('allowGroups' in item)) && groups.some(group => item.allowGroups.includes(group)); - const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user); - const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item)); - - return userAllow || groupAllow || allowAll; -} - +export const filterAllowedWidgets = (perms, widgets) => widgets.filter((widget) => authAllow(perms, widget.options)) \ No newline at end of file diff --git a/src/utils/auth/null.js b/src/utils/auth/null.js index f1656823..8b16bd3c 100644 --- a/src/utils/auth/null.js +++ b/src/utils/auth/null.js @@ -3,19 +3,21 @@ const NullAuthKey = "none" function createNullAuth() { return { - authorize: (request) => NullPermissions, - getContext: (request) => { return { + authorize: () => NullPermissions, + getContext: () => ({ provider: NullAuthKey - } }, + }), } } -async function fetchNullAuth([key, context]) { +async function fetchNullAuth([key]) { return fetch(key).then((res) => res.json()) } -export const NullAuthProvider = { +const NullAuthProvider = { key: NullAuthKey, create: createNullAuth, fetch: fetchNullAuth } + +export default NullAuthProvider; diff --git a/src/utils/auth/proxy.js b/src/utils/auth/proxy.js index 3369e62f..cb7e9355 100644 --- a/src/utils/auth/proxy.js +++ b/src/utils/auth/proxy.js @@ -3,21 +3,19 @@ const ProxyAuthKey="proxy" function getProxyPermissions(userHeader, groupHeader, request) { - const user = (userHeader)?request.headers.get(userHeader):None; + const user = (userHeader)?request.headers.get(userHeader):null; const groupsString = (groupHeader)?request.headers.get(groupHeader):""; - return {user: user, groups: (groupsString)?groupsString.split(",").map((v) => v.trimStart()):[]} + return {user, groups: (groupsString)?groupsString.split(",").map((v) => v.trimStart()):[]} } function createProxyAuth({groupHeader, userHeader}) { return { - getContext : (request) => { - return { + getContext : (request) => ({ type: ProxyAuthKey, ...userHeader && {[userHeader]: request.headers.get(userHeader) }, ...groupHeader && {[groupHeader]: request.headers.get(groupHeader)} - } - }, + }), authorize : (request) => getProxyPermissions(userHeader, groupHeader, request) } } @@ -26,8 +24,10 @@ async function fetchProxyAuth([key, context]) { return fetch(key, {headers: context.headers}).then((res) => res.json()) } -export const ProxyAuthProvider = { +const ProxyAuthProvider = { key: ProxyAuthKey, create: createProxyAuth, fetch: fetchProxyAuth -} \ No newline at end of file +} + +export default ProxyAuthProvider; \ No newline at end of file diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 280a8268..311cda9a 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -12,7 +12,6 @@ import { servicesFromKubernetes, } from "utils/config/service-helpers"; import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers"; - import { filterAllowedBookmarks, filterAllowedServices, From c593fa7487349c583c8e681e10b100e124642e15 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Sat, 9 Dec 2023 10:18:56 -0500 Subject: [PATCH 6/8] Update README.md Remove custom fork note --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index b80dd71b..7fc05672 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,6 @@

-!!! Note: - -This is a custom fork of the original homepage that integrates some per-user and per-group configuration settings -via proxy auth. Use at your own risk. -

A modern, fully static, fast, secure fully proxied, highly customizable application dashboard with integrations for over 100 services and translations into multiple languages. Easily configured via YAML files or through docker label discovery.

From 44774c60223903077e318aa4d41cce9819d2cd17 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Sat, 9 Dec 2023 10:19:48 -0500 Subject: [PATCH 7/8] Delete environment.yml Accidently committed conda environment --- environment.yml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 environment.yml diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 6d271a9f..00000000 --- a/environment.yml +++ /dev/null @@ -1,14 +0,0 @@ -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 From 0fed2f42672c44e38ed24be7c61e919776428a89 Mon Sep 17 00:00:00 2001 From: Aaron Dalton Date: Mon, 25 Dec 2023 12:05:54 -0500 Subject: [PATCH 8/8] Fix precommit isssues --- docs/configs/settings.md | 47 ++++++++++++------------ src/pages/api/auth.js | 20 +++++------ src/pages/api/bookmarks.js | 2 +- src/pages/api/services/index.js | 2 +- src/pages/api/widgets/index.js | 2 +- src/pages/index.jsx | 12 +++---- src/utils/auth/auth-helpers.js | 62 +++++++++++++++++--------------- src/utils/auth/null.js | 28 +++++++-------- src/utils/auth/proxy.js | 46 ++++++++++++------------ src/utils/config/api-response.js | 20 ++++++----- 10 files changed, 124 insertions(+), 117 deletions(-) diff --git a/docs/configs/settings.md b/docs/configs/settings.md index 6fbee81b..2ace2c34 100644 --- a/docs/configs/settings.md +++ b/docs/configs/settings.md @@ -456,41 +456,42 @@ or per service widget (`services.yaml`) with: If either value is set to true, the error message will be hidden. -## Authentication +## Authentication -Basic auth integration is implemeted via an `auth` section. An auth provider can be configured using the `provider` section with the given type. Currently the only provider supported is `proxy`, where the users identification and group membership are passed via HTTP Request headers (in plaintext). The expectation is that the application will be accessed only via an authenticating proxy (i.e treafik ). +Basic auth integration is implemeted via an `auth` section. An auth provider can be configured using the `provider` section with the given type. Currently the only provider supported is `proxy`, where the users identification and group membership are passed via HTTP Request headers (in plaintext). The expectation is that the application will be accessed only via an authenticating proxy (i.e treafik ). -The group and user headers are both configurable like so: -```yaml -auth: - provider: - type: proxy - groupHeader: "X-group-header" - userHeader: "X-user-header" -``` - -Auth can be configured on the service, bookmark, and widget level using the `allowUsers` and `allowGroups` list. +The group and user headers are both configurable like so: ```yaml -- Example Servie: - allowGroups: +auth: + provider: + type: proxy + groupHeader: "X-group-header" + userHeader: "X-user-header" +``` + +Auth can be configured on the service, bookmark, and widget level using the `allowUsers` and `allowGroups` list. + +```yaml +- Example Servie: + allowGroups: - Group1 - Group2 - Group3 - allowUsers: + allowUsers: - User1 - User2 - User3 ``` -Auth for groups can be set in the `groups` under `auth`. In general the `groups` tag follows the format of the `layout` -section. For example: +Auth for groups can be set in the `groups` under `auth`. In general the `groups` tag follows the format of the `layout` +section. For example: -```yaml -auth: - groups: +```yaml +auth: + groups: My Service Group: - allowGroups: ['Group1', 'Group2'] + allowGroups: ["Group1", "Group2"] My Other Group: - allowGroups: ['Group1'] -``` \ No newline at end of file + allowGroups: ["Group1"] +``` diff --git a/src/pages/api/auth.js b/src/pages/api/auth.js index bdc547db..5c876e56 100644 --- a/src/pages/api/auth.js +++ b/src/pages/api/auth.js @@ -2,16 +2,16 @@ import { checkAllowedGroup, readAuthSettings } from "utils/auth/auth-helpers"; import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const { group } = req.query; - const { provider, groups } = readAuthSettings(getSettings().auth) + const { group } = req.query; + const { provider, groups } = readAuthSettings(getSettings().auth); - try { - if (checkAllowedGroup(provider.permissions(req), groups, group)) { - res.json({group}) - } else { - res.status(401).json({message:"Group unathorized"}) - } - } catch (err) { - res.status(500).send("Error authenticating"); + try { + if (checkAllowedGroup(provider.permissions(req), groups, group)) { + res.json({ group }); + } else { + res.status(401).json({ message: "Group unathorized" }); } + } catch (err) { + res.status(500).send("Error authenticating"); + } } diff --git a/src/pages/api/bookmarks.js b/src/pages/api/bookmarks.js index d8c4e456..99699495 100644 --- a/src/pages/api/bookmarks.js +++ b/src/pages/api/bookmarks.js @@ -3,6 +3,6 @@ import { bookmarksResponse } from "utils/config/api-response"; import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const { provider, groups } = readAuthSettings(getSettings().auth) + const { provider, groups } = readAuthSettings(getSettings().auth); res.send(await bookmarksResponse(provider.permissions(req), groups)); } diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js index 77573ead..88925b4d 100644 --- a/src/pages/api/services/index.js +++ b/src/pages/api/services/index.js @@ -3,6 +3,6 @@ import { servicesResponse } from "utils/config/api-response"; import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const { provider, groups } = readAuthSettings(getSettings().auth) + const { provider, groups } = readAuthSettings(getSettings().auth); res.send(await servicesResponse(provider.permissions(req), groups)); } diff --git a/src/pages/api/widgets/index.js b/src/pages/api/widgets/index.js index a0aa3d1c..10d726d9 100644 --- a/src/pages/api/widgets/index.js +++ b/src/pages/api/widgets/index.js @@ -3,6 +3,6 @@ import { widgetsResponse } from "utils/config/api-response"; import { getSettings } from "utils/config/config"; export default async function handler(req, res) { - const { provider } = readAuthSettings(getSettings().auth) + const { provider } = readAuthSettings(getSettings().auth); res.send(await widgetsResponse(provider.permissions(req))); } diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 61f54a94..c874ce49 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -44,17 +44,17 @@ const Version = dynamic(() => import("components/version"), { const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "openmeteo", "search", "datetime"]; -export async function getServerSideProps({req}) { +export async function getServerSideProps({ req }) { let logger; try { logger = createLogger("index"); const { providers, auth, ...settings } = getSettings(); - const { provider, groups } = readAuthSettings(auth); + const { provider, groups } = readAuthSettings(auth); const services = await servicesResponse(provider.authorize(req), groups); const bookmarks = await bookmarksResponse(provider.authorize(req), groups); const widgets = await widgetsResponse(provider.authorize(req)); - const authContext = provider.getContext(req); + const authContext = provider.getContext(req); return { props: { @@ -160,7 +160,7 @@ function Index({ initialSettings, fallback, authContext }) { return ( fetch(resource, init).then((res) => res.json()) }}> - + ); @@ -187,7 +187,7 @@ function Home({ initialSettings, authContext }) { }, [initialSettings, setSettings]); const { data: services } = useSWR(["/api/services", authContext], fetchWithAuth); - const { data: bookmarks } = useSWR(["/api/bookmarks", authContext], fetchWithAuth); + const { data: bookmarks } = useSWR(["/api/bookmarks", authContext], fetchWithAuth); const { data: widgets } = useSWR(["/api/widgets", authContext], fetchWithAuth); const servicesAndBookmarks = [ @@ -530,7 +530,7 @@ export default function Wrapper({ initialSettings, fallback, authContext }) { backgroundBrightness && `backdrop-brightness-${initialSettings.background.brightness}`, )} > - + diff --git a/src/utils/auth/auth-helpers.js b/src/utils/auth/auth-helpers.js index 38d86708..50f62db2 100644 --- a/src/utils/auth/auth-helpers.js +++ b/src/utils/auth/auth-helpers.js @@ -1,53 +1,57 @@ import ProxyAuthProvider from "./proxy"; -import NullAuthProvider from "./null"; +import NullAuthProvider from "./null"; const AuthProviders = { - NullAuthProvider, - ProxyAuthProvider -}; + NullAuthProvider, + ProxyAuthProvider, +}; function getProviderByKey(key) { - return AuthProviders.find((provider) => provider.key === key) ?? NullAuthProvider; + return AuthProviders.find((provider) => provider.key === key) ?? NullAuthProvider; } -function authAllow({user, groups}, item) { - const groupAllow = (('allowGroups' in item)) && groups.some(group => item.allowGroups.includes(group)); - const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user); - const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item)); +function authAllow({ user, groups }, item) { + const groupAllow = "allowGroups" in item && groups.some((group) => item.allowGroups.includes(group)); + const userAllow = "allowUsers" in item && item.allowUsers.includes(user); + const allowAll = !("allowGroups" in item) && !("allowUsers" in item); - return userAllow || groupAllow || allowAll; + return userAllow || groupAllow || allowAll; } export function checkAllowedGroup(perms, authGroups, groupName) { - const testGroup = authGroups.find((group) => group.name === groupName ) - return testGroup ? authAllow(perms, testGroup) : true + const testGroup = authGroups.find((group) => group.name === groupName); + return testGroup ? authAllow(perms, testGroup) : true; } - function filterAllowedItems(perms, authGroups, groups, groupKey) { - return groups.filter((group) => checkAllowedGroup(perms, authGroups, group.name)) + return groups + .filter((group) => checkAllowedGroup(perms, authGroups, group.name)) .map((group) => ({ - name: group.name, - [groupKey]: group[groupKey].filter((item) => authAllow(perms, item)) + name: group.name, + [groupKey]: group[groupKey].filter((item) => authAllow(perms, item)), })) .filter((group) => group[groupKey].length); } -export function readAuthSettings({provider, groups} = {}) { - return { - provider: provider ? getProviderByKey(provider.type).create(provider) : NullAuthProvider.create(), - groups: groups ? groups.map((group) => ({ - name: Object.keys(group)[0], - allowUsers: group[Object.keys(group)[0]].allowUsers, - allowGroups: group[Object.keys(group)[0]].allowGroups - })) : [] - } +export function readAuthSettings({ provider, groups } = {}) { + return { + provider: provider ? getProviderByKey(provider.type).create(provider) : NullAuthProvider.create(), + groups: groups + ? groups.map((group) => ({ + name: Object.keys(group)[0], + allowUsers: group[Object.keys(group)[0]].allowUsers, + allowGroups: group[Object.keys(group)[0]].allowGroups, + })) + : [], + }; } export async function fetchWithAuth(key, context) { - return getProviderByKey(context.provider).fetch([key, context]); + return getProviderByKey(context.provider).fetch([key, context]); } -export const filterAllowedServices = (perms, authGroups, services) => filterAllowedItems(perms, authGroups, services, 'services'); -export const filterAllowedBookmarks = (perms, authGroups, bookmarks) => filterAllowedItems(perms, authGroups, bookmarks, 'bookmarks'); -export const filterAllowedWidgets = (perms, widgets) => widgets.filter((widget) => authAllow(perms, widget.options)) \ No newline at end of file +export const filterAllowedServices = (perms, authGroups, services) => + filterAllowedItems(perms, authGroups, services, "services"); +export const filterAllowedBookmarks = (perms, authGroups, bookmarks) => + filterAllowedItems(perms, authGroups, bookmarks, "bookmarks"); +export const filterAllowedWidgets = (perms, widgets) => widgets.filter((widget) => authAllow(perms, widget.options)); diff --git a/src/utils/auth/null.js b/src/utils/auth/null.js index 8b16bd3c..686e5e0a 100644 --- a/src/utils/auth/null.js +++ b/src/utils/auth/null.js @@ -1,23 +1,23 @@ -const NullPermissions = { user: null, groups:[]} -const NullAuthKey = "none" +const NullPermissions = { user: null, groups: [] }; +const NullAuthKey = "none"; function createNullAuth() { - return { - authorize: () => NullPermissions, - getContext: () => ({ - provider: NullAuthKey - }), - } -} + return { + authorize: () => NullPermissions, + getContext: () => ({ + provider: NullAuthKey, + }), + }; +} async function fetchNullAuth([key]) { - return fetch(key).then((res) => res.json()) + return fetch(key).then((res) => res.json()); } const NullAuthProvider = { - key: NullAuthKey, - create: createNullAuth, - fetch: fetchNullAuth -} + key: NullAuthKey, + create: createNullAuth, + fetch: fetchNullAuth, +}; export default NullAuthProvider; diff --git a/src/utils/auth/proxy.js b/src/utils/auth/proxy.js index cb7e9355..384e17f8 100644 --- a/src/utils/auth/proxy.js +++ b/src/utils/auth/proxy.js @@ -1,33 +1,33 @@ -// 'proxy' auth provider 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. -const ProxyAuthKey="proxy" +// 'proxy' auth provider 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. +const ProxyAuthKey = "proxy"; -function getProxyPermissions(userHeader, groupHeader, request) { - const user = (userHeader)?request.headers.get(userHeader):null; - const groupsString = (groupHeader)?request.headers.get(groupHeader):""; +function getProxyPermissions(userHeader, groupHeader, request) { + const user = userHeader ? request.headers.get(userHeader) : null; + const groupsString = groupHeader ? request.headers.get(groupHeader) : ""; - return {user, groups: (groupsString)?groupsString.split(",").map((v) => v.trimStart()):[]} + return { user, groups: groupsString ? groupsString.split(",").map((v) => v.trimStart()) : [] }; } -function createProxyAuth({groupHeader, userHeader}) { - return { - getContext : (request) => ({ - type: ProxyAuthKey, - ...userHeader && {[userHeader]: request.headers.get(userHeader) }, - ...groupHeader && {[groupHeader]: request.headers.get(groupHeader)} - }), - authorize : (request) => getProxyPermissions(userHeader, groupHeader, request) - } +function createProxyAuth({ groupHeader, userHeader }) { + return { + getContext: (request) => ({ + type: ProxyAuthKey, + ...(userHeader && { [userHeader]: request.headers.get(userHeader) }), + ...(groupHeader && { [groupHeader]: request.headers.get(groupHeader) }), + }), + authorize: (request) => getProxyPermissions(userHeader, groupHeader, request), + }; } async function fetchProxyAuth([key, context]) { - return fetch(key, {headers: context.headers}).then((res) => res.json()) + return fetch(key, { headers: context.headers }).then((res) => res.json()); } -const ProxyAuthProvider = { - key: ProxyAuthKey, - create: createProxyAuth, - fetch: fetchProxyAuth -} +const ProxyAuthProvider = { + key: ProxyAuthKey, + create: createProxyAuth, + fetch: fetchProxyAuth, +}; -export default ProxyAuthProvider; \ No newline at end of file +export default ProxyAuthProvider; diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js index 311cda9a..58df71f1 100644 --- a/src/utils/config/api-response.js +++ b/src/utils/config/api-response.js @@ -12,11 +12,7 @@ import { servicesFromKubernetes, } from "utils/config/service-helpers"; import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers"; -import { - filterAllowedBookmarks, - filterAllowedServices, - filterAllowedWidgets -} from "utils/auth/auth-helpers"; +import { filterAllowedBookmarks, filterAllowedServices, filterAllowedWidgets } from "utils/auth/auth-helpers"; /** * Compares services by weight then by name. @@ -35,7 +31,7 @@ export async function bookmarksResponse(perms, authGroups) { 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 []; @@ -50,14 +46,16 @@ export async function bookmarksResponse(perms, authGroups) { } // map easy to write YAML objects into easy to consume JS arrays - const bookmarksArray = filterAllowedBookmarks(perms, authGroups, + const bookmarksArray = filterAllowedBookmarks( + perms, + authGroups, 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 = []; @@ -109,7 +107,11 @@ export async function servicesResponse(perms, authGroups) { } try { - discoveredKubernetesServices = filterAllowedServices(perms, authGroups, cleanServiceGroups(await servicesFromKubernetes())); + discoveredKubernetesServices = filterAllowedServices( + perms, + authGroups, + 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());