Initial add auth to homepage

This commit is contained in:
Aaron Dalton 2023-11-05 20:49:56 -05:00
parent 654f16dbb5
commit 2fee5a8db7
8 changed files with 144 additions and 25 deletions

14
environment.yml Normal file
View File

@ -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

View File

@ -1,5 +1,7 @@
import { createAuthFromSettings } from "utils/auth/auth-helpers";
import { bookmarksResponse } from "utils/config/api-response"; import { bookmarksResponse } from "utils/config/api-response";
export default async function handler(req, res) { export default async function handler(req, res) {
res.send(await bookmarksResponse()); const auth = createAuthFromSettings()
res.send(await bookmarksResponse(auth.permissions(req)));
} }

View File

@ -1,5 +1,8 @@
import { createAuthFromSettings } from "utils/auth/auth-helpers";
import { servicesResponse } from "utils/config/api-response"; import { servicesResponse } from "utils/config/api-response";
export default async function handler(req, res) { export default async function handler(req, res) {
res.send(await servicesResponse()); const auth = createAuthFromSettings()
res.send(await servicesResponse(auth.permissions(req)));
} }

View File

@ -1,5 +1,8 @@
import { createAuthFromSettings } from "utils/auth/auth-helpers";
import { widgetsResponse } from "utils/config/api-response"; import { widgetsResponse } from "utils/config/api-response";
export default async function handler(req, res) { export default async function handler(req, res) {
res.send(await widgetsResponse()); const auth = createAuthFromSettings();
res.send(await widgetsResponse(auth.permissions(req)));
} }

View File

@ -1,6 +1,7 @@
/* eslint-disable react/no-array-index-key */ /* eslint-disable react/no-array-index-key */
import useSWR, { SWRConfig } from "swr"; import useSWR, { SWRConfig } from "swr";
import Head from "next/head"; import Head from "next/head";
import {headers} from "next/header";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import classNames from "classnames"; import classNames from "classnames";
import { useTranslation } from "next-i18next"; import { useTranslation } from "next-i18next";
@ -27,6 +28,7 @@ import ErrorBoundary from "components/errorboundry";
import themes from "utils/styles/themes"; import themes from "utils/styles/themes";
import QuickLaunch from "components/quicklaunch"; import QuickLaunch from "components/quicklaunch";
import { getStoredProvider, searchProviders } from "components/widgets/search/search"; import { getStoredProvider, searchProviders } from "components/widgets/search/search";
import { NullPermissions, createAuthFromSettings } from "utils/auth/auth-helpers";
const ThemeToggle = dynamic(() => import("components/toggles/theme"), { const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
ssr: false, ssr: false,
@ -46,11 +48,11 @@ export async function getStaticProps() {
let logger; let logger;
try { try {
logger = createLogger("index"); logger = createLogger("index");
const { providers, ...settings } = getSettings(); const { providers, auth, ...settings } = getSettings();
const services = await servicesResponse(); const services = await servicesResponse(NullPermissions);
const bookmarks = await bookmarksResponse(); const bookmarks = await bookmarksResponse(NullPermissions);
const widgets = await widgetsResponse(); const widgets = await widgetsResponse(NullPermissions);
return { return {
props: { props: {
@ -179,9 +181,11 @@ function Home({ initialSettings }) {
setSettings(initialSettings); setSettings(initialSettings);
}, [initialSettings, setSettings]); }, [initialSettings, setSettings]);
const { data: services } = useSWR("/api/services"); const auth = createAuthFromSettings();
const { data: bookmarks } = useSWR("/api/bookmarks");
const { data: widgets } = useSWR("/api/widgets"); 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 = [ const servicesAndBookmarks = [
...services.map((sg) => sg.services).flat(), ...services.map((sg) => sg.services).flat(),

View File

@ -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;
}

42
src/utils/auth/proxy.js Normal file
View File

@ -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())
}
}
}

View File

@ -12,6 +12,13 @@ import {
servicesFromKubernetes, servicesFromKubernetes,
} from "utils/config/service-helpers"; } from "utils/config/service-helpers";
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-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. * Compares services by weight then by name.
@ -24,7 +31,7 @@ function compareServices(service1, service2) {
return service1.name.localeCompare(service2.name); return service1.name.localeCompare(service2.name);
} }
export async function bookmarksResponse() { export async function bookmarksResponse(perms) {
checkAndCopyConfig("bookmarks.yaml"); checkAndCopyConfig("bookmarks.yaml");
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml"); const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
@ -45,13 +52,15 @@ export async function bookmarksResponse() {
} }
// map easy to write YAML objects into easy to consume JS arrays // map easy to write YAML objects into easy to consume JS arrays
const bookmarksArray = bookmarks.map((group) => ({ const bookmarksArray = filterAllowedBookmarks(perms,
bookmarks.map((group) => ({
name: Object.keys(group)[0], name: Object.keys(group)[0],
bookmarks: group[Object.keys(group)[0]].map((entries) => ({ bookmarks: group[Object.keys(group)[0]].map((entries) => ({
name: Object.keys(entries)[0], name: Object.keys(entries)[0],
...entries[Object.keys(entries)[0]][0], ...entries[Object.keys(entries)[0]][0],
})), })),
})); }))
);
const sortedGroups = []; const sortedGroups = [];
const unsortedGroups = []; const unsortedGroups = [];
@ -70,11 +79,11 @@ export async function bookmarksResponse() {
return [...sortedGroups.filter((g) => g), ...unsortedGroups]; return [...sortedGroups.filter((g) => g), ...unsortedGroups];
} }
export async function widgetsResponse() { export async function widgetsResponse(perms) {
let configuredWidgets; let configuredWidgets;
try { try {
configuredWidgets = cleanWidgetGroups(await widgetsFromConfig()); configuredWidgets = filterAllowedWidgets(perms, cleanWidgetGroups(await widgetsFromConfig()));
} catch (e) { } catch (e) {
console.error("Failed to load widgets, please check widgets.yaml for errors or remove example entries."); console.error("Failed to load widgets, please check widgets.yaml for errors or remove example entries.");
if (e) console.error(e); if (e) console.error(e);
@ -84,14 +93,14 @@ export async function widgetsResponse() {
return configuredWidgets; return configuredWidgets;
} }
export async function servicesResponse() { export async function servicesResponse(perms) {
let discoveredDockerServices; let discoveredDockerServices;
let discoveredKubernetesServices; let discoveredKubernetesServices;
let configuredServices; let configuredServices;
let initialSettings; let initialSettings;
try { try {
discoveredDockerServices = cleanServiceGroups(await servicesFromDocker()); discoveredDockerServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromDocker()));
if (discoveredDockerServices?.length === 0) { if (discoveredDockerServices?.length === 0) {
console.debug("No containers were found with homepage labels."); console.debug("No containers were found with homepage labels.");
} }
@ -102,7 +111,7 @@ export async function servicesResponse() {
} }
try { try {
discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes()); discoveredKubernetesServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromKubernetes()));
} catch (e) { } catch (e) {
console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries."); console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
if (e) console.error(e.toString()); if (e) console.error(e.toString());
@ -110,7 +119,7 @@ export async function servicesResponse() {
} }
try { try {
configuredServices = cleanServiceGroups(await servicesFromConfig()); configuredServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromConfig()));
} catch (e) { } catch (e) {
console.error("Failed to load services.yaml, please check for errors"); console.error("Failed to load services.yaml, please check for errors");
if (e) console.error(e.toString()); if (e) console.error(e.toString());