mirror of https://github.com/Lissy93/dashy.git
❇️ add keycloak group and role based visibility
This commit is contained in:
parent
312450a898
commit
da98ea5cf2
|
@ -215,6 +215,8 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
|
||||||
**`hideForUsers`** | `string[]` | _Optional_ | Current section will be visible to all users, except for those specified in this list
|
**`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
|
**`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`
|
**`hideForGuests`** | `boolean` | _Optional_ | Current section will be visible for logged in users, but not for guests (see `appConfig.enableGuestAccess`). Defaults to `false`
|
||||||
|
**`hideForKeycloakUsers`** | `object` | _Optional_ | Current section will be visible to all keycloak users, except for those configured via these groups and roles. See `hideForKeycloakUsers`
|
||||||
|
**`showForKeycloakUsers`** | `object` | _Optional_ | Current section will be hidden from all keyclaok users, except for those configured via these groups and roles. See `showForKeycloakUsers`
|
||||||
|
|
||||||
**[⬆️ Back to Top](#configuring)**
|
**[⬆️ Back to Top](#configuring)**
|
||||||
|
|
||||||
|
@ -226,6 +228,15 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
|
||||||
|
|
||||||
**[⬆️ Back to Top](#configuring)**
|
**[⬆️ Back to Top](#configuring)**
|
||||||
|
|
||||||
|
### `section.displayData.hideForKeycloakUsers` and `section.displayData.showForKeycloakUsers`
|
||||||
|
|
||||||
|
**Field** | **Type** | **Required**| **Description**
|
||||||
|
--- |------------| --- | ---
|
||||||
|
**`groups`** | `string[]` | _Optional_ | Current Section will be hidden or shown based on the user having any of the groups in this list
|
||||||
|
**`roles`** | `string[]` | _Optional_ | Current Section will be hidden or shown based on the user having any of the roles in this list
|
||||||
|
|
||||||
|
**[⬆️ Back to Top](#configuring)**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "Dashy",
|
"name": "Dashy",
|
||||||
"version": "1.9.5",
|
"version": "1.9.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "server",
|
"main": "server",
|
||||||
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
||||||
|
|
22
src/main.js
22
src/main.js
|
@ -2,7 +2,6 @@
|
||||||
// Import core framework and essential utils
|
// Import core framework and essential utils
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import VueI18n from 'vue-i18n'; // i18n for localization
|
import VueI18n from 'vue-i18n'; // i18n for localization
|
||||||
import Keycloak from 'keycloak-js';
|
|
||||||
|
|
||||||
// Import component Vue plugins, used throughout the app
|
// Import component Vue plugins, used throughout the app
|
||||||
import VTooltip from 'v-tooltip'; // A Vue directive for Popper.js, tooltip component
|
import VTooltip from 'v-tooltip'; // A Vue directive for Popper.js, tooltip component
|
||||||
|
@ -21,7 +20,7 @@ import clickOutside from '@/utils/ClickOutside'; // Directive for closing p
|
||||||
import { messages } from '@/utils/languages'; // Language texts
|
import { messages } from '@/utils/languages'; // Language texts
|
||||||
import ErrorReporting from '@/utils/ErrorReporting'; // Error reporting initializer (off)
|
import ErrorReporting from '@/utils/ErrorReporting'; // Error reporting initializer (off)
|
||||||
import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults';
|
import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults';
|
||||||
import { isKeycloakEnabled, getKeycloakConfig } from '@/utils/Auth'; // Keycloak auth config
|
import { isKeycloakEnabled, cleanupKeycloakInfo, initKeycloak } from '@/utils/Auth'; // Keycloak auth config
|
||||||
|
|
||||||
// Initialize global Vue components
|
// Initialize global Vue components
|
||||||
Vue.use(VueI18n);
|
Vue.use(VueI18n);
|
||||||
|
@ -59,22 +58,13 @@ const mount = () => new Vue({
|
||||||
store, router, render, i18n,
|
store, router, render, i18n,
|
||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
|
|
||||||
|
// every page reload removes keycloak user data
|
||||||
|
cleanupKeycloakInfo();
|
||||||
// If Keycloak not enabled, then proceed straight to the app
|
// If Keycloak not enabled, then proceed straight to the app
|
||||||
if (!isKeycloakEnabled()) {
|
if (!isKeycloakEnabled()) {
|
||||||
mount();
|
mount();
|
||||||
} else { // Keycloak is enabled, redirect to KC login page
|
} else { // Keycloak is enabled, redirect to KC login page
|
||||||
const { serverUrl, realm, clientId } = getKeycloakConfig();
|
initKeycloak()
|
||||||
const initOptions = {
|
.then(() => mount())
|
||||||
url: `${serverUrl}/auth`, realm, clientId, onLoad: 'login-required',
|
.catch(() => window.location.reload());
|
||||||
};
|
|
||||||
const keycloak = Keycloak(initOptions);
|
|
||||||
keycloak.init({ onLoad: initOptions.onLoad }).then((auth) => {
|
|
||||||
if (!auth) {
|
|
||||||
// Not authenticated, reload to Keycloak login page
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
// Yay - user successfully authenticated with Keycloak, render the app!
|
|
||||||
mount();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import sha256 from 'crypto-js/sha256';
|
import sha256 from 'crypto-js/sha256';
|
||||||
|
import Keycloak from 'keycloak-js';
|
||||||
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
||||||
import ErrorHandler from '@/utils/ErrorHandler';
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
import { cookieKeys, localStorageKeys, userStateEnum } from '@/utils/defaults';
|
import { cookieKeys, localStorageKeys, userStateEnum } from '@/utils/defaults';
|
||||||
|
@ -39,6 +40,62 @@ export const getKeycloakConfig = () => {
|
||||||
return keycloak;
|
return keycloak;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* helper that persists keycloak user data in browser local storage
|
||||||
|
* @param {Keycloak.KeycloakInstance} keycloak The username of user
|
||||||
|
*/
|
||||||
|
const storeKeycloakInfo = (keycloak) => {
|
||||||
|
if (keycloak.tokenParsed && typeof keycloak.tokenParsed === 'object') {
|
||||||
|
const {
|
||||||
|
groups,
|
||||||
|
realm_access: realmAccess,
|
||||||
|
resource_access: resourceAccess,
|
||||||
|
azp: clientId,
|
||||||
|
} = keycloak.tokenParsed;
|
||||||
|
|
||||||
|
const realmRoles = realmAccess.roles || [];
|
||||||
|
|
||||||
|
let clientRoles = [];
|
||||||
|
if (Object.hasOwn(resourceAccess, clientId)) {
|
||||||
|
clientRoles = resourceAccess[clientId].roles || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = [...realmRoles, ...clientRoles];
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
groups,
|
||||||
|
roles,
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem(localStorageKeys.KEYCLOAK_INFO, JSON.stringify(info));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* remove keycloak local storage */
|
||||||
|
export const cleanupKeycloakInfo = () => localStorage.removeItem(localStorageKeys.KEYCLOAK_INFO);
|
||||||
|
|
||||||
|
/* starts the keycloak login process and gathers user data */
|
||||||
|
export const initKeycloak = () => {
|
||||||
|
const { serverUrl, realm, clientId } = getKeycloakConfig();
|
||||||
|
const initOptions = {
|
||||||
|
url: `${serverUrl}/auth`, realm, clientId, onLoad: 'login-required',
|
||||||
|
};
|
||||||
|
const keycloak = Keycloak(initOptions);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
keycloak.init({ onLoad: initOptions.onLoad })
|
||||||
|
.then((auth) => {
|
||||||
|
if (auth) {
|
||||||
|
storeKeycloakInfo(keycloak);
|
||||||
|
return resolve();
|
||||||
|
} else {
|
||||||
|
return reject(new Error('Not authenticated'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((reason) => reject(reason));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/* Returns array of users from appConfig.auth, if available, else an empty array */
|
/* Returns array of users from appConfig.auth, if available, else an empty array */
|
||||||
const getUsers = () => {
|
const getUsers = () => {
|
||||||
const appConfig = getAppConfig();
|
const appConfig = getAppConfig();
|
||||||
|
@ -65,7 +122,6 @@ const generateUserToken = (user) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user is currently authenticated
|
* Checks if the user is currently authenticated
|
||||||
* @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
|
* @returns {Boolean} Will return true if the user is logged in, else false
|
||||||
*/
|
*/
|
||||||
export const isLoggedIn = () => {
|
export const isLoggedIn = () => {
|
||||||
|
@ -95,7 +151,7 @@ export const isAuthEnabled = () => {
|
||||||
/* Returns true if guest access is enabled */
|
/* Returns true if guest access is enabled */
|
||||||
export const isGuestAccessEnabled = () => {
|
export const isGuestAccessEnabled = () => {
|
||||||
const appConfig = getAppConfig();
|
const appConfig = getAppConfig();
|
||||||
if (appConfig.auth && typeof appConfig.auth === 'object') {
|
if (appConfig.auth && typeof appConfig.auth === 'object' && !isKeycloakEnabled()) {
|
||||||
return appConfig.auth.enableGuestAccess || false;
|
return appConfig.auth.enableGuestAccess || false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -108,6 +164,7 @@ export const isGuestAccessEnabled = () => {
|
||||||
* @param {String} username The username entered by the user
|
* @param {String} username The username entered by the user
|
||||||
* @param {String} pass The password entered by the user
|
* @param {String} pass The password entered by the user
|
||||||
* @param {String[]} users An array of valid user objects
|
* @param {String[]} users An array of valid user objects
|
||||||
|
* @param {Object} messages A static message template object
|
||||||
* @returns {Object} An object containing a boolean result and a message
|
* @returns {Object} An object containing a boolean result and a message
|
||||||
*/
|
*/
|
||||||
export const checkCredentials = (username, pass, users, messages) => {
|
export const checkCredentials = (username, pass, users, messages) => {
|
||||||
|
@ -146,7 +203,7 @@ export const login = (username, pass, timeout) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removed the browsers cookie, causing user to be logged out
|
* Removed the browsers' cookie, causing user to be logged out
|
||||||
*/
|
*/
|
||||||
export const logout = () => {
|
export const logout = () => {
|
||||||
document.cookie = 'authenticationToken=null';
|
document.cookie = 'authenticationToken=null';
|
||||||
|
@ -164,7 +221,7 @@ export const getCurrentUser = () => {
|
||||||
if (!username) return false; // No username
|
if (!username) return false; // No username
|
||||||
let foundUserObject = false; // Value to return
|
let foundUserObject = false; // Value to return
|
||||||
getUsers().forEach((user) => {
|
getUsers().forEach((user) => {
|
||||||
// If current logged in user found, then return that user
|
// If current logged-in user found, then return that user
|
||||||
if (user.user === username) foundUserObject = user;
|
if (user.user === username) foundUserObject = user;
|
||||||
});
|
});
|
||||||
return foundUserObject;
|
return foundUserObject;
|
||||||
|
@ -182,11 +239,10 @@ export const isLoggedInAsGuest = () => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the current user has admin privileges.
|
* Checks if the current user has admin privileges.
|
||||||
* If no users are setup, then function will always return true
|
* If no users are set up, then function will always return true
|
||||||
* But if auth is configured, then will verify user is correctly
|
* But if auth is configured, then will verify user is correctly
|
||||||
* logged in and then check weather they are of type admin, and
|
* logged in and then check weather they are of type admin, and
|
||||||
* return false if any conditions fail
|
* return false if any conditions fail
|
||||||
* @param {String[]} - Array of users
|
|
||||||
* @returns {Boolean} - True if admin privileges
|
* @returns {Boolean} - True if admin privileges
|
||||||
*/
|
*/
|
||||||
export const isUserAdmin = () => {
|
export const isUserAdmin = () => {
|
||||||
|
|
|
@ -6,24 +6,32 @@
|
||||||
|
|
||||||
// Import helper functions from auth, to get current user, and check if guest
|
// Import helper functions from auth, to get current user, and check if guest
|
||||||
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/Auth';
|
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/Auth';
|
||||||
|
import { localStorageKeys } from '@/utils/defaults';
|
||||||
|
|
||||||
/* Helper function, checks if a given username appears in a user array */
|
/* Helper function, checks if a given testValue is found in the visibility list */
|
||||||
const determineVisibility = (visibilityList, cUsername) => {
|
const determineVisibility = (visibilityList, testValue) => {
|
||||||
let isFound = false;
|
let isFound = false;
|
||||||
visibilityList.forEach((userInList) => {
|
visibilityList.forEach((visibilityItem) => {
|
||||||
if (userInList.toLowerCase() === cUsername) isFound = true;
|
if (visibilityItem.toLowerCase() === testValue.toLowerCase()) isFound = true;
|
||||||
});
|
});
|
||||||
return isFound;
|
return isFound;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Helper function, determines if two arrays have any intersecting elements
|
||||||
|
(one or more items that are the same) */
|
||||||
|
const determineIntersection = (source = [], target = []) => {
|
||||||
|
const intersections = source.filter(item => target.indexOf(item) !== -1);
|
||||||
|
return intersections.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
/* Returns false if this section should not be rendered for the current user/ guest */
|
/* Returns false if this section should not be rendered for the current user/ guest */
|
||||||
const isSectionVisibleToUser = (displayData, currentUser, isGuest) => {
|
const isSectionVisibleToUser = (displayData, currentUser, isGuest) => {
|
||||||
// Checks if user explicitly has access to a certain section
|
// Checks if user explicitly has access to a certain section
|
||||||
const checkVisiblity = () => {
|
const checkVisibility = () => {
|
||||||
if (!currentUser) return true;
|
if (!currentUser) return true;
|
||||||
const hideFor = displayData.hideForUsers || [];
|
const hideForUsers = displayData.hideForUsers || [];
|
||||||
const cUsername = currentUser.user.toLowerCase();
|
const cUsername = currentUser.user.toLowerCase();
|
||||||
return !determineVisibility(hideFor, cUsername);
|
return !determineVisibility(hideForUsers, cUsername);
|
||||||
};
|
};
|
||||||
// Checks if user is explicitly prevented from viewing a certain section
|
// Checks if user is explicitly prevented from viewing a certain section
|
||||||
const checkHiddenability = () => {
|
const checkHiddenability = () => {
|
||||||
|
@ -33,12 +41,36 @@ const isSectionVisibleToUser = (displayData, currentUser, isGuest) => {
|
||||||
if (showForUsers.length < 1) return true;
|
if (showForUsers.length < 1) return true;
|
||||||
return determineVisibility(showForUsers, cUsername);
|
return determineVisibility(showForUsers, cUsername);
|
||||||
};
|
};
|
||||||
|
const checkKeycloakVisibility = () => {
|
||||||
|
if (!displayData.hideForKeycloakUsers) return true;
|
||||||
|
|
||||||
|
const { groups, roles } = JSON.parse(localStorage.getItem(localStorageKeys.KEYCLOAK_INFO) || '{}');
|
||||||
|
const hideForGroups = displayData.hideForKeycloakUsers.groups || [];
|
||||||
|
const hideForRoles = displayData.hideForKeycloakUsers.roles || [];
|
||||||
|
|
||||||
|
return !(determineIntersection(hideForRoles, roles)
|
||||||
|
|| determineIntersection(hideForGroups, groups));
|
||||||
|
};
|
||||||
|
const checkKeycloakHiddenability = () => {
|
||||||
|
if (!displayData.showForKeycloakUsers) return true;
|
||||||
|
|
||||||
|
const { groups, roles } = JSON.parse(localStorage.getItem(localStorageKeys.KEYCLOAK_INFO) || '{}');
|
||||||
|
const showForGroups = displayData.showForKeycloakUsers.groups || [];
|
||||||
|
const showForRoles = displayData.showForKeycloakUsers.roles || [];
|
||||||
|
|
||||||
|
return determineIntersection(showForRoles, roles)
|
||||||
|
|| determineIntersection(showForGroups, groups);
|
||||||
|
};
|
||||||
// Checks if the current user is a guest, and if section allows for guests
|
// Checks if the current user is a guest, and if section allows for guests
|
||||||
const checkIfHideForGuest = () => {
|
const checkIfHideForGuest = () => {
|
||||||
const hideForGuest = displayData.hideForGuests;
|
const hideForGuest = displayData.hideForGuests;
|
||||||
return !(hideForGuest && isGuest);
|
return !(hideForGuest && isGuest);
|
||||||
};
|
};
|
||||||
return checkVisiblity() && checkHiddenability() && checkIfHideForGuest();
|
return checkVisibility()
|
||||||
|
&& checkHiddenability()
|
||||||
|
&& checkIfHideForGuest()
|
||||||
|
&& checkKeycloakVisibility()
|
||||||
|
&& checkKeycloakHiddenability();
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Putting it all together, the function to export */
|
/* Putting it all together, the function to export */
|
||||||
|
|
|
@ -613,6 +613,60 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": false,
|
"default": false,
|
||||||
"description": "If set to true, section will be visible for logged in users, but not for guests"
|
"description": "If set to true, section will be visible for logged in users, but not for guests"
|
||||||
|
},
|
||||||
|
"showForKeycloakUsers": {
|
||||||
|
"title": "Show for select Keycloak groups or roles",
|
||||||
|
"type": "object",
|
||||||
|
"description": "Configure the Keycloak groups or roles that will have access to this section",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"groups": {
|
||||||
|
"title": "Show for Groups",
|
||||||
|
"type": "array",
|
||||||
|
"description": "Section will be hidden from all users except those with one or more of these groups",
|
||||||
|
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the group that will be able to view this section"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"title": "Show for Roles",
|
||||||
|
"type": "array",
|
||||||
|
"description": "Section will be hidden from all users except those with one or more of these roles",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the role that will be able to view this section"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hideForKeycloakUsers": {
|
||||||
|
"title": "Hide for select Keycloak groups or roles",
|
||||||
|
"type": "object",
|
||||||
|
"description": "Configure the Keycloak groups or roles that will not have access to this section",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"groups": {
|
||||||
|
"title": "Hide for Groups",
|
||||||
|
"type": "array",
|
||||||
|
"description": "Section will be hidden from users with any of these groups",
|
||||||
|
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the group that will not be able to view this section"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"title": "Hide for Roles",
|
||||||
|
"type": "array",
|
||||||
|
"description": "Section will be hidden from users with any of roles",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the role that will not be able to view this section"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -124,6 +124,7 @@ module.exports = {
|
||||||
USERNAME: 'username',
|
USERNAME: 'username',
|
||||||
MOST_USED: 'mostUsed',
|
MOST_USED: 'mostUsed',
|
||||||
LAST_USED: 'lastUsed',
|
LAST_USED: 'lastUsed',
|
||||||
|
KEYCLOAK_INFO: 'keycloakInfo',
|
||||||
},
|
},
|
||||||
/* Key names for cookie identifiers */
|
/* Key names for cookie identifiers */
|
||||||
cookieKeys: {
|
cookieKeys: {
|
||||||
|
|
Loading…
Reference in New Issue