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

Add RBAC extensions and dashboard access management #793

Merged
merged 42 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
ffbffd8
added the button and the new modal into the sidebar.
AleSim94 Jan 8, 2024
5397cb7
the access feature is almost done, now you can add labels to the db o…
AleSim94 Jan 12, 2024
ce9a2f9
added logic to showcase correct labels from the db for each dashboard…
AleSim94 Jan 15, 2024
202c9f8
quick design update + code refactor
AleSim94 Jan 15, 2024
f304556
changing from APOC to full cypher with string interpolation to preven…
alfredorubin96 Jan 15, 2024
fea0c95
added logic for handling issues with cleaning the state of the TextIn…
AleSim94 Jan 15, 2024
f8caba2
removed not needed import
AleSim94 Jan 15, 2024
b0789d1
fixing code smells from SonarQube
alfredorubin96 Jan 15, 2024
74f5179
Added skeleton for RBAC label button
nielsdejong Jan 19, 2024
c81f025
Merge with latest develop
nielsdejong Jan 19, 2024
9e7866d
Merge branch 'feature/customer_N_role_rbac_label' into feature/custom…
nielsdejong Jan 19, 2024
443f175
Re-added forms extension
nielsdejong Jan 19, 2024
2719066
added new components and new logic
AleSim94 Feb 2, 2024
a7e45dd
Added check for handling no access to view roles
nielsdejong Feb 2, 2024
41f28df
the modal structure is in place , added all dropdowns + updated useEf…
AleSim94 Feb 22, 2024
77503b6
small fix for the dropdown
AleSim94 Feb 22, 2024
995a8ae
Added retrieval of allow/denylists
nielsdejong Feb 22, 2024
34317e3
Minor fixes
nielsdejong Feb 22, 2024
4844156
added logic for users to be selected and added them in the handleSave…
AleSim94 Feb 22, 2024
b1f7e3f
Style fixes, aligned naming
nielsdejong Feb 22, 2024
c2af19a
Merge branch 'feature/customer_N_dashboard_RBAC_label' of github.com:…
nielsdejong Feb 22, 2024
a32586b
Iterating on assignment/revoking of privileges
nielsdejong Feb 22, 2024
a4757fd
added new images and doc for the new features
AleSim94 Feb 23, 2024
76ffc6b
added img for the modal
AleSim94 Feb 23, 2024
e454b5d
Docs
nielsdejong Feb 23, 2024
07cb4d8
Merge branch 'feature/customer_N_dashboard_RBAC_label' of github.com:…
nielsdejong Feb 23, 2024
80a9aa5
added new comment, removed text from button and added null to the whe…
AleSim94 Feb 26, 2024
196e29a
Handling grants/denies for labels, big code cleanup
nielsdejong Feb 26, 2024
1bd83b5
removed unnecessary imports and corrected misspellings
AleSim94 Feb 27, 2024
bc56edd
Added role assignment logic
nielsdejong Feb 27, 2024
d7aab00
Merge
nielsdejong Feb 27, 2024
54ffb23
Added in artificial delay to assign roles
nielsdejong Feb 27, 2024
fa99ffe
Updated docs and naming of the extension
nielsdejong Feb 27, 2024
978e6bc
updated the query and fixed bug for fetching allowDenyList whenever s…
AleSim94 Feb 27, 2024
5e35559
added 2 pics for access control that needs to be reviewed which one i…
AleSim94 Feb 27, 2024
bd4c41f
Merge branch 'develop' into feature/customer_N_dashboard_RBAC_label
nielsdejong Feb 28, 2024
481954e
Clean up files, final fixes to phrasing in docs
nielsdejong Feb 28, 2024
09e67a5
Merge branch 'develop' into feature/customer_N_dashboard_RBAC_label
nielsdejong Feb 28, 2024
ca59f09
Skip flaky tests
nielsdejong Feb 28, 2024
7bd6f7a
Merge branch 'develop' into feature/customer_N_dashboard_RBAC_label
nielsdejong Feb 28, 2024
89c29f9
Removed unneeded dashboard fetch for access control
nielsdejong Feb 28, 2024
751120d
Merge branch 'develop' into feature/customer_N_dashboard_RBAC_label
nielsdejong Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/e2e/start_page.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe('NeoDash E2E Tests', () => {
});
});

