Allow User Access Control of D2 Pages

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

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

  • 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 - :warning: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

Example Flow

D2 User Access Control Flow.json (77.3 KB)
or :
copy from PR on Github in Example Flow section. (full flow doesn't fit in this post)

  • 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

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

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.

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

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.

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:

{
    // --- 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": ...,
}

Got it, will do!

1 Like

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