Skip to content

Commit fa56446

Browse files
authored
🔀 Merge pull request gchq#171 from Lissy93/FEATURE/granular-access-165
[FEATURE] Granular User Access Fixes gchq#165
2 parents aa6081d + d0c7fef commit fa56446

File tree

12 files changed

+165
-21
lines changed

12 files changed

+165
-21
lines changed

.github/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

3-
## ✨ 1.6.3 - Dependency and Build File Updates [PR #168](https://github.com/Lissy93/dashy/pull/168)
3+
## ✨ 1.6.4 - Adds functionality for Granular Auth Control [PR #171](https://github.com/Lissy93/dashy/pull/171)
4+
- Enables sections to be visible for all users except for those specified
5+
- Enables sections to be hidden from all users except for those specified
6+
- Enables sections to be hidden from guests, but visible to all authenticated users
7+
8+
## ⚡️ 1.6.3 - Dependency and Build File Updates [PR #168](https://github.com/Lissy93/dashy/pull/168)
49
- Removes any dependencies which are not 100% essential
510
- Moves packages that are only used for building into devDependencies
611
- Updates dependencies to latest version

docs/authentication.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,33 @@ Once authentication is enabled, so long as there is no valid token in cookie sto
3939
## Enabling Guest Access
4040
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`.
4141

42+
## Granular Access
43+
You can use the following properties to make certain sections only visible to some users, or hide sections from guests.
44+
- `hideForUsers` - Section will be visible to all users, except for those specified in this list
45+
- `showForUsers` - Section will be hidden from all users, except for those specified in this list
46+
- `hideForGuests` - Section will be visible for logged in users, but not for guests
47+
48+
For Example:
49+
50+
```yaml
51+
- name: Code Analysis & Monitoring
52+
icon: fas fa-code
53+
displayData:
54+
cols: 2
55+
hideForUsers: [alicia, bob]
56+
items:
57+
...
58+
```
59+
60+
```yaml
61+
- name: Deployment Pipelines
62+
icon: fas fa-rocket
63+
displayData:
64+
hideForGuests: true
65+
items:
66+
...
67+
```
68+
4269
## Security
4370
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.
4471

docs/configuring.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,9 @@ To disallow any changes from being written to disk via the UI config editor, set
149149
**`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`
150150
**`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`
151151
**`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`
152+
**`hideForUsers`** | `string[]` | _Optional_ | Current section will be visible to all users, except for those specified in this list
153+
**`showForUsers`** | `string[]` | _Optional_ | Current section will be hidden from all users, except for those specified in this list
154+
**`hideForGuests`** | `boolean` | _Optional_ | Current section will be visible for logged in users, but not for guests (see `appConfig.enableGuestAccess`). Defaults to `false`
152155

153156
**[⬆️ Back to Top](#configuring)**
154157

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Dashy",
3-
"version": "1.6.3",
3+
"version": "1.6.4",
44
"license": "MIT",
55
"main": "server",
66
"scripts": {

src/components/Configuration/JsonEditor.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export default {
102102
methods: {
103103
shouldAllowWriteToDisk() {
104104
const { appConfig } = this.config;
105-
return appConfig.allowConfigEdit !== false && isUserAdmin(appConfig.auth);
105+
return appConfig.allowConfigEdit !== false && isUserAdmin();
106106
},
107107
save() {
108108
if (this.saveMode === 'local' || !this.allowWriteToDisk) {

src/components/LinkItems/ItemGroup.vue

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
:rows="displayData.rows"
99
:color="displayData.color"
1010
:customStyles="displayData.customStyles"
11+
v-if="isSectionVisibleToUser()"
1112
>
1213
<div v-if="!items || items.length < 1" class="no-items">
1314
No Items to Show Yet
@@ -51,6 +52,7 @@
5152
import Item from '@/components/LinkItems/Item.vue';
5253
import Collapsable from '@/components/LinkItems/Collapsable.vue';
5354
import IframeModal from '@/components/LinkItems/IframeModal.vue';
55+
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/Auth';
5456
5557
export default {
5658
name: 'ItemGroup',
@@ -85,6 +87,9 @@ export default {
8587
? `grid-template-rows: repeat(${this.displayData.itemCountY}, 1fr);` : '';
8688
return styles;
8789
},
90+
currentUser() {
91+
return getCurrentUser();
92+
},
8893
},
8994
methods: {
9095
/* Returns a unique lowercase string, based on name, for section ID */
@@ -95,9 +100,11 @@ export default {
95100
triggerModal(url) {
96101
this.$refs[`iframeModal-${this.groupId}`].show(url);
97102
},
103+
/* Emmit value upwards when iframe modal opened/ closed */
98104
modalChanged(changedTo) {
99105
this.$emit('change-modal-visibility', changedTo);
100106
},
107+
/* Determines if user has enabled online status checks */
101108
shouldEnableStatusCheck(itemPreference) {
102109
const globalPreference = this.config.appConfig.statusCheck || false;
103110
return itemPreference !== undefined ? itemPreference : globalPreference;
@@ -109,6 +116,35 @@ export default {
109116
if (interval < 1) interval = 0;
110117
return interval;
111118
},
119+
/* Returns false if this section should not be rendered for the current user/ guest */
120+
isSectionVisibleToUser() {
121+
const determineVisibility = (visibilityList, currentUser) => {
122+
let isFound = false;
123+
visibilityList.forEach((userInList) => {
124+
if (userInList.toLowerCase() === currentUser) isFound = true;
125+
});
126+
return isFound;
127+
};
128+
const checkVisiblity = () => {
129+
if (!this.currentUser) return true;
130+
const hideFor = this.displayData.hideForUsers || [];
131+
const currentUser = this.currentUser.user.toLowerCase();
132+
return !determineVisibility(hideFor, currentUser);
133+
};
134+
const checkHiddenability = () => {
135+
if (!this.currentUser) return true;
136+
const currentUser = this.currentUser.user.toLowerCase();
137+
const showForUsers = this.displayData.showForUsers || [];
138+
if (showForUsers.length < 1) return true;
139+
return determineVisibility(showForUsers, currentUser);
140+
};
141+
const checkIfHideForGuest = () => {
142+
const hideForGuest = this.displayData.hideForGuests;
143+
const isGuest = isLoggedInAsGuest();
144+
return !(hideForGuest && isGuest);
145+
};
146+
return checkVisiblity() && checkHiddenability() && checkIfHideForGuest();
147+
},
112148
},
113149
};
114150
</script>

src/components/Settings/SettingsContainer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export default {
114114
* then they will never be able to view the homepage, so no button needed
115115
*/
116116
userState() {
117-
return getUserState(this.appConfig || {});
117+
return getUserState();
118118
},
119119
},
120120
data() {

src/router.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const isGuestEnabled = () => {
3131
/* Returns true if user is already authenticated, or if auth is not enabled */
3232
const isAuthenticated = () => {
3333
const users = config.appConfig.auth;
34-
return (!users || users.length === 0 || isLoggedIn(users) || isGuestEnabled());
34+
return (!users || users.length === 0 || isLoggedIn() || isGuestEnabled());
3535
};
3636

3737
/* Get the users chosen starting view from app config, or return default */

src/utils/Auth.js

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
import sha256 from 'crypto-js/sha256';
2-
import { cookieKeys, localStorageKeys, userStateEnum } from './defaults';
2+
import ConfigAccumulator from '@/utils/ConfigAccumalator';
3+
import { cookieKeys, localStorageKeys, userStateEnum } from '@/utils/defaults';
4+
5+
/* Uses config accumulator to get and return app config */
6+
const getAppConfig = () => {
7+
const Accumulator = new ConfigAccumulator();
8+
const config = Accumulator.config();
9+
return config.appConfig || {};
10+
};
11+
12+
/* Returns the users array from appConfig, if available, else an empty array */
13+
const getUsers = () => {
14+
const appConfig = getAppConfig();
15+
return appConfig.auth || [];
16+
};
317

418
/**
519
* Generates a 1-way hash, in order to be stored in local storage for authentication
@@ -17,7 +31,8 @@ const generateUserToken = (user) => {
1731
* @param {Array[Object]} users An array of user objects pulled from the config
1832
* @returns {Boolean} Will return true if the user is logged in, else false
1933
*/
20-
export const isLoggedIn = (users) => {
34+
export const isLoggedIn = () => {
35+
const users = getUsers();
2136
const validTokens = users.map((user) => generateUserToken(user));
2237
let userAuthenticated = false;
2338
document.cookie.split(';').forEach((cookie) => {
@@ -35,10 +50,16 @@ export const isLoggedIn = (users) => {
3550
};
3651

3752
/* Returns true if authentication is enabled */
38-
export const isAuthEnabled = (users) => (users && users.length > 0);
53+
export const isAuthEnabled = () => {
54+
const users = getUsers();
55+
return (users.length > 0);
56+
};
3957

4058
/* Returns true if guest access is enabled */
41-
export const isGuestAccessEnabled = (appConfig) => appConfig.enableGuestAccess || false;
59+
export const isGuestAccessEnabled = () => {
60+
const appConfig = getAppConfig();
61+
return appConfig.enableGuestAccess || false;
62+
};
4263

4364
/**
4465
* Checks credentials entered by the user against those in the config
@@ -92,6 +113,33 @@ export const logout = () => {
92113
localStorage.removeItem(localStorageKeys.USERNAME);
93114
};
94115

116+
/**
117+
* If correctly logged in as a valid, authenticated user,
118+
* then returns the user object for the current user
119+
* If not logged in, will return false
120+
* */
121+
export const getCurrentUser = () => {
122+
if (!isLoggedIn()) return false; // User not logged in
123+
const username = localStorage[localStorageKeys.USERNAME]; // Get username
124+
if (!username) return false; // No username
125+
let foundUserObject = false; // Value to return
126+
getUsers().forEach((user) => {
127+
// If current logged in user found, then return that user
128+
if (user.user === username) foundUserObject = user;
129+
});
130+
return foundUserObject;
131+
};
132+
133+
/**
134+
* Checks if the user is viewing the dashboard as a guest
135+
* Returns true if guest mode enabled, and user not logged in
136+
* */
137+
export const isLoggedInAsGuest = () => {
138+
const guestEnabled = isGuestAccessEnabled();
139+
const notLoggedIn = !isLoggedIn();
140+
return guestEnabled && notLoggedIn;
141+
};
142+
95143
/**
96144
* Checks if the current user has admin privileges.
97145
* If no users are setup, then function will always return true
@@ -101,9 +149,10 @@ export const logout = () => {
101149
* @param {String[]} - Array of users
102150
* @returns {Boolean} - True if admin privileges
103151
*/
104-
export const isUserAdmin = (users) => {
105-
if (!users || users.length === 0) return true; // Authentication not setup
106-
if (!isLoggedIn(users)) return false; // Auth setup, but not signed in as a valid user
152+
export const isUserAdmin = () => {
153+
const users = getUsers();
154+
if (users.length === 0) return true; // Authentication not setup
155+
if (!isLoggedIn()) return false; // Auth setup, but not signed in as a valid user
107156
const currentUser = localStorage[localStorageKeys.USERNAME];
108157
let isAdmin = false;
109158
users.forEach((user) => {
@@ -122,11 +171,10 @@ export const isUserAdmin = (users) => {
122171
* Note that if auth is enabled, but not guest access, and user not logged in,
123172
* then they will never be able to view the homepage, so no button needed
124173
*/
125-
export const getUserState = (appConfig) => {
174+
export const getUserState = () => {
126175
const { notConfigured, loggedIn, guestAccess } = userStateEnum; // Numeric enum options
127-
const users = appConfig.auth || []; // Get auth object
128-
if (!isAuthEnabled(users)) return notConfigured; // No auth enabled
129-
if (isLoggedIn(users)) return loggedIn; // User is logged in
130-
if (isGuestAccessEnabled(appConfig)) return guestAccess; // Guest is viewing
176+
if (!isAuthEnabled()) return notConfigured; // No auth enabled
177+
if (isLoggedIn()) return loggedIn; // User is logged in
178+
if (isGuestAccessEnabled()) return guestAccess; // Guest is viewing
131179
return notConfigured;
132180
};

src/utils/ConfigAccumalator.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,25 @@ export default class ConfigAccumulator {
2424
/* App Config */
2525
appConfig() {
2626
let appConfigFile = {};
27-
if (this.conf) {
28-
appConfigFile = this.conf.appConfig || {};
29-
}
27+
// Set app config from file
28+
if (this.conf) appConfigFile = this.conf.appConfig || {};
29+
// Fill in defaults if anything missing
3030
let usersAppConfig = defaultAppConfig;
3131
if (localStorage[localStorageKeys.APP_CONFIG]) {
3232
usersAppConfig = JSON.parse(localStorage[localStorageKeys.APP_CONFIG]);
3333
} else if (appConfigFile !== {}) {
3434
usersAppConfig = appConfigFile;
3535
}
36+
// Some settings have their own local storage keys, apply them here
3637
usersAppConfig.layout = localStorage[localStorageKeys.LAYOUT_ORIENTATION]
3738
|| appConfigFile.layout || defaultLayout;
3839
usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE]
3940
|| appConfigFile.iconSize || defaultIconSize;
4041
usersAppConfig.language = localStorage[localStorageKeys.LANGUAGE]
4142
|| appConfigFile.language || defaultLanguage;
43+
// Don't let users modify users locally
44+
if (appConfigFile.auth) usersAppConfig.auth = appConfigFile.auth;
45+
// All done, return final appConfig object
4246
return usersAppConfig;
4347
}
4448

src/utils/ConfigSchema.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,27 @@
369369
"minimum": 1,
370370
"maximum": 12,
371371
"description": "Number of items per row"
372+
},
373+
"hideForUsers": {
374+
"type": "array",
375+
"description": "Section will be visible to all users, except for those specified in this list",
376+
"items": {
377+
"type": "string",
378+
"description": "Username for the user that will not be able to view this section"
379+
}
380+
},
381+
"showForUsers": {
382+
"type": "array",
383+
"description": "Section will be hidden from all users, except for those specified in this list",
384+
"items": {
385+
"type": "string",
386+
"description": "Username for the user that will have access to this section"
387+
}
388+
},
389+
"hideForGuests": {
390+
"type": "boolean",
391+
"default": false,
392+
"description": "If set to true, section will be visible for logged in users, but not for guests"
372393
}
373394
}
374395
},

src/views/Login.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export default {
126126
},
127127
isUserAlreadyLoggedIn() {
128128
const users = this.appConfig.auth;
129-
const loggedIn = (!users || users.length === 0 || isLoggedIn(users));
129+
const loggedIn = (!users || users.length === 0 || isLoggedIn());
130130
return (loggedIn && this.existingUsername);
131131
},
132132
isGuestAccessEnabled() {

0 commit comments

Comments
 (0)