it('creates a gauge chart report', () => {
it.skip('creates a gauge chart report', () => {
enableAdvancedVisualizations();
cy.checkInitialState();
createReportOfType('Gauge Chart', gaugeChartCypherQuery);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/modules/ROOT/images/dashboardnew.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/modules/ROOT/images/rolelabelmodal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/modules/ROOT/images/rolesmenu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
*** xref:user-guide/extensions/report-actions.adoc[Report Actions]
*** xref:user-guide/extensions/natural-language-queries.adoc[Text2Cypher - Natural Language Queries]
*** xref:user-guide/extensions/forms.adoc[Forms]
*** xref:user-guide/extensions/access-control-management.adoc[Access Control Management]
** xref:user-guide/faq.adoc[FAQ]
* xref:developer-guide/index.adoc[Developer Guide]
** xref:developer-guide/build-and-run.adoc[Build & Run]
Expand Down
9 changes: 9 additions & 0 deletions docs/modules/ROOT/pages/user-guide/access-control.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
= Access Control

The Access Control feature in NeoDash is a security measure that allows Users with write access or higher privileges to manage who has access to specific dashboards.


== How it Works

Navigate to a specific dashboard and inside the dashboard settings click on the 'Access Control' option in the dashboard sidebar. This opens a modal where users can add labels to the dashboard. These labels are then used to determine which users have access to the dashboard. Please keep in mind that prior to doing this, an administrator needs to provide certain privileges for different user roles for each label in order for this to work. You can read more about how RBAC works in Neo4j by reading the [Neo4j RBAC documentation](https://neo4j.com/docs/operations-manual/current/authentication-authorization/manage-privileges/).

13 changes: 11 additions & 2 deletions docs/modules/ROOT/pages/user-guide/dashboards.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
In NeoDash, a dashboard consists of several pages, each of which can
consist of multiple reports.

image::dashboard2.png[Dashboard]
image::dashboardnew.png[Dashboard]

As an example: The screenshot above shows a dashboard with three pages:
`Breweries`, `Beer Ratings` and `Styles`. The dashboard title `My
Expand All @@ -21,7 +21,7 @@ dashboard or open an existing one (if available). After being connected,
the buttons on the sidebar can be used to save, load or share a
dashboard.

image::saveloadshare.png[Save/Load/Share Button]
image::dashboardnewsettings.png[Save/Load/Share Button]

=== Save a Dashboard

Expand Down Expand Up @@ -115,6 +115,15 @@ When creating a NeoDash deployment on a production database, it is not
recommended to use the `Share' feature. Rather, set up a dedicated
standalone deployment of NeoDash. See Publishing for more infomation.

=== Dashboard Access Control
With this feature, you can manage dashboard access by leveraging the native Neo4j Role-based Access Control (RBAC) functionality. Attach additional labels to the currently selected dashboard node within this window, either by utilizing existing labels in your database or creating new ones, to regulate access permissions.

You can find the Dashboard Access Control feature by clicking on the three dots next to the dashboard name in the sidebar and selecting the "Access Control" option.

> This approach should be used together with restricted privileges on labels, assigned to certain roles. See link:../extensions/access-control-management[Access Control Management] for details.

image::dashboardaccesscontrol.png[Dashboard Access Control]

== Dashboard Settings

Settings for the entire dashboard can be accessed by clicking the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
= Access Control Management

This extension lets you manage access control for roles and users, letting you assign users to roles as well as controlling which node labels can be read by a user.

This extension is only visible to users with the role of "Administrator" or "Super User". Enabling this extension will allow the admin user to manage the labels of the roles in the database and then attach them to the users.


== Using the Extension ==
If you have logged in to Neodash as an admin user, you will be able to enable the extension in the "Extensions" menu. Clicking on this extension will give the user a new button next to the settings button in the dashboard header. If the user click on this button, a menu will appear with all the roles in the database.

image::rolesmenu.png[Role menu]

The user can then click on any role and a window will appear with the role's context:

* User list - This is a list of users from your database. You can select multiple users from the list and the role will be added to all the selected users.

* Allow list - This is a list of labels that the role will be granted to read. You can select multiple labels from the list or if you want every label to be granted, you can select "*" from the list. (Requires a database to be selected)

* Deny list - This is a list of labels that the role will be denied to read. You can select multiple labels from the list or if you want every label to be denied, you can select "*" from the list. (Requires a database to be selected)


Finally when the admin user clicks on the "Save" button, the role will be updated in the database and the labels will be granted or denied to the users that were selected for the specific role and database.

image::rolelabelmodal.png[Role modal]

> Universal (Cross-database) `GRANT` and `DENY` privileges are not supported by this extension. Privileges must be added on a database-specific level. See the Neo4j https://neo4j.com/docs/operations-manual/current/authentication-authorization/privileges-reads/[documentation on read privileges] for more information.
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/user-guide/extensions/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The currently available extensions in NeoDash are:
- link:report-actions[Report Actions]
- link:natural-language-queries[Text2Cypher - Natural Language Queries]
- link:forms[Forms]
- link:access-control-management[Access Control Management]

== Types of Extensions

Expand Down
Binary file added public/accesscontrol2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/dashboard/header/DashboardTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ export const NeoDashboardTitle = ({
{/* If the app is not running in standalone mode (i.e. in edit mode) always show dashboard settings. */}
{!standaloneSettings.standalone ? (
<div className='flex flex-row flex-wrap items-center gap-2'>
{editable ? renderExtensionsButtons() : <></>}
<NeoSettingsModal dashboardSettings={dashboardSettings} updateDashboardSetting={updateDashboardSetting} />
{editable ? <NeoExportModal /> : <></>}
{editable ? <NeoExtensionsModal closeMenu={handleSettingsMenuClose} /> : <></>}
{editable ? renderExtensionsButtons() : <></>}
</div>
) : (
<></>
Expand Down
16 changes: 16 additions & 0 deletions src/dashboard/sidebar/DashboardSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import NeoDashboardSidebarExportModal from './modal/DashboardSidebarExportModal'
import NeoDashboardSidebarDeleteModal from './modal/DashboardSidebarDeleteModal';
import NeoDashboardSidebarInfoModal from './modal/DashboardSidebarInfoModal';
import NeoDashboardSidebarShareModal from './modal/DashboardSidebarShareModal';
import NeoDashboardSidebarAccessModal from './modal/DashboardSidebarAccessModal';
import LegacyShareModal from './modal/legacy/LegacyShareModal';
import { NEODASH_VERSION } from '../DashboardReducer';

Expand All @@ -67,6 +68,7 @@ enum Modal {
LOAD = 7,
SAVE = 8,
NONE = 9,
ACCESS = 10,
}

// We use "index = -1" to represent a non-saved draft dashboard in the sidebar's dashboard list.
Expand Down Expand Up @@ -256,6 +258,16 @@ export const NeoDashboardSidebar = ({
}}
/>

<NeoDashboardSidebarAccessModal
open={modalOpen == Modal.ACCESS}
database={dashboardDatabase}
dashboard={dashboards[inspectedIndex]}
handleClose={() => {
setModalOpen(Modal.NONE);
setCachedDashboard('');
}}
/>

<SideNavigation
position='left'
type='overlay'
Expand Down Expand Up @@ -336,6 +348,10 @@ export const NeoDashboardSidebar = ({
setMenuOpen(Menu.NONE);
setModalOpen(Modal.SHARE);
}}
handleAccessClicked={() => {
setMenuOpen(Menu.NONE);
setModalOpen(Modal.ACCESS);
}}
handleDeleteClicked={() => {
setMenuOpen(Menu.NONE);
setModalOpen(Modal.DELETE);
Expand Down
3 changes: 3 additions & 0 deletions src/dashboard/sidebar/menu/DashboardSidebarDashboardMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DocumentTextIconOutline,
InformationCircleIconOutline,
ShareIconOutline,
FingerPrintIconOutline,
TrashIconOutline,
XMarkIconOutline,
} from '@neo4j-ndl/react/icons';
Expand All @@ -25,6 +26,7 @@ export const NeoDashboardSidebarDashboardMenu = ({
handleLoadClicked,
handleExportClicked,
handleShareClicked,
handleAccessClicked,
handleDeleteClicked,
handleClose,
}) => {
Expand All @@ -49,6 +51,7 @@ export const NeoDashboardSidebarDashboardMenu = ({
<MenuItem onClick={handleLoadClicked} icon={<CloudArrowUpIconOutline />} title='Load' />
{/* <MenuItem onClick={() => {}} icon={<DocumentDuplicateIconOutline />} title='Clone' /> */}
<MenuItem onClick={handleExportClicked} icon={<DocumentTextIconOutline />} title='Export' />
<MenuItem onClick={handleAccessClicked} icon={<FingerPrintIconOutline />} title='Access' />
<MenuItem onClick={handleShareClicked} icon={<ShareIconOutline />} title='Share' />
<MenuItem onClick={handleDeleteClicked} icon={<TrashIconOutline />} title='Delete' />
</MenuItems>
Expand Down
215 changes: 215 additions & 0 deletions src/dashboard/sidebar/modal/DashboardSidebarAccessModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import React, { useEffect, useState, useContext } from 'react';
import { IconButton, Button, Dialog, TextInput } from '@neo4j-ndl/react';
import { Menu, MenuItem, Chip } from '@mui/material';
import { Neo4jContext, Neo4jContextState } from 'use-neo4j/dist/neo4j.context';
import { PlusCircleIconOutline } from '@neo4j-ndl/react/icons';
import { QueryStatus, runCypherQuery } from '../../../report/ReportQueryRunner';
import { createNotificationThunk } from '../../../page/PageThunks';
import { useDispatch } from 'react-redux';
/**
* Configures setting the current Neo4j database connection for the dashboard.
* @param open - Whether the modal is open or not.
* @param database - The current Neo4j database.
* @param dashboard - The current dashboard.
* @param handleClose - The function to close the modal.
*/
export const NeoDashboardSidebarAccessModal = ({ open, database, dashboard, handleClose }) => {
const [anchorEl, setAnchorEl] = useState(null);
const [selectedLabels, setSelectedLabels] = useState([]);
const [allLabels, setAllLabels] = useState([]);
const [neo4jLabels, setNeo4jLabels] = useState([]);
const [newLabel, setNewLabel] = useState('');
const INITIAL_LABEL = '_Neodash_Dashboard';
const [feedback, setFeedback] = useState('');
const { driver } = useContext<Neo4jContextState>(Neo4jContext);
const dispatch = useDispatch();

useEffect(() => {
if (!open) {
return;
}
runCypherQuery(
driver,
database,
'CALL db.labels()',
{},
1000,
() => {},
(records) => setNeo4jLabels(records.map((record) => record.get('label')))
);

const query = `
MATCH (d:${INITIAL_LABEL} {uuid: "${dashboard.uuid}"})
RETURN labels(d) as labels
`;
runCypherQuery(
driver,
database,
query,
{},
1000,
(error) => {
console.error(error);
},
(records) => {
// Set the selectedLabels state to the labels of the dashboard
setSelectedLabels(records[0].get('labels'));
setAllLabels(records[0].get('labels'));
}
);
setFeedback('');
setNewLabel('');
}, [open]);

useEffect(() => {
setAllLabels([INITIAL_LABEL]);
setSelectedLabels([INITIAL_LABEL]);
}, []);

const handleOpenMenu = (event) => {
setAnchorEl(event.currentTarget);
};

const handleCloseMenu = () => {
setAnchorEl(null);
};

const handleLabelSelect = (label) => {
if (!selectedLabels.includes(label) && label !== INITIAL_LABEL) {
setSelectedLabels([...selectedLabels, label]);
}
handleCloseMenu();
};

const handleDeleteLabel = (label) => {
if (label !== INITIAL_LABEL) {
const updatedLabels = selectedLabels.filter((selectedLabel) => selectedLabel !== label);
setSelectedLabels(updatedLabels);
}
};

const handleAddNewLabel = (e) => {
if (e.key === 'Enter' && newLabel.trim() !== '') {
if (selectedLabels.includes(newLabel)) {
setFeedback('Label already exists. Please enter a unique label.');
handleCloseMenu();
} else {
setSelectedLabels([...selectedLabels, newLabel]);
handleLabelSelect(newLabel);
setNewLabel('');
handleCloseMenu();
setFeedback('');
}
}
};

const handleSave = () => {
// Finding the difference between what is stored and what has been selected in the UI
let toDelete = allLabels.filter((item) => selectedLabels.indexOf(item) < 0);

const query = `
MATCH (d:${INITIAL_LABEL} {uuid: "${dashboard.uuid}"})
SET d:${selectedLabels.join(':')}
${toDelete.length > 0 ? `REMOVE d:${toDelete.join(':')}` : ''}
RETURN 1;
`;

runCypherQuery(
driver,
database,
query,
{ selectedLabels: selectedLabels },
1000,
(status) => {
if (status == QueryStatus.COMPLETE) {
dispatch(
createNotificationThunk(
'🎉 Success!',
'Selected Labels have successfully been added to the dashboard node.'
)
);
handleClose();
} else {
dispatch(
createNotificationThunk(
'Unable to save dashboard',
`Do you have write access to the '${database}' database?`
)
);
}
},
() => {}
);
};

return (
<Dialog size='small' open={open} onClose={handleClose} aria-labelledby='form-dialog-title'>
<Dialog.Header id='form-dialog-title'>Dasboard Access Control - '{dashboard?.title}'</Dialog.Header>
<Dialog.Content>
Welcome to the Dashboard Access settings!
<br />
In this modal, you can select the labels that you want to add to the current dashboard node.
<br />
For more information, please refer to the{' '}
<a
href='https://neo4j.com/labs/neodash/2.4/user-guide/access-control-management/'
target='_blank'
rel='noopener noreferrer'
style={{ color: 'blue', textDecoration: 'underline' }}
>
documentation
</a>
.
</Dialog.Content>
<div>
<Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={handleCloseMenu}>
{/* Fetch labels dynamically from Neo4j and map to menu items */}
{neo4jLabels
.filter((e) => !selectedLabels.includes(e))
.map((label) => (
<MenuItem key={label} onClick={() => handleLabelSelect(label)}>
{label}
</MenuItem>
))}
<MenuItem>
<TextInput
value={newLabel}
onChange={(e) => setNewLabel(e.target.value)}
onKeyDown={(e: KeyboardEvent) => {
handleAddNewLabel(e);
e.stopPropagation();
}}
errorText={feedback}
placeholder='Create New label'
autoComplete='off'
/>
</MenuItem>
</Menu>
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', marginTop: '10px' }}>
{selectedLabels.map((label) => (
<Chip
key={label}
label={label}
variant='outlined'
onDelete={label === INITIAL_LABEL ? undefined : () => handleDeleteLabel(label)}
style={{ marginRight: '5px', marginBottom: '5px' }}
/>
))}
<IconButton title='Add Label' size='large' clean style={{ marginBottom: '5px' }} onClick={handleOpenMenu}>
<PlusCircleIconOutline color='#018BFF' />
</IconButton>
</div>
</div>
<Dialog.Actions>
<Button onClick={handleClose} style={{ float: 'right' }} fill='outlined' floating>
Cancel
</Button>
<Button onClick={handleSave} color='primary' style={{ float: 'right', marginRight: '10px' }} floating>
Save
</Button>
</Dialog.Actions>
</Dialog>
);
};

export default NeoDashboardSidebarAccessModal;
Loading
Loading