🔀 Merge pull request #691 from pinarruiz/feature/extend-visibility-items

[FEATURE] Extend visibility to section items
This commit is contained in:
Alicia Sykes 2022-06-03 19:16:37 +01:00 committed by GitHub
commit ef786dbb7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 233 additions and 83 deletions

View File

@ -48,10 +48,10 @@ Once authentication is enabled, so long as there is no valid token in cookie sto
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.auth.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
You can use the following properties to make certain sections or items only visible to some users, or hide sections and items from guests.
- `hideForUsers` - Section or Item will be visible to all users, except for those specified in this list
- `showForUsers` - Section or Item will be hidden from all users, except for those specified in this list
- `hideForGuests` - Section or Item will be visible for logged in users, but not for guests
For Example:
@ -71,7 +71,9 @@ For Example:
displayData:
hideForGuests: true
items:
...
- title: Hide Me
displayData:
hideForUsers: [alicia, bob]
```
### Permissions
@ -149,9 +151,9 @@ appConfig:
Note that if you are using Keycloak V 17 or older, you will also need to set `legacySupport: true` (also under `appConfig.auth.keycloak`). This is because the API endpoint was updated in later versions.
### 4. Add groups and roles (Optional)
Keycloak allows you to assign users roles and groups. You can use these values to configure who can access various sections in Dashy.
Keycloak allows you to assign users roles and groups. You can use these values to configure who can access various sections or items in Dashy.
Keycloak server administration and configuration is a deep topic; please refer to the [server admin guide](https://www.keycloak.org/docs/latest/server_admin/index.html#assigning-permissions-and-access-using-roles-and-groups) to see details about creating and assigning roles and groups.
Once you have groups or roles assigned to users you can configure access under each sections `displayData.showForKeycloakUser` and `displayData.hideForKeycloakUser`.
Once you have groups or roles assigned to users you can configure access under each section or item `displayData.showForKeycloakUser` and `displayData.hideForKeycloakUser`.
Both show and hide configurations accept a list of `groups` and `roles` that limit access. If a users data matches one or more items in these lists they will be allowed or excluded as defined.
```yaml
sections:
@ -161,6 +163,11 @@ sections:
roles: ['canViewDevResources']
hideForKeycloakUsers:
groups: ['ProductTeam']
items:
- title: Not Visible for developers
displayData:
hideForKeycloakUsers:
groups: ['DevelopmentTeam']
```
Depending on how you're hosting Dashy and Keycloak, you may also need to set some HTTP headers, to prevent a CORS error. This would typically be the `Access-Control-Allow-Origin [URL-of Dashy]` on your Keycloak instance. See the [Setting Headers](https://github.com/Lissy93/dashy/blob/master/docs/management.md#setting-headers) guide in the management docs for more info.

View File

@ -36,10 +36,12 @@ The following file provides a reference of all supported configuration options.
- [`keycloak`](#appconfigauthkeycloak-optional) - Auth config for Keycloak
- [**`sections`**](#section) - List of sections
- [`displayData`](#sectiondisplaydata-optional) - Section display settings
- [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-and-sectiondisplaydatashowforkeycloakusers) - Set user controls
- [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-sectiondisplaydatashowforkeycloakusers-itemdisplaydatahideforkeycloakusers-and-itemdisplaydatashowforkeycloakusers) - Set user controls
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for a section
- [`items`](#sectionitem) - List of items
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for an item
- [`displayData`](#itemdisplaydata-optional) - Item display settings
- [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-sectiondisplaydatashowforkeycloakusers-itemdisplaydatahideforkeycloakusers-and-itemdisplaydatashowforkeycloakusers) - Set user controls
- [`widgets`](#sectionwidget-optional) - List of widgets
- [**Notes**](#notes)
- [Editing Config through the UI](#editing-config-through-the-ui)
@ -224,9 +226,24 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**`color`** | `string` | _Optional_ | An optional color for the text and font-awesome icon to be displayed in. Note that this will override the current theme and so may not display well
**`backgroundColor`** | `string` | _Optional_ | An optional background fill color for the that given item. Again, this will override the current theme and so might not display well against the background
**`provider`** | `string` | _Optional_ | The name of the provider for a given service, useful for when including hosted apps. In some themes, this is visible under the item name
**`displayData`** | `object` | _Optional_ | Meta-data to optionally overide display settings for a given item. See [`displayData`](#itemdisplaydata-optional)
**[⬆️ Back to Top](#configuring)**
### `item.displayData` _(optional)_
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`hideForUsers`** | `string[]` | _Optional_ | Current item will be visible to all users, except for those specified in this list
**`showForUsers`** | `string[]` | _Optional_ | Current item will be hidden from all users, except for those specified in this list
**`hideForGuests`** | `boolean` | _Optional_ | Current item will be visible for logged in users, but not for guests (see `appConfig.enableGuestAccess`). Defaults to `false`
**`hideForKeycloakUsers`** | `object` | _Optional_ | Current item will be visible to all keycloak users, except for those configured via these groups and roles. See `hideForKeycloakUsers`
**`showForKeycloakUsers`** | `object` | _Optional_ | Current item will be hidden from all keycloak users, except for those configured via these groups and roles. See `showForKeycloakUsers`
**[⬆️ Back to Top](#configuring)**
### `section.widget` _(optional)_
**Field** | **Type** | **Required**| **Description**
@ -259,7 +276,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**`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`
**`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`
**`showForKeycloakUsers`** | `object` | _Optional_ | Current section will be hidden from all keycloak users, except for those configured via these groups and roles. See `showForKeycloakUsers`
**[⬆️ Back to Top](#configuring)**
@ -271,12 +288,12 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**[⬆️ Back to Top](#configuring)**
### `section.displayData.hideForKeycloakUsers` and `section.displayData.showForKeycloakUsers`
### `section.displayData.hideForKeycloakUsers`, `section.displayData.showForKeycloakUsers`, `item.displayData.hideForKeycloakUsers` and `item.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
**`groups`** | `string[]` | _Optional_ | Current Section or Item will be hidden or shown based on the user having any of the groups in this list
**`roles`** | `string[]` | _Optional_ | Current Section or Item will be hidden or shown based on the user having any of the roles in this list
**[⬆️ Back to Top](#configuring)**

View File

@ -13,7 +13,7 @@
<transition name="slide">
<SideBarSection
v-if="isOpen[index]"
:items="section.items"
:items="filterTiles(section.items)"
@launch-app="launchApp"
/>
</transition>
@ -36,6 +36,7 @@ import SideBarItem from '@/components/Workspace/SideBarItem.vue';
import SideBarSection from '@/components/Workspace/SideBarSection.vue';
import IconHome from '@/assets/interface-icons/application-home.svg';
import IconMinimalView from '@/assets/interface-icons/application-minimal.svg';
import { checkItemVisibility } from '@/utils/CheckItemVisibility';
export default {
name: 'SideBar',
@ -77,6 +78,13 @@ export default {
});
});
},
/* Return a list with visible items on a section to the user or guest */
filterTiles(allTiles) {
if (!allTiles) {
return [];
}
return allTiles.filter((tile) => checkItemVisibility(tile));
},
},
mounted() {
if (this.sections.length === 1) { // If only 1 section, go ahead and open it

View File

@ -5,6 +5,7 @@
import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
import { searchTiles } from '@/utils/Search';
import { checkItemVisibility } from '@/utils/CheckItemVisibility';
const HomeMixin = {
props: {
@ -64,8 +65,11 @@ const HomeMixin = {
},
/* Returns only the tiles that match the users search query */
filterTiles(allTiles) {
if (!allTiles) return [];
return searchTiles(allTiles, this.searchValue);
if (!allTiles) {
return [];
}
const visibleTiles = allTiles.filter((tile) => checkItemVisibility(tile));
return searchTiles(visibleTiles, this.searchValue);
},
/* Checks if any sections or items use icons from a given CDN */
checkIfIconLibraryNeeded(prefix) {

View File

@ -0,0 +1,19 @@
/**
* A helper function that checks if an item is visible based on current users permissions
* Checks an item displayData for hideForUsers, showForUsers and hideForGuests
* Returns a boolean that determines if the user has the required permissions
*/
// Import helper functions from auth, to get current user, and check if guest
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/Auth';
import { isVisibleToUser } from '@/utils/IsVisibleToUser';
/* Putting it all together, the function to export */
export const checkItemVisibility = (item) => {
const currentUser = getCurrentUser(); // Get current user object
const isGuest = isLoggedInAsGuest(); // Check if current user is a guest
const displayData = item.displayData || {};
return isVisibleToUser(displayData, currentUser, isGuest);
};
export default checkItemVisibility;

View File

@ -6,80 +6,15 @@
// Import helper functions from auth, to get current user, and check if guest
import { getCurrentUser, isLoggedInAsGuest } from '@/utils/Auth';
import { localStorageKeys } from '@/utils/defaults';
/* Helper function, checks if a given testValue is found in the visibility list */
const determineVisibility = (visibilityList, testValue) => {
let isFound = false;
visibilityList.forEach((visibilityItem) => {
if (visibilityItem.toLowerCase() === testValue.toLowerCase()) isFound = true;
});
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 */
const isSectionVisibleToUser = (displayData, currentUser, isGuest) => {
// Checks if user explicitly has access to a certain section
const checkVisibility = () => {
if (!currentUser) return true;
const hideForUsers = displayData.hideForUsers || [];
const cUsername = currentUser.user.toLowerCase();
return !determineVisibility(hideForUsers, cUsername);
};
// Checks if user is explicitly prevented from viewing a certain section
const checkHiddenability = () => {
if (!currentUser) return true;
const cUsername = currentUser.user.toLowerCase();
const showForUsers = displayData.showForUsers || [];
if (showForUsers.length < 1) return true;
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
const checkIfHideForGuest = () => {
const hideForGuest = displayData.hideForGuests;
return !(hideForGuest && isGuest);
};
return checkVisibility()
&& checkHiddenability()
&& checkIfHideForGuest()
&& checkKeycloakVisibility()
&& checkKeycloakHiddenability();
};
import { isVisibleToUser } from '@/utils/IsVisibleToUser';
/* Putting it all together, the function to export */
const checkSectionVisibility = (sections) => {
export const checkSectionVisibility = (sections) => {
const currentUser = getCurrentUser(); // Get current user object
const isGuest = isLoggedInAsGuest(); // Check if current user is a guest
return sections.filter((currentSection) => {
const displayData = currentSection.displayData || {};
return isSectionVisibleToUser(displayData, currentUser, isGuest);
return isVisibleToUser(displayData, currentUser, isGuest);
});
};

View File

@ -778,6 +778,90 @@
"type": "string",
"description": "The destination to navigate to when item is clicked, expressed as a valid URL, IP or hostname"
},
"displayData": {
"title": "Display Data",
"type": "object",
"additionalProperties": false,
"description": "Optional meta data for customizing an item",
"properties": {
"hideForUsers": {
"title": "Hide for Users",
"type": "array",
"description": "Item 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 item"
}
},
"showForUsers": {
"title": "Show for Users",
"type": "array",
"description": "Item 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 item"
}
},
"hideForGuests": {
"title": "Hide for Guests?",
"type": "boolean",
"default": false,
"description": "If set to true, item 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 item",
"additionalProperties": false,
"properties": {
"groups": {
"title": "Show for Groups",
"type": "array",
"description": "Item 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 item"
}
},
"roles": {
"title": "Show for Roles",
"type": "array",
"description": "Item 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 item"
}
}
}
},
"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 item",
"additionalProperties": false,
"properties": {
"groups": {
"title": "Hide for Groups",
"type": "array",
"description": "Item 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 item"
}
},
"roles": {
"title": "Hide for Roles",
"type": "array",
"description": "Item 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 item"
}
}
}
}
}
},
"target": {
"title": "Opening Method",
"type": "string",

View File

@ -0,0 +1,76 @@
/**
* A helper function that filters all the sections or an item based on current users permissions
* Checks each sections displayData for hideForUsers, showForUsers and hideForGuests
* Returns an array of sections that the current logged in user has permissions for
*/
// Import helper functions from auth, to get current user, and check if guest
import { localStorageKeys } from '@/utils/defaults';
/* Helper function, checks if a given testValue is found in the visibility list */
const determineVisibility = (visibilityList, testValue) => {
let isFound = false;
visibilityList.forEach((visibilityItem) => {
if (visibilityItem.toLowerCase() === testValue.toLowerCase()) isFound = true;
});
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 the displayData of a section/item
should not be rendered for the current user/ guest */
export const isVisibleToUser = (displayData, currentUser, isGuest) => {
// Checks if user explicitly has access to a certain section
const checkVisibility = () => {
if (!currentUser) return true;
const hideForUsers = displayData.hideForUsers || [];
const cUsername = currentUser.user.toLowerCase();
return !determineVisibility(hideForUsers, cUsername);
};
// Checks if user is explicitly prevented from viewing a certain section/item
const checkHiddenability = () => {
if (!currentUser) return true;
const cUsername = currentUser.user.toLowerCase();
const showForUsers = displayData.showForUsers || [];
if (showForUsers.length < 1) return true;
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/item allows for guests
const checkIfHideForGuest = () => {
const hideForGuest = displayData.hideForGuests;
return !(hideForGuest && isGuest);
};
return checkVisibility()
&& checkHiddenability()
&& checkIfHideForGuest()
&& checkKeycloakVisibility()
&& checkKeycloakHiddenability();
};
export default isVisibleToUser;