Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Adds support for guest access #167

Merged
merged 10 commits into from
Aug 18, 2021
4 changes: 4 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## ✨ 1.6.2 - Support for Guest Access [PR #167](https://github.com/Lissy93/dashy/pull/167)
- Adds functionality for optional read-only guest access to dashboards with authentication
- Can be enabled by setting `appConfig.enableGuestAccess: true`

## 💄 1.6.1 - Adds new Theme [PR #166](https://github.com/Lissy93/dashy/issues/166)
- Adds Dashy theme, for use in the dev dashboard
## ✨ 1.5.9 - New Minimal/ Startpage View [PR #155](https://github.com/Lissy93/dashy/issues/155)
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,10 @@ appConfig:
- user: alicia
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
```
At present, access control is handled on the frontend, and therefore in security-critical situations, it is recommended to use an alternate method for authentication, such as [Authelia](https://www.authelia.com/), a VPN or web server and firewall rules.

By default, when authentication is configured no user can access your dashboard without first logging in. If you would like to allow for read-only access by unauthenticated users, then you can enable guest mode, by setting `appConfig.enableGuestAccess: true`.

**Note**: At present, access control is handled on the frontend, and therefore in security-critical situations, it is recommended to use an alternate method for authentication, such as [Authelia](https://www.authelia.com/), a VPN or web server and firewall rules. Instructions for setting this up can be found [in the docs](docs/authentication.md#alternative-authentication-methods).

<p align="center">
<img
Expand Down
3 changes: 3 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ A hash is a one-way cryptographic function, meaning that it is easy to generate
## Logging In and Out
Once authentication is enabled, so long as there is no valid token in cookie storage, the application will redirect the user to the login page. When the user enters credentials in the login page, they will be checked, and if valid, then a token will be generated, and they can be redirected to the home page. If credentials are invalid, then an error message will be shown, and they will remain on the login page. Once in the application, to log out the user can click the logout button (in the top-right), which will clear cookie storage, causing them to be redirected back to the login page.

## 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`.

## 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.

Expand Down
1 change: 1 addition & 0 deletions docs/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ To disallow any changes from being written to disk via the UI config editor, set
**`fontAwesomeKey`** | `string` | _Optional_ | If you have a font-awesome key, then you can use it here and make use of premium icons. It is a 10-digit alpha-numeric string from you're FA kit URL (e.g. `13014ae648`)
**`faviconApi`** | `enum` | _Optional_ | Only applicable if you are using favicons for item icons. Specifies which service to use to resolve favicons. Set to `local` to do this locally, without using an API. Services running locally will use this option always. Available options are: `local`, `faviconkit`, `google`, `clearbit`, `webmasterapi` and `allesedv`. Defaults to `faviconkit`. See [Icons](/docs/icons.md#favicons) for more info
**`auth`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. Note authentication is done on the client side, and so if your instance of Dashy is exposed to the internet, it is recommend to configure your web server to handle this. See [`auth`](#appconfigauth-optional)
**`enableGuestAccess`** | `boolean` | _Optional_ | When set to `true`, an unauthenticated user will be able to access the dashboard, with read-only access, without having to login. Requires `auth` to be configured. Defaults to `false`.
**`layout`** | `enum` | _Optional_ | App layout, either `horizontal`, `vertical`, `auto` or `sidebar`. Defaults to `auto`. This specifies the layout and direction of how sections are positioned on the home screen. This can also be modified from the UI.
**`iconSize`** | `enum` | _Optional_ | The size of link items / icons. Can be either `small`, `medium,` or `large`. Defaults to `medium`. This can also be set directly from the UI.
**`theme`** | `string` | _Optional_ | The default theme for first load (you can change this later from the UI)
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "1.6.1",
"version": "1.6.2",
"license": "MIT",
"main": "server",
"scripts": {
Expand Down
16 changes: 12 additions & 4 deletions src/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
"error-incorrect-username": "User not found",
"error-incorrect-password": "Incorrect Password",
"success-message": "Logging in...",
"logout-message": "Logged Out"
"logout-message": "Logged Out",
"already-logged-in-title": "Already Logged In",
"already-logged-in-text": "You're logged in as",
"proceed-to-dashboard": "Proceed to Dashboard",
"log-out-button": "Logout",
"proceed-guest-button": "Proceed as Guest"
},
"config": {
"main-tab": "Main Menu",
Expand Down Expand Up @@ -67,7 +72,9 @@
"item-size-large": "Large",
"config-launcher-label": "Config",
"config-launcher-tooltip": "Update Configuration",
"sign-out-tooltip": "Sign Out"
"sign-out-tooltip": "Sign Out",
"sign-in-tooltip": "Log In",
"sign-in-welcome": "Hello {username}!"
},
"updates": {
"app-version-note": "Dashy version",
Expand Down Expand Up @@ -109,7 +116,8 @@
"error-msg-save-mode": "Please select a Save Mode: Local or File",
"error-msg-cannot-save": "An error occurred saving config",
"error-msg-bad-json": "Error in JSON, possibly malformed",
"warning-msg-validation": "Validation Warning"
"warning-msg-validation": "Validation Warning",
"not-admin-note": "You cannot write changed to disk, because you are not logged in as an admin"
},
"app-rebuild": {
"title": "Rebuild Application",
Expand Down Expand Up @@ -154,4 +162,4 @@
"modal": "Open in Pop-Up Modal",
"workspace": "Open in Workspace View"
}
}
}
7 changes: 7 additions & 0 deletions src/components/Configuration/JsonEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
{{saveSuccess
? $t('config-editor.status-success-msg') : $t('config-editor.status-fail-msg') }}
</p>
<p v-if="!allowWriteToDisk" class="no-permission-note">
{{ $t('config-editor.not-admin-note') }}
</p>
<p class="response-output">{{ responseText }}</p>
<p v-if="saveSuccess" class="response-output">
{{ $t('config-editor.success-note-l1') }}
Expand Down Expand Up @@ -243,6 +246,10 @@ p.response-output {
}
}

