Skip to content

Commit

Permalink
V0.13.1 (#137)
Browse files Browse the repository at this point in the history
* fix bug reported by ship where GRAPHS * was being escaped by backticks, inappropriately

* COPY ROLE support for Neo4j 4.0, first commit

* sentry error reporting

* add copy role preview element

* v0.13.1 and release notes
  • Loading branch information
moxious authored Apr 6, 2020
1 parent 3a77b9b commit d53e23a
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 13 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "halin",
"description": "Halin helps you monitor and improve your Neo4j graph",
"version": "0.13.0",
"version": "0.13.1",
"neo4jDesktop": {
"apiVersion": "^1.2.0"
},
Expand Down
7 changes: 7 additions & 0 deletions release-notes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Halin Release Notes

## 0.13.1 Bugfix Release & COPY ROLE

- A bug in the generation of privilege commands was fixed, which could give the wrong results
when the selected graph was `*`
- A "copy role" option was added for Neo4j 4.0 installs; the "copy" icon button in Actions on
the role administration page

## 0.13.0 Limited Alerting

- Added alerts for certain memory conditions, such as heap re-allocation and imminent out-of-memory errors (OOMs)
Expand Down
13 changes: 13 additions & 0 deletions src/api/cluster/ClusterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ export default class ClusterManager {
})
}

copyRole(existingRole, toBeCreatedRole) {
return this.clusterWideQuery(`CREATE ROLE \`${toBeCreatedRole}\` AS COPY OF \`${existingRole}\``)
.then(result => {
this.addEvent({
type: 'addrole',
message: `Created role ${toBeCreatedRole} as copy of ${existingRole}`,
payload: { existingRole, toBeCreatedRole },
alert: true,
});
return result;
});
}

addRole(role) {
if (!role) { throw new Error('Must provide role'); }

Expand Down
12 changes: 8 additions & 4 deletions src/api/cluster/PrivilegeOperation.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ export default class PrivilegeOperation {
// sentry.fine('buildQuery', this);
const op = this.operation;
const priv = this.privilege;
const db = this.database;

// Escape with back-ticks ONLY if the database isn't *.
// #operability otherwise we defeat the * expansion and are talking about
// a database named * which cannot exist due to naming rules.
const db = this.database === '*' ? this.database : `\`${this.database}\``;

// When the entity is 'DATABASE' effectively the entity doesn't apply.
// For example when GRANT START ON DATABASE FOO TO ROLE
Expand All @@ -229,11 +233,11 @@ export default class PrivilegeOperation {
* https://neo4j.com/docs/cypher-manual/4.0-preview/administration/security/subgraph/#administration-security-subgraph-write
*/
if (priv.indexOf('WRITE') > -1) {
return `${op} ${priv} ON ${graphToken} \`${db}\` ${preposition} ${role}`;
return `${op} ${priv} ON ${graphToken} ${db} ${preposition} ${role}`;
} else if(Object.values(PrivilegeOperation.DATABASE_OPERATIONS).indexOf(priv) > -1) {
return `${op} ${priv} ON DATABASE \`${db}\` ${preposition} ${role}`;
return `${op} ${priv} ON DATABASE ${db} ${preposition} ${role}`;
}

return `${op} ${priv} ON ${graphToken} \`${db}\` ${entity} ${preposition} ${role}`;
return `${op} ${priv} ON ${graphToken} ${db} ${entity} ${preposition} ${role}`;
}
};
14 changes: 13 additions & 1 deletion src/api/cluster/PrivilegeOperation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,19 @@ describe('Privilege Operations', function() {
role: 'microuser',
},
expected: 'GRANT TRAVERSE ON GRAPH `neo4j` RELATIONSHIPS PHONE TO microuser',
}
},
{
reversalAction: 'GRANT',
op: {
access: 'DENIED',
action: 'read',
resource: 'all_properties',
graph: '*',
segment: 'NODE(SSN)',
role: 'public',
},
expected: 'GRANT READ {*} ON GRAPHS * NODES SSN TO public',
},
];

