From 46c1ea87e876ffd2408dfa7090bc52a14320c8ee Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 21:48:35 +0100 Subject: [PATCH 1/8] :lock: Prevent modification of users locally --- src/utils/ConfigAccumalator.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/utils/ConfigAccumalator.js b/src/utils/ConfigAccumalator.js index b92ee569..30fc6be3 100644 --- a/src/utils/ConfigAccumalator.js +++ b/src/utils/ConfigAccumalator.js @@ -24,21 +24,25 @@ export default class ConfigAccumulator { /* App Config */ appConfig() { let appConfigFile = {}; - if (this.conf) { - appConfigFile = this.conf.appConfig || {}; - } + // Set app config from file + if (this.conf) appConfigFile = this.conf.appConfig || {}; + // Fill in defaults if anything missing let usersAppConfig = defaultAppConfig; if (localStorage[localStorageKeys.APP_CONFIG]) { usersAppConfig = JSON.parse(localStorage[localStorageKeys.APP_CONFIG]); } else if (appConfigFile !== {}) { usersAppConfig = appConfigFile; } + // Some settings have their own local storage keys, apply them here usersAppConfig.layout = localStorage[localStorageKeys.LAYOUT_ORIENTATION] || appConfigFile.layout || defaultLayout; usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE] || appConfigFile.iconSize || defaultIconSize; usersAppConfig.language = localStorage[localStorageKeys.LANGUAGE] || appConfigFile.language || defaultLanguage; + // Don't let users modify users locally + if (this.conf.auth) usersAppConfig.auth = appConfigFile.auth; + // All done, return final appConfig object return usersAppConfig; } From d54bb517dbc6ed22e76fc7ddf2155ff03a693de9 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 21:50:36 +0100 Subject: [PATCH 2/8] :sparkles: Re: #165 - Adds helper functions for granular access --- src/utils/Auth.js | 68 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/src/utils/Auth.js b/src/utils/Auth.js index 269b8e28..8af5c458 100644 --- a/src/utils/Auth.js +++ b/src/utils/Auth.js @@ -1,5 +1,19 @@ import sha256 from 'crypto-js/sha256'; -import { cookieKeys, localStorageKeys, userStateEnum } from './defaults'; +import ConfigAccumulator from '@/utils/ConfigAccumalator'; +import { cookieKeys, localStorageKeys, userStateEnum } from '@/utils/defaults'; + +/* Uses config accumulator to get and return app config */ +const getAppConfig = () => { + const Accumulator = new ConfigAccumulator(); + const config = Accumulator.config(); + return config.appConfig || {}; +}; + +/* Returns the users array from appConfig, if available, else an empty array */ +const getUsers = () => { + const appConfig = getAppConfig(); + return appConfig.auth || []; +}; /** * Generates a 1-way hash, in order to be stored in local storage for authentication @@ -17,7 +31,8 @@ const generateUserToken = (user) => { * @param {Array[Object]} users An array of user objects pulled from the config * @returns {Boolean} Will return true if the user is logged in, else false */ -export const isLoggedIn = (users) => { +export const isLoggedIn = () => { + const users = getUsers(); const validTokens = users.map((user) => generateUserToken(user)); let userAuthenticated = false; document.cookie.split(';').forEach((cookie) => { @@ -35,10 +50,16 @@ export const isLoggedIn = (users) => { }; /* Returns true if authentication is enabled */ -export const isAuthEnabled = (users) => (users && users.length > 0); +export const isAuthEnabled = () => { + const users = getUsers(); + return (users && users.length > 0); +}; /* Returns true if guest access is enabled */ -export const isGuestAccessEnabled = (appConfig) => appConfig.enableGuestAccess || false; +export const isGuestAccessEnabled = () => { + const appConfig = getAppConfig(); + return appConfig.enableGuestAccess || false; +}; /** * Checks credentials entered by the user against those in the config @@ -92,6 +113,33 @@ export const logout = () => { localStorage.removeItem(localStorageKeys.USERNAME); }; +/** + * If correctly logged in as a valid, authenticated user, + * then returns the user object for the current user + * If not logged in, will return false + * */ +export const getCurrentUser = () => { + if (!isLoggedIn()) return false; // User not logged in + const username = localStorage[localStorageKeys.USERNAME]; // Get username + if (!username) return false; // No username + let foundUserObject = false; // Value to return + getUsers().forEach((user) => { + // If current logged in user found, then return that user + if (user.user === username) foundUserObject = user; + }); + return foundUserObject; +}; + +/** + * Checks if the user is viewing the dashboard as a guest + * Returns true if guest mode enabled, and user not logged in + * */ +export const isLoggedInAsGuest = () => { + const guestEnabled = isGuestAccessEnabled(); + const notLoggedIn = !isLoggedIn(); + return guestEnabled && notLoggedIn; +}; + /** * Checks if the current user has admin privileges. * If no users are setup, then function will always return true @@ -101,9 +149,10 @@ export const logout = () => { * @param {String[]} - Array of users * @returns {Boolean} - True if admin privileges */ -export const isUserAdmin = (users) => { +export const isUserAdmin = () => { + const users = getUsers(); if (!users || users.length === 0) return true; // Authentication not setup - if (!isLoggedIn(users)) return false; // Auth setup, but not signed in as a valid user + if (!isLoggedIn()) return false; // Auth setup, but not signed in as a valid user const currentUser = localStorage[localStorageKeys.USERNAME]; let isAdmin = false; users.forEach((user) => { @@ -122,11 +171,12 @@ export const isUserAdmin = (users) => { * Note that if auth is enabled, but not guest access, and user not logged in, * then they will never be able to view the homepage, so no button needed */ -export const getUserState = (appConfig) => { +export const getUserState = () => { + const appConfig = getAppConfig(); const { notConfigured, loggedIn, guestAccess } = userStateEnum; // Numeric enum options const users = appConfig.auth || []; // Get auth object if (!isAuthEnabled(users)) return notConfigured; // No auth enabled - if (isLoggedIn(users)) return loggedIn; // User is logged in - if (isGuestAccessEnabled(appConfig)) return guestAccess; // Guest is viewing + if (isLoggedIn()) return loggedIn; // User is logged in + if (isGuestAccessEnabled()) return guestAccess; // Guest is viewing return notConfigured; }; From b71e1548ee702b94776893d9347d019150588721 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 21:51:43 +0100 Subject: [PATCH 3/8] :zap: Refactor, no longer need parameter passed to Auth functions --- src/components/Configuration/JsonEditor.vue | 2 +- src/components/Settings/SettingsContainer.vue | 2 +- src/router.js | 2 +- src/views/Login.vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Configuration/JsonEditor.vue b/src/components/Configuration/JsonEditor.vue index fc618cf5..28782dc4 100644 --- a/src/components/Configuration/JsonEditor.vue +++ b/src/components/Configuration/JsonEditor.vue @@ -102,7 +102,7 @@ export default { methods: { shouldAllowWriteToDisk() { const { appConfig } = this.config; - return appConfig.allowConfigEdit !== false && isUserAdmin(appConfig.auth); + return appConfig.allowConfigEdit !== false && isUserAdmin(); }, save() { if (this.saveMode === 'local' || !this.allowWriteToDisk) { diff --git a/src/components/Settings/SettingsContainer.vue b/src/components/Settings/SettingsContainer.vue index e3e2e4d6..0024ad6c 100644 --- a/src/components/Settings/SettingsContainer.vue +++ b/src/components/Settings/SettingsContainer.vue @@ -114,7 +114,7 @@ export default { * then they will never be able to view the homepage, so no button needed */ userState() { - return getUserState(this.appConfig || {}); + return getUserState(); }, }, data() { diff --git a/src/router.js b/src/router.js index aaa52e66..2800f8a5 100644 --- a/src/router.js +++ b/src/router.js @@ -31,7 +31,7 @@ const isGuestEnabled = () => { /* Returns true if user is already authenticated, or if auth is not enabled */ const isAuthenticated = () => { const users = config.appConfig.auth; - return (!users || users.length === 0 || isLoggedIn(users) || isGuestEnabled()); + return (!users || users.length === 0 || isLoggedIn() || isGuestEnabled()); }; /* Get the users chosen starting view from app config, or return default */ diff --git a/src/views/Login.vue b/src/views/Login.vue index 3e287a6c..a86908f3 100644 --- a/src/views/Login.vue +++ b/src/views/Login.vue @@ -126,7 +126,7 @@ export default { }, isUserAlreadyLoggedIn() { const users = this.appConfig.auth; - const loggedIn = (!users || users.length === 0 || isLoggedIn(users)); + const loggedIn = (!users || users.length === 0 || isLoggedIn()); return (loggedIn && this.existingUsername); }, isGuestAccessEnabled() { From 8df84252ac8f1651834104de17e558ee81d44547 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 21:52:24 +0100 Subject: [PATCH 4/8] :sparkles: Re: #165 - Implements granular user controlls --- src/components/LinkItems/ItemGroup.vue | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/components/LinkItems/ItemGroup.vue b/src/components/LinkItems/ItemGroup.vue index 7eb4589c..482c83b6 100644 --- a/src/components/LinkItems/ItemGroup.vue +++ b/src/components/LinkItems/ItemGroup.vue @@ -8,6 +8,7 @@ :rows="displayData.rows" :color="displayData.color" :customStyles="displayData.customStyles" + v-if="isSectionVisibleToUser()" >
No Items to Show Yet @@ -51,6 +52,7 @@ import Item from '@/components/LinkItems/Item.vue'; import Collapsable from '@/components/LinkItems/Collapsable.vue'; import IframeModal from '@/components/LinkItems/IframeModal.vue'; +import { getCurrentUser, isLoggedInAsGuest } from '@/utils/Auth'; export default { name: 'ItemGroup', @@ -85,6 +87,9 @@ export default { ? `grid-template-rows: repeat(${this.displayData.itemCountY}, 1fr);` : ''; return styles; }, + currentUser() { + return getCurrentUser(); + }, }, methods: { /* Returns a unique lowercase string, based on name, for section ID */ @@ -95,9 +100,11 @@ export default { triggerModal(url) { this.$refs[`iframeModal-${this.groupId}`].show(url); }, + /* Emmit value upwards when iframe modal opened/ closed */ modalChanged(changedTo) { this.$emit('change-modal-visibility', changedTo); }, + /* Determines if user has enabled online status checks */ shouldEnableStatusCheck(itemPreference) { const globalPreference = this.config.appConfig.statusCheck || false; return itemPreference !== undefined ? itemPreference : globalPreference; @@ -109,6 +116,35 @@ export default { if (interval < 1) interval = 0; return interval; }, + /* Returns false if this section should not be rendered for the current user/ guest */ + isSectionVisibleToUser() { + const determineVisibility = (visibilityList, currentUser) => { + let isFound = false; + visibilityList.forEach((userInList) => { + if (userInList.toLowerCase() === currentUser) isFound = true; + }); + return isFound; + }; + const checkVisiblity = () => { + if (!this.currentUser) return true; + const hideFor = this.displayData.hideForUsers || []; + const currentUser = this.currentUser.user.toLowerCase(); + return !determineVisibility(hideFor, currentUser); + }; + const checkHiddenability = () => { + if (!this.currentUser) return true; + const currentUser = this.currentUser.user.toLowerCase(); + const showForUsers = this.displayData.showForUsers || []; + if (showForUsers.length < 1) return true; + return determineVisibility(showForUsers, currentUser); + }; + const checkIfHideForGuest = () => { + const hideForGuest = this.displayData.hideForGuests; + const isGuest = isLoggedInAsGuest(); + return !(hideForGuest && isGuest); + }; + return checkVisiblity() && checkHiddenability() && checkIfHideForGuest(); + }, }, }; From 78e1fc6c94ceb6f68920ea5f82a51ada2b81dc85 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 22:19:17 +0100 Subject: [PATCH 5/8] :zap: Code improvments --- src/utils/Auth.js | 8 +++----- src/utils/ConfigAccumalator.js | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/utils/Auth.js b/src/utils/Auth.js index 8af5c458..3fd00d44 100644 --- a/src/utils/Auth.js +++ b/src/utils/Auth.js @@ -52,7 +52,7 @@ export const isLoggedIn = () => { /* Returns true if authentication is enabled */ export const isAuthEnabled = () => { const users = getUsers(); - return (users && users.length > 0); + return (users.length > 0); }; /* Returns true if guest access is enabled */ @@ -151,7 +151,7 @@ export const isLoggedInAsGuest = () => { */ export const isUserAdmin = () => { const users = getUsers(); - if (!users || users.length === 0) return true; // Authentication not setup + if (users.length === 0) return true; // Authentication not setup if (!isLoggedIn()) return false; // Auth setup, but not signed in as a valid user const currentUser = localStorage[localStorageKeys.USERNAME]; let isAdmin = false; @@ -172,10 +172,8 @@ export const isUserAdmin = () => { * then they will never be able to view the homepage, so no button needed */ export const getUserState = () => { - const appConfig = getAppConfig(); const { notConfigured, loggedIn, guestAccess } = userStateEnum; // Numeric enum options - const users = appConfig.auth || []; // Get auth object - if (!isAuthEnabled(users)) return notConfigured; // No auth enabled + if (!isAuthEnabled()) return notConfigured; // No auth enabled if (isLoggedIn()) return loggedIn; // User is logged in if (isGuestAccessEnabled()) return guestAccess; // Guest is viewing return notConfigured; diff --git a/src/utils/ConfigAccumalator.js b/src/utils/ConfigAccumalator.js index 30fc6be3..2edae611 100644 --- a/src/utils/ConfigAccumalator.js +++ b/src/utils/ConfigAccumalator.js @@ -41,7 +41,7 @@ export default class ConfigAccumulator { usersAppConfig.language = localStorage[localStorageKeys.LANGUAGE] || appConfigFile.language || defaultLanguage; // Don't let users modify users locally - if (this.conf.auth) usersAppConfig.auth = appConfigFile.auth; + if (appConfigFile.auth) usersAppConfig.auth = appConfigFile.auth; // All done, return final appConfig object return usersAppConfig; } From eca0c44320ad9cce6577a89cf1b8b6aa0ce35c01 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 22:20:04 +0100 Subject: [PATCH 6/8] :memo: Writes docs for granular auth access --- docs/authentication.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/authentication.md b/docs/authentication.md index cb46cf02..4013bc15 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -39,6 +39,33 @@ Once authentication is enabled, so long as there is no valid token in cookie sto ## Enabling Guest Access With authentication setup, by default no access is allowed to your dashboard without first logging in with valid credentials. Guest mode can be enabled to allow for read-only access to a secured dashboard by any user, without the need to log in. A guest user cannot write any changes to the config file, but can apply modifications locally (stored in their browser). You can enable guest access, by setting `appConfig.enableGuestAccess: true`. +## Granular Access +You can use the following properties to make certain sections only visible to some users, or hide sections from guests. +- `hideForUsers` - Section will be visible to all users, except for those specified in this list +- `showForUsers` - Section will be hidden from all users, except for those specified in this list +- `hideForGuests` - Section will be visible for logged in users, but not for guests + +For Example: + +```yaml +- name: Code Analysis & Monitoring + icon: fas fa-code + displayData: + cols: 2 + hideForUsers: [alicia, bob] + items: + ... +``` + +```yaml +- name: Deployment Pipelines + icon: fas fa-rocket + displayData: + hideForGuests: true + items: + ... +``` + ## Security Since all authentication is happening entirely on the client-side, it is vulnerable to manipulation by an adversary. An attacker could look at the source code, find the function used generate the auth token, then decode the minified JavaScript to find the hash, and manually generate a token using it, then just insert that value as a cookie using the console, and become a logged in user. Therefore, if you need secure authentication for your app, it is strongly recommended to implement this using your web server, or use a VPN to control access to Dashy. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage. From 95bc5d2c49d02763c46537924a121858e9b32c91 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 22:20:40 +0100 Subject: [PATCH 7/8] :card_file_box: Adds new config attributes for granular auth access --- docs/configuring.md | 3 +++ src/utils/ConfigSchema.json | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/docs/configuring.md b/docs/configuring.md index efef35bd..a17d93b8 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -149,6 +149,9 @@ To disallow any changes from being written to disk via the UI config editor, set **`sectionLayout`** | `string` | _Optional_ | Specify which CSS layout will be used to responsivley place items. Can be either `auto` (which uses flex layout), or `grid`. If `grid` is selected, then `itemCountX` and `itemCountY` may also be set. Defaults to `auto` **`itemCountX`** | `number` | _Optional_ | The number of items to display per row / horizontally. If not set, it will be calculated automatically based on available space. Can only be set if `sectionLayout` is set to `grid`. Must be a whole number between `1` and `12` **`itemCountY`** | `number` | _Optional_ | The number of items to display per column / vertically. If not set, it will be calculated automatically based on available space. If `itemCountX` is set, then `itemCountY` can be calculated automatically. Can only be set if `sectionLayout` is set to `grid`. Must be a whole number between `1` and `12` +**`hideForUsers`** | `string[]` | _Optional_ | Current section will be visible to all users, except for those specified in this list +**`showForUsers`** | `string[]` | _Optional_ | Current section will be hidden from all users, except for those specified in this list +**`hideForGuests`** | `boolean` | _Optional_ | Current section will be visible for logged in users, but not for guests (see `appConfig.enableGuestAccess`). Defaults to `false` **[⬆️ Back to Top](#configuring)** diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json index 96aef5e2..73b7b684 100644 --- a/src/utils/ConfigSchema.json +++ b/src/utils/ConfigSchema.json @@ -369,6 +369,27 @@ "minimum": 1, "maximum": 12, "description": "Number of items per row" + }, + "hideForUsers": { + "type": "array", + "description": "Section will be visible to all users, except for those specified in this list", + "items": { + "type": "string", + "description": "Username for the user that will not be able to view this section" + } + }, + "showForUsers": { + "type": "array", + "description": "Section will be hidden from all users, except for those specified in this list", + "items": { + "type": "string", + "description": "Username for the user that will have access to this section" + } + }, + "hideForGuests": { + "type": "boolean", + "default": false, + "description": "If set to true, section will be visible for logged in users, but not for guests" } } }, From d0c7fefbb02a8637e0550a92882fee3bb9d58f5f Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 20 Aug 2021 22:24:29 +0100 Subject: [PATCH 8/8] :bookmark: Bumps to V 1.6.4 and updates changelog --- .github/CHANGELOG.md | 7 ++++++- package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 98616a30..2879f3b0 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog -## ✨ 1.6.3 - Dependency and Build File Updates [PR #168](https://github.com/Lissy93/dashy/pull/168) +## ✨ 1.6.4 - Adds functionality for Granular Auth Control [PR #171](https://github.com/Lissy93/dashy/pull/171) +- Enables sections to be visible for all users except for those specified +- Enables sections to be hidden from all users except for those specified +- Enables sections to be hidden from guests, but visible to all authenticated users + +## ⚡️ 1.6.3 - Dependency and Build File Updates [PR #168](https://github.com/Lissy93/dashy/pull/168) - Removes any dependencies which are not 100% essential - Moves packages that are only used for building into devDependencies - Updates dependencies to latest version diff --git a/package.json b/package.json index 9a95cb46..dd692cd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Dashy", - "version": "1.6.3", + "version": "1.6.4", "license": "MIT", "main": "server", "scripts": {