p.no-permission-note {
color: var(--config-settings-color);
}

button.save-button {
padding: 0.5rem 1rem;
margin: 0.25rem auto;
Expand Down
62 changes: 0 additions & 62 deletions src/components/Settings/AppButtons.vue

This file was deleted.

85 changes: 85 additions & 0 deletions src/components/Settings/AuthButtons.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<template>
<div>
<!-- If auth configured, show status text -->
<span class="user-type-note">{{ makeText() }}</span>
<div class="display-options">
<!-- If user logged in, show logout button -->
<IconLogout
v-if="userType == userStateEnum.loggedIn"
@click="logout()"
v-tooltip="tooltip($t('settings.sign-out-tooltip'))"
class="layout-icon" tabindex="-2"
/>
<!-- If not logged in, and gues mode enabled, show login button -->
<IconLogout
v-if="userType == userStateEnum.guestAccess"
@click="goToLogin()"
v-tooltip="tooltip($t('settings.sign-in-tooltip'))"
class="layout-icon" tabindex="-2"
/>
</div>
</div>
</template>

<script>
import router from '@/router';
import { logout as registerLogout } from '@/utils/Auth';
import { localStorageKeys, userStateEnum } from '@/utils/defaults';
import IconLogout from '@/assets/interface-icons/user-logout.svg';

export default {
name: 'AuthButtons',
components: {
IconLogout,
},
props: {
userType: Number,
},
data() {
return {
userStateEnum,
};
},
methods: {
logout() {
registerLogout();
this.$toasted.show(this.$t('login.logout-message'));
setTimeout(() => {
router.push({ path: '/login' });
}, 500);
},
goToLogin() {
router.push({ path: '/login' });
},
tooltip(content) {
return { content, trigger: 'hover focus', delay: 250 };
},
makeText() {
if (this.userType === userStateEnum.loggedIn) {
const username = localStorage[localStorageKeys.USERNAME];
return this.$t('settings.sign-in-welcome', { username });
}
if (this.userType === userStateEnum.guestAccess) {
return this.$t('settings.sign-in-tooltip');
}
return '';
},
},
};
</script>

<style scoped lang="scss">
@import '@/styles/style-helpers.scss';

span.user-type-note {
color: var(--settings-text-color);
text-transform: capitalize;
margin-right: 0.5rem;
}

.display-options {
@extend .svg-button;
color: var(--settings-text-color);
}

</style>
24 changes: 18 additions & 6 deletions src/components/Settings/SettingsContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
@modalChanged="modalChanged" />
<AppButtons v-if="isUserLoggedIn()" />
<AuthButtons v-if="userState != 'noone'" :userType="userState" />
</div>
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
<button @click="toggleSettingsVisibility()"
Expand All @@ -34,7 +34,7 @@ import ConfigLauncher from '@/components/Settings/ConfigLauncher';
import ThemeSelector from '@/components/Settings/ThemeSelector';
import LayoutSelector from '@/components/Settings/LayoutSelector';
import ItemSizeSelector from '@/components/Settings/ItemSizeSelector';
import AppButtons from '@/components/Settings/AppButtons';
import AuthButtons from '@/components/Settings/AuthButtons';
import KeyboardShortcutInfo from '@/components/Settings/KeyboardShortcutInfo';
import AppInfoModal from '@/components/Configuration/AppInfoModal';
import IconOpen from '@/assets/interface-icons/config-open-settings.svg';
Expand All @@ -44,6 +44,8 @@ import {
visibleComponents as defaultVisibleComponents,
} from '@/utils/defaults';

import { getUserState } from '@/utils/Auth';

export default {
name: 'SettingsContainer',
props: {
Expand All @@ -61,7 +63,7 @@ export default {
ThemeSelector,
LayoutSelector,
ItemSizeSelector,
AppButtons,
AuthButtons,
KeyboardShortcutInfo,
AppInfoModal,
IconOpen,
Expand All @@ -87,9 +89,6 @@ export default {
getInitialTheme() {
return this.appConfig.theme || '';
},
isUserLoggedIn() {
return !!localStorage[localStorageKeys.USERNAME];
},
/* Gets user themes if available */
getUserThemes() {
const userThemes = this.appConfig.cssThemes || [];
Expand All @@ -105,6 +104,19 @@ export default {
|| (this.visibleComponents || defaultVisibleComponents).settings);
},
},
computed: {
/**
* Determines which button should display, based on the user type
* 0 = Auth not configured, don't show anything
* 1 = Auth condifured, and user logged in, show logout button
* 2 = Auth configured, guest access enabled, and not logged in, show login
* 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
*/
userState() {
return getUserState(this.appConfig || {});
},
},
data() {
return {
settingsVisible: this.getSettingsVisibility(),
Expand Down
27 changes: 15 additions & 12 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ import { metaTagData, startingView, routePaths } from '@/utils/defaults';

Vue.use(Router);

/**
* Checks if the current user is either authenticated,
* or if authentication is not enabled
* @returns true if user logged in, or user management not enabled
*/
/* Checks if guest mode is enabled in appConfig */
const isGuestEnabled = () => {
if (!config || !config.appConfig) return false;
return config.appConfig.enableGuestAccess || false;
};

/* 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));
return (!users || users.length === 0 || isLoggedIn(users) || isGuestEnabled());
};

/* Get the users chosen starting view from app config, or return default */
Expand Down Expand Up @@ -94,13 +96,14 @@ const router = new Router({
appConfig: config.appConfig,
},
beforeEnter: (to, from, next) => {
if (isAuthenticated()) router.push({ path: '/' });
// If the user already logged in + guest mode not enabled, then redirect home
if (isAuthenticated() && !isGuestEnabled()) router.push({ path: '/' });
next();
},
},
{ // The about app page
path: routePaths.about,
name: 'about',
name: 'about', // We lazy load the About page so as to not slow down the app
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
meta: makeMetaTags('About Dashy'),
},
Expand All @@ -115,9 +118,9 @@ const router = new Router({
});

/**
* Before loading a route, check if the user has authentication enabled *
* if so, then ensure that they are correctly logged in as a valid user *
* If not logged in, prevent access and redirect them to the login page *
* Before loading a route, check if the user has authentication enabled
* if so, then ensure that they are correctly logged in as a valid user
* If not logged in, prevent all access and redirect them to login page
* */
router.beforeEach((to, from, next) => {
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
Expand All @@ -131,5 +134,5 @@ router.afterEach((to) => {
});
});

// Export the now configured router
// All done - export the now configured router
export default router;
2 changes: 0 additions & 2 deletions src/styles/style-helpers.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@

.svg-button {
color: var(--primary);
width: 1.5rem;
height: 1.5rem;
svg {
path {
fill: var(--settings-text-color);
Expand Down
Loading