Add group authentication and documentation
This commit is contained in:
parent
37f25a2ceb
commit
1c6d13b34b
@ -5,6 +5,11 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
!!! 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.
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
A modern, <em>fully static, fast</em>, secure <em>fully proxied</em>, 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.
|
A modern, <em>fully static, fast</em>, secure <em>fully proxied</em>, 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.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -423,3 +423,39 @@ or per service widget (`services.yaml`) with:
|
|||||||
```
|
```
|
||||||
|
|
||||||
If either value is set to true, the error message will be hidden.
|
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']
|
||||||
|
```
|
||||||
17
src/pages/api/auth.js
Normal file
17
src/pages/api/auth.js
Normal file
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 { bookmarksResponse } from "utils/config/api-response";
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const auth = createAuthProvider(getSettings())
|
const { provider, groups } = readAuthSettings(getSettings().auth)
|
||||||
res.send(await bookmarksResponse(auth.permissions(req)));
|
res.send(await bookmarksResponse(provider.permissions(req), groups));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { servicesResponse } from "utils/config/api-response";
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
import createLogger from "utils/logger";
|
|
||||||
|
|
||||||
let logger = createLogger("services_index")
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
logger.log("Call services");
|
const { provider, groups } = readAuthSettings(getSettings().auth)
|
||||||
const auth = createAuthProvider(getSettings)
|
res.send(await servicesResponse(provider.permissions(req), groups));
|
||||||
const result = await servicesResponse(auth.permissions(req))
|
|
||||||
logger.log(result);
|
|
||||||
res.send(result);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { widgetsResponse } from "utils/config/api-response";
|
||||||
import { getSettings } from "utils/config/config";
|
import { getSettings } from "utils/config/config";
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const auth = createAuthProvider(getSettings());
|
const { provider } = readAuthSettings(getSettings().auth)
|
||||||
|
res.send(await widgetsResponse(provider.permissions(req)));
|
||||||
res.send(await widgetsResponse(auth.permissions(req)));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,7 +27,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 { createAuthorizer, fetchWithAuth } from "utils/auth/auth-helpers";
|
import { fetchWithAuth, readAuthSettings } from "utils/auth/auth-helpers";
|
||||||
import { NullAuthProvider } from "utils/auth/null";
|
import { NullAuthProvider } from "utils/auth/null";
|
||||||
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
|
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@ -48,12 +48,12 @@ export async function getServerSideProps({req}) {
|
|||||||
try {
|
try {
|
||||||
logger = createLogger("index");
|
logger = createLogger("index");
|
||||||
const { providers, auth, ...settings } = getSettings();
|
const { providers, auth, ...settings } = getSettings();
|
||||||
const authProvider = createAuthorizer({auth: auth});
|
const { provider, groups } = readAuthSettings(auth);
|
||||||
|
|
||||||
const services = await servicesResponse(authProvider.authorize(req));
|
const services = await servicesResponse(provider.authorize(req), groups);
|
||||||
const bookmarks = await bookmarksResponse(authProvider.authorize(req));
|
const bookmarks = await bookmarksResponse(provider.authorize(req), groups);
|
||||||
const widgets = await widgetsResponse(authProvider.authorize(req));
|
const widgets = await widgetsResponse(provider.authorize(req));
|
||||||
const authContext = authProvider.getContext(req);
|
const authContext = provider.getContext(req);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -10,31 +10,44 @@ function getProviderByKey(key) {
|
|||||||
return AuthProviders.find((provider) => provider.key == key) ?? NullAuthProvider;
|
return AuthProviders.find((provider) => provider.key == key) ?? NullAuthProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuthorizer({auth}) {
|
export function readAuthSettings({provider, groups} = {}) {
|
||||||
if (auth) {
|
return {
|
||||||
getProviderByKey(Object.keys(auth)[0]).create(auth[ProxyAuthKey]);
|
provider: provider ? getProviderByKey(provider.type).create(provider) : NullAuthProvider.create(),
|
||||||
}
|
groups: groups ? groups.map((group) => ({
|
||||||
return NullAuthProvider.create();
|
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) {
|
export async function fetchWithAuth(key, context) {
|
||||||
return getProviderByKey(context.provider).fetch([key, context]);
|
return getProviderByKey(context.provider).fetch([key, context]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterAllowedServices = (perms, services) => filterAllowedItems(perms, services, 'services');
|
export function checkAllowedGroup(perms, authGroups, groupName) {
|
||||||
export const filterAllowedBookmarks = (perms, bookmarks) => filterAllowedItems(perms, bookmarks, 'bookmarks');
|
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) => {
|
export const filterAllowedWidgets = (perms, widgets) => {
|
||||||
return widgets.filter((widget) => authItemFilter(perms, widget.options) )
|
return widgets.filter((widget) => authItemFilter(perms, widget.options) )
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterAllowedItems({user, groups}, itemGroups, groupKey) {
|
function filterAllowedItems(perms, authGroups, groups, groupKey) {
|
||||||
return itemGroups.map((group) => ({
|
return groups.filter((group) => checkAllowedGroup(perms, authGroups, group.name))
|
||||||
|
.map((group) => ({
|
||||||
name: group.name,
|
name: group.name,
|
||||||
[groupKey]: group[groupKey].filter((item) => authItemFilter({user, groups}, item))
|
[groupKey]: group[groupKey].filter((item) => authAllow(perms, item))
|
||||||
})).filter((group) => group[groupKey].length);
|
}))
|
||||||
|
.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 groupAllow = (('allowGroups' in item)) && groups.some(group => item.allowGroups.includes(group));
|
||||||
const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user);
|
const userAllow = (('allowUsers' in item)) && item.allowUsers.includes(user);
|
||||||
const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item));
|
const allowAll = (!('allowGroups' in item)) && (!('allowUsers' in item));
|
||||||
|
|||||||
@ -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.
|
// 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) {
|
function getProxyPermissions(userHeader, groupHeader, request) {
|
||||||
const user = (userHeader)?request.headers.get(userHeader):None;
|
const user = (userHeader)?request.headers.get(userHeader):None;
|
||||||
@ -13,11 +13,9 @@ function createProxyAuth({groupHeader, userHeader}) {
|
|||||||
return {
|
return {
|
||||||
getContext : (request) => {
|
getContext : (request) => {
|
||||||
return {
|
return {
|
||||||
provider: ProxyAuthKey,
|
type: ProxyAuthKey,
|
||||||
headers: {
|
...userHeader && {[userHeader]: request.headers.get(userHeader) },
|
||||||
...userHeader && {[userHeader]: request.headers.get(userHeader) },
|
...groupHeader && {[groupHeader]: request.headers.get(groupHeader)}
|
||||||
...groupHeader && {[groupHeader]: request.headers.get(groupHeader)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authorize : (request) => getProxyPermissions(userHeader, groupHeader, request)
|
authorize : (request) => getProxyPermissions(userHeader, groupHeader, request)
|
||||||
|
|||||||
@ -12,7 +12,6 @@ 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 {
|
import {
|
||||||
filterAllowedBookmarks,
|
filterAllowedBookmarks,
|
||||||
@ -31,7 +30,7 @@ function compareServices(service1, service2) {
|
|||||||
return service1.name.localeCompare(service2.name);
|
return service1.name.localeCompare(service2.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bookmarksResponse(perms) {
|
export async function bookmarksResponse(perms, authGroups) {
|
||||||
checkAndCopyConfig("bookmarks.yaml");
|
checkAndCopyConfig("bookmarks.yaml");
|
||||||
|
|
||||||
const bookmarksYaml = path.join(CONF_DIR, "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
|
// map easy to write YAML objects into easy to consume JS arrays
|
||||||
const bookmarksArray = filterAllowedBookmarks(perms,
|
const bookmarksArray = filterAllowedBookmarks(perms, authGroups,
|
||||||
bookmarks.map((group) => ({
|
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) => ({
|
||||||
@ -93,14 +92,14 @@ export async function widgetsResponse(perms) {
|
|||||||
return configuredWidgets;
|
return configuredWidgets;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function servicesResponse(perms) {
|
export async function servicesResponse(perms, authGroups) {
|
||||||
let discoveredDockerServices;
|
let discoveredDockerServices;
|
||||||
let discoveredKubernetesServices;
|
let discoveredKubernetesServices;
|
||||||
let configuredServices;
|
let configuredServices;
|
||||||
let initialSettings;
|
let initialSettings;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
discoveredDockerServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromDocker()));
|
discoveredDockerServices = filterAllowedServices(perms, authGroups, 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.");
|
||||||
}
|
}
|
||||||
@ -111,7 +110,7 @@ export async function servicesResponse(perms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
discoveredKubernetesServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromKubernetes()));
|
discoveredKubernetesServices = filterAllowedServices(perms, authGroups, 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());
|
||||||
@ -119,7 +118,7 @@ export async function servicesResponse(perms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
configuredServices = filterAllowedServices(perms, cleanServiceGroups(await servicesFromConfig()));
|
configuredServices = filterAllowedServices(perms, authGroups, 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());
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user