# Allow User Access Control of D2 Pages

**URL:** https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100
**Category:** Dashboard
**Tags:** node-red-dashboard, dashboard-2
**Created:** [11 August 2024 02:13 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100 "2024-08-11T02:13:26Z")
**Posts on this page:** 6
**Page:** 1

<div class="post-metadata">

### Author: ![cgjgh](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/cgjgh/32/99867_2.png) [@cgjgh](https://discourse.nodered.org/u/cgjgh)
#### Post date: [11 August 2024 02:13 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100/1 "2024-08-11T02:13:26Z")

</div>

My Node-RED apps using D2 often need to be accessed by multiple users, and I've often found the need for a way to easily control access to pages via the UI. I've created a work in progress PR that adds User Allowed Page filtering by utilizing user credentials provided by auth plugins. Would highly appreciate any feedback, suggestions or pointers from the more experienced people here.

[Full PR info here](https://github.com/FlowFuse/node-red-dashboard/pull/1160)

## Description

As said, this is more a proof-of-concept PR for user page filtering using auth plugins. With my tests this seems to have been quite robust, looking for additional feedback.  
Still needs to be decided which direction to take here, whether this functionality should be integrated into D2 directly, added as a plugin or as a 3rd party node.

## Working Principle

- D2 will function exactly as usual unless `global.store.enablePageFilter` with context `file` is set to `true`.

- For now, UI for Admin Management of users, groups and pages is just a Node-RED flow using D2 widgets.

- Users and groups are saved to Node-RED `file` context storage so no database is needed. User allowed pages are then determined on a group basis and saved to and retrieved from `global.store[{user}].allowedPages` in `memory only` context.

- On `ui-base` `emitConfig()` all pages, groups and widgets are filtered based on the page they reside in or if they are UI scoped. Therefore, only the allowed pages, groups, and widgets are emitted in the config to the connected dashboards. This could also have added benefit of improved load speeds.

- Includes an optional zero-trust filtering approach where all users are denied access to all pages by default, or alternatively, enabling filtering for a specific user only if they have been added to user list (_default?_).

- D2 pages are automatically populated as form options to `ui-form` for adding Groups in Admin Management UI, same with groups when adding User

- Uses `msg._client.user.email` as user ID, will need to arrive at some sort of standardization across auth plugins concerning this

- Any users logged in using an auth provider will show up as detected user in UI, which you can then assign groups to.

## Usage

- Use this branch of D2: [https://github.com/cgjgh/node-red-dashboard/tree/User-Page-Filtering](https://github.com/cgjgh/node-red-dashboard/tree/User-Page-Filtering)

- Attached below is a Node-RED flow with the necessary D2 UI for Admin Management of users, groups and pages.

- Integrated into the PR is a test mode which you can toggle in UI and impersonate a specific user, therefore no auth plugins are required for testing.

- New groups and users can be added with the included `ui-form`.

- Table rows can be selected to load a form to edit user or group

- Since D2 `ui-table` does not yet support dynamic props, included in the flow below are `inject` nodes with functions to generate JSON for each table with all columns for pages or groups predefined. This you can then copy from debug window and import and replace the existing tables in flow, or alternatively edit the table node.

### Example UI for Admin Management

#### Controls

- Enable Page Filtering - turn filtering on or off
- Enable Zero Trust - turn `on` to deny access to all pages by default, `off` to enable filtering only for users existing in user list (prevents locking out users while in development)
- Enable Test Mode - impersonate a specific user to check page access, current user is defined by the dropdown or textbox - **⚠Test mode settings are globally applied to _anyone_ using D2, needs to be turned off during normal use**
- Dropdown allows selecting users existing in user list
- Textbox allows setting user not existing in user list

#### Tables

- First table shows users and what groups they are part of
- Second table shows users and what pages they are allowed access to
- Third table shows users detected using dashboard, and whether they exist in user list
- Fourth table shows groups and what pages are part of the group

 ![Screenshot 2024-08-07 204214](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/c/3/c34b8afa1e02f360f19fe35f8007d2960b6a3e76.jpeg)

### Example Flow

[D2 User Access Control Flow.json](https://discourse.nodered.org/uploads/short-url/im63HajBy2LsTutIjc1SXeIQSM2.json) (77.3 KB)  
or :  
copy from [PR on Github](https://github.com/FlowFuse/node-red-dashboard/pull/1160) in Example Flow section. (full flow doesn't fit in this post)

---

<div class="post-metadata">

### Author: ![joepavitt](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/joepavitt/32/59722_2.png) [@joepavitt](https://discourse.nodered.org/u/joepavitt)
#### Post date: [11 August 2024 17:36 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100/2 "2024-08-11T17:36:47Z")

</div>

> - Uses `msg._client.user.email` as user ID, will need to arrive at some sort of standardization across auth plugins concerning this

This bit may be tricky, as different auth providers will provide slight variant of data structures. I believe in another thread we've agreed to have `user.userId` as an _always_ present standard as a unique identifier for a user.

> - D2 will function exactly as usual unless `global.store.enablePageFilter` with context `file` is set to `true`.

_If_ this goes into core, which it's difficult to justify at the moment, this can end up as a sidebar configuration option, rather than relying on global context.

Overall thoughts - amazing usability, but feels like a plugin/addon, and something that should live within Node-RED UI, rather than exposed in a Dashboard? The advantage of using Dashboard widgets/nodes to build the UI vs raw jQuery though is decision I wholeheartedly understand

---

<div class="post-metadata">

### Author: ![cgjgh](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/cgjgh/32/99867_2.png) [@cgjgh](https://discourse.nodered.org/u/cgjgh)
#### Post date: [12 August 2024 02:11 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100/3 "2024-08-12T02:11:30Z")

</div>

> [@joepavitt](#):
>
> This bit may be tricky, as different auth providers will provide slight variant of data structures. I believe in another thread we've agreed to have `user.userId` as an _always_ present standard as a unique identifier for a user.

No problem, this can be easily changed to `userId`. Just settled on `email` for human readability rather than an ID string of random characters.

> [@joepavitt](#):
>
> _If_ this goes into core, which it's difficult to justify at the moment, this can end up as a sidebar configuration option, rather than relying on global context.

Absolutely, just wanted to keep it as light weight and independent as possible since we're unsure if this should be a plugin or core.

> [@joepavitt](#):
>
> feels like a plugin/addon, and something that should live within Node-RED UI, rather than exposed in a Dashboard

Is that due to security concerns? ...as that has crossed my mind as well.

> [@joepavitt](#):
>
> feels like a plugin/addon

A couple questions if I may:

1. If this were to be made into a plugin, would it be possible to make a hook available like `onEmitConfig()` that would allow plugins to modify the D2 config sent to users?
2. How would you recommend passing list of D2 pages to plugin
3. What storage options (temporary and persistent, D2 built in storage or not) would be best suited for a plugin like this, preferably that can also be accessed within Node-RED editor.

---

<div class="post-metadata">

### Author: ![TotallyInformation](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/totallyinformation/32/31_2.png) [@TotallyInformation](https://discourse.nodered.org/u/TotallyInformation)
#### Post date: [12 August 2024 07:52 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100/4 "2024-08-12T07:52:38Z")

</div>

> [@cgjgh](#):
>
> Uses `msg._client.user.email` as user ID, will need to arrive at some sort of standardization across auth plugins concerning this

It would be better to stick to the agreed format of the msg.\_client as shown below. Note that this is from the next release of UIBUILDER but we agreed that D2 and UIBUILDER would use essencially the same format.:

* * *

## msg.\_client properties

The content of `msg._client` will vary slightly depending on the external tool doing the authentication but it has a number of key properties and some optional ones:

```auto
{
    // --- REQUIRED ---
    // One of `Cloudflare Access`, `FlowFuse`, `Keycloak`, `Authentik`, 
    // `Custom`, or `Proxied Custom`. Others may be added in the future.
    "provider": authProvider,
    // Derived from specific tooling headers or the more generic
    // 'remote-user', 'x-remote-user', 'x-forwarded-user', or 'x-user-id' headers
    "userId": userID,
    // The Socket.IO socket id (which changes on each (re)connection)
    "socketId": socket.id,
    // User email address derived from specific tooling headers or the more generic
    // 'remote-email' or 'x-user-email' headers
    "email": email,
    // Users name derived from specific tooling headers or the more generic
    // 'remote-name', or 'x-remote-name' headers
    "name": name,
    // uibuilder performs checks on various headers to attempt to get
    // the actual client source IP address. The process used can be
    // reviewed at https://github.com/pbojinov/request-ip/blob/master/src/index.js
    "ip": realClientIP,
    // The client browser's user agent string
    "agent": headers['user-agent'],
    // The server's host name/ip and optionally the port number for the request.
    "host": headers['host'],

    // --- OPTIONAL ---
    // Authorisation groups that the user belongs to. Authelia & Authentik,
    // or anything else that sets the 'x-forwarded-groups' header
    "groups": "",
    // Authorisation roles that the user belongs to.
    // Anything that sets the 'x-user-role' header
    "role": "",
    // Only for Authentik
    "username": "",
    // Only for FlowFuse, user icon image
    "image": ...,
}

```

---

<div class="post-metadata">

### Author: ![cgjgh](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/cgjgh/32/99867_2.png) [@cgjgh](https://discourse.nodered.org/u/cgjgh)
#### Post date: [12 August 2024 12:04 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100/5 "2024-08-12T12:04:55Z")

</div>

> [@TotallyInformation](#):
>
> It would be better to stick to the agreed format of the msg.\_client as shown below.

Got it, will do!

---

<div class="post-metadata">

### Author: ![system](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/1X/d073cd938eafa2e558d7c2cd59003b3ef4963033.png) [@system](https://discourse.nodered.org/u/system)
#### Post date: [11 September 2024 12:05 UTC](https://discourse.nodered.org/t/allow-user-access-control-of-d2-pages/90100/6 "2024-09-11T12:05:53Z")

</div>

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.