scenarios.forEach(scenario => {
Expand Down
125 changes: 125 additions & 0 deletions src/components/configuration/roles/CopyRoleForm/CopyRoleForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import React, { Component } from 'react';
import { Form, Message } from 'semantic-ui-react';
import status from '../../../../api/status/index';
import hoc from '../../../higherOrderComponents';
import sentry from '../../../../api/sentry/index';
import PropTypes from 'prop-types';

class CopyRoleForm extends Component {
state = {
role: null,
pending: false,
error: null,
};

constructor(props, context) {
super(props, context);
this.onRoleCreate = props.onRoleCreate || (() => null);
}

createRole() {
this.setState({ pending: true });
sentry.info('Creating role copy with driver ', this.driver);

const mgr = window.halinContext.getClusterManager();

return mgr.copyRole(this.props.role, this.state.role)
.then(clusterOpRes => {
sentry.fine('ClusterMgr result', clusterOpRes);
const action = `Creating role ${this.state.role}`;

if (clusterOpRes.success) {
this.setState({
pending: false,
error: null,
});
} else {
this.setState({
pending: false,
error: status.fromClusterOp(action, clusterOpRes),
});
}
})
.catch(err => this.setState({
pending: false,
error: status.message('Error',
`Could not create role ${this.state.role}: ${err}`),
}))
.finally(() => this.state.error ? status.toastify(this) : null);
}

formValid() {
return this.state.role;
}

submit(event) {
sentry.fine('submit', this.state);
event.preventDefault();
this.createRole();
}

handleChange(field, event) {
const mod = {};
mod[field] = event.target.value;
// sentry.debug(mod);
this.setState(mod);
}

inputStyle = {
minWidth: '150px',
paddingTop: '10px',
paddingBottom: '10px',
};

valid() {
if (!this.state.role) { return true; }
return this.state.role.match(/^[A-Za-z0-9]+$/);
}

preview() {
return `CREATE ROLE \`${this.state.role || '(enter role below)'}\` AS COPY OF \`${this.props.role}\``;
}

render() {
return (
<div className='CopyRoleForm'>
<Form error={!this.valid()} size="small" style={{ textAlign: 'left' }}>
<Form.Group>
<h3>Preview</h3>
<h4>{this.preview()}</h4>
</Form.Group>

<Form.Group widths='equal'>
<Form.Input
fluid
style={this.inputStyle}
disabled={this.state.pending}
onChange={e => this.handleChange('role', e)}
label='Role Name'
placeholder='myCustomRole'
/>
</Form.Group>

<Message
error
header='Invalid role name'
content='Role names may consist only of simple letters and numbers'/>

<Form.Button positive
style={this.inputStyle}
disabled={this.state.pending || !this.valid() || !this.state.role}
onClick={data => this.submit(data)}
type='submit'>
<i className="icon copy" /> Copy
</Form.Button>
</Form>
</div>
)
}
}

CopyRoleForm.props = {
role: PropTypes.string.isRequired,
};

export default hoc.enterpriseOnlyComponent(CopyRoleForm, 'Copy Role');
31 changes: 26 additions & 5 deletions src/components/configuration/roles/Neo4jRoles/Neo4jRoles.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ import './Neo4jRoles.css';
import hoc from '../../../higherOrderComponents';
import Explainer from '../../../ui/scaffold/Explainer/Explainer';
import NewRoleForm from '../NewRoleForm/NewRoleForm';
import CopyRoleForm from '../CopyRoleForm/CopyRoleForm';

const copyRoleButton = (input) =>
<Modal closeIcon
trigger={
<Button compact icon='copy' type='submit' />
}>
<Modal.Header>Copy Role: {input.role}</Modal.Header>

<Modal.Content>
<CopyRoleForm role={input.role} />
</Modal.Content>
</Modal>

class Neo4jRoles extends Component {
query = 'call dbms.security.listRoles()';
Expand All @@ -30,11 +43,15 @@ class Neo4jRoles extends Component {
minWidth: 70,
maxWidth: 100,
Cell: ({ row }) => (
<Button compact negative
// Don't let people delete neo4j or admins for now.
disabled={!Neo4jRoles.canDelete(row.role)}
onClick={e => this.open(row)/*this.deleteUser(e, row)*/}
type='submit' icon="cancel"/>
<div>
<Button compact negative
// Don't let people delete neo4j or admins for now.
disabled={!Neo4jRoles.canDelete(row.role)}
onClick={e => this.open(row)}
type='submit' icon="cancel"/>
{/* Only Neo4j 4.0 can do the COPY ROLE operation */}
{ window.halinContext.getVersion().major >= 4 ? copyRoleButton(row) : ''}
</div>
),
},
{ Header: 'Role', accessor: 'role' },
Expand Down Expand Up @@ -128,6 +145,10 @@ class Neo4jRoles extends Component {
});
};

copy = (row) => {
console.log('COPY ROLE',row);
}

confirm = () => {
const roleToDelete = this.state.activeRole;
this.setState({
Expand Down
5 changes: 3 additions & 2 deletions src/components/data/CypherPieChart/CypherPieChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ export default class CypherPieChart extends Component {
})),
['value']);

this.setState({
return this.setState({
data,
error: null,
total,
units,
});
})
.catch(err => {
api.sentry.reportError('Error getting pie chart data', err);
const str = `${err}`;
api.sentry.reportError(`Error getting pie chart data (${str})`, err);
this.setState({
error: err,
total: 1,
Expand Down

0 comments on commit d53e23a

Please sign in to comment.