Seeming working (though not very thoroughly tested) granular access control for reports, report templates and namespaces.
Should work both in local auth case and LDAP auth case.
This commit is contained in:
parent
89256d62bd
commit
34823cf0cf
17 changed files with 352 additions and 146 deletions
|
@ -585,13 +585,14 @@ function withForm(target) {
|
|||
scheduleValidateForm(self);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Ignoring unhandled error in "validateFormState": ' + error);
|
||||
console.log('Error in "validateFormState": ' + error);
|
||||
|
||||
self.setState(previousState => ({
|
||||
formState: previousState.formState.set('isServerValidationRunning', false)
|
||||
}));
|
||||
|
||||
scheduleValidateForm(self);
|
||||
// TODO: It might be good not to give up immediatelly, but retry a couple of times
|
||||
// scheduleValidateForm(self);
|
||||
});
|
||||
} else {
|
||||
if (formValidateResolve) {
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
border-top: 0px none;
|
||||
}
|
||||
|
||||
.mt-treetable-container .mt-treetable-title {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.form-group .mt-treetable-container {
|
||||
border: 1px solid #cccccc;
|
||||
|
|
|
@ -49,13 +49,15 @@ class TreeTable extends Component {
|
|||
const response = await axios.get(dataUrl);
|
||||
const treeData = response.data;
|
||||
|
||||
treeData.expanded = true;
|
||||
for (const child of treeData.children) {
|
||||
child.expanded = true;
|
||||
for (const root of treeData) {
|
||||
root.expanded = true;
|
||||
for (const child of root.children) {
|
||||
child.expanded = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
treeData: [ response.data ]
|
||||
treeData
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -66,7 +68,8 @@ class TreeTable extends Component {
|
|||
selection: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||
onSelectionChangedAsync: PropTypes.func,
|
||||
actions: PropTypes.func,
|
||||
withHeader: PropTypes.bool
|
||||
withHeader: PropTypes.bool,
|
||||
withDescription: PropTypes.bool
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
|
@ -100,26 +103,35 @@ class TreeTable extends Component {
|
|||
};
|
||||
|
||||
let createNodeFn;
|
||||
if (this.props.actions) {
|
||||
createNodeFn = (event, data) => {
|
||||
const node = data.node;
|
||||
const tdList = jQuery(node.tr).find(">td");
|
||||
createNodeFn = (event, data) => {
|
||||
const node = data.node;
|
||||
const tdList = jQuery(node.tr).find(">td");
|
||||
|
||||
let tdIdx = 1;
|
||||
|
||||
if (this.props.withDescription) {
|
||||
const descHtml = ReactDOMServer.renderToStaticMarkup(<div>{node.data.description}</div>);
|
||||
tdList.eq(tdIdx).html(descHtml);
|
||||
tdIdx += 1;
|
||||
}
|
||||
|
||||
if (this.props.actions) {
|
||||
const linksContainer = jQuery('<span class="mt-action-links"/>');
|
||||
|
||||
const actions = this.props.actions(node.key);
|
||||
const actions = this.props.actions(node);
|
||||
for (const {label, link} of actions) {
|
||||
const lnkHtml = ReactDOMServer.renderToStaticMarkup(<a href={link}>{label}</a>);
|
||||
const lnk = jQuery(lnkHtml);
|
||||
lnk.click((evt) => { evt.preventDefault(); this.navigateTo(link) });
|
||||
lnk.click((evt) => {
|
||||
evt.preventDefault();
|
||||
this.navigateTo(link)
|
||||
});
|
||||
linksContainer.append(lnk);
|
||||
}
|
||||
|
||||
tdList.eq(1).html(linksContainer);
|
||||
};
|
||||
} else {
|
||||
createNodeFn = (event, data) => {};
|
||||
}
|
||||
tdList.eq(tdIdx).html(linksContainer);
|
||||
tdIdx += 1;
|
||||
}
|
||||
};
|
||||
|
||||
this.tree = jQuery(this.domTable).fancytree({
|
||||
extensions: ['glyph', 'table'],
|
||||
|
@ -220,6 +232,7 @@ class TreeTable extends Component {
|
|||
const props = this.props;
|
||||
const actions = props.actions;
|
||||
const withHeader = props.withHeader;
|
||||
const withDescription = props.withDescription;
|
||||
|
||||
let containerClass = 'mt-treetable-container';
|
||||
if (this.selectMode === TreeSelectMode.NONE) {
|
||||
|
@ -238,7 +251,8 @@ class TreeTable extends Component {
|
|||
{props.withHeader &&
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('Name')}</th>
|
||||
<th className="mt-treetable-title">{t('Name')}</th>
|
||||
{withDescription && <th>{t('Description')}</th>}
|
||||
{actions && <th></th>}
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -246,6 +260,7 @@ class TreeTable extends Component {
|
|||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
{withDescription && <td></td>}
|
||||
{actions && <td></td>}
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
|
@ -66,8 +66,10 @@ export default class CUD extends Component {
|
|||
axios.get('/rest/namespaces-tree')
|
||||
.then(response => {
|
||||
|
||||
response.data.expanded = true;
|
||||
const data = [response.data];
|
||||
const data = response.data;
|
||||
for (const root of data) {
|
||||
root.expanded = true;
|
||||
}
|
||||
|
||||
if (this.props.edit && !this.isEditGlobal()) {
|
||||
this.removeNsIdSubtree(data);
|
||||
|
|
|
@ -16,16 +16,25 @@ export default class List extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const actions = key => [
|
||||
{
|
||||
label: 'Edit',
|
||||
link: '/namespaces/edit/' + key
|
||||
},
|
||||
{
|
||||
label: 'Share',
|
||||
link: '/namespaces/share/' + key
|
||||
const actions = node => {
|
||||
const actions = [];
|
||||
|
||||
if (node.data.permissions.includes('edit')) {
|
||||
actions.push({
|
||||
label: 'Edit',
|
||||
link: '/namespaces/edit/' + node.key
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
if (node.data.permissions.includes('share')) {
|
||||
actions.push({
|
||||
label: 'Share',
|
||||
link: '/namespaces/share/' + node.key
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -35,7 +44,7 @@ export default class List extends Component {
|
|||
|
||||
<Title>{t('Namespaces')}</Title>
|
||||
|
||||
<TreeTable withHeader dataUrl="/rest/namespaces-tree" actions={actions} />
|
||||
<TreeTable withHeader withDescription dataUrl="/rest/namespaces-tree" actions={actions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,10 @@ export default class List extends Component {
|
|||
entityTypeId: 'reportTemplate',
|
||||
requiredOperations: ['execute']
|
||||
},
|
||||
createReportTemplate: {
|
||||
entityTypeId: 'namespace',
|
||||
requiredOperations: ['createReportTemplate']
|
||||
},
|
||||
viewReportTemplate: {
|
||||
entityTypeId: 'reportTemplate',
|
||||
requiredOperations: ['view']
|
||||
|
@ -41,7 +45,7 @@ export default class List extends Component {
|
|||
|
||||
this.setState({
|
||||
createPermitted: result.data.createReport && result.data.executeReportTemplate,
|
||||
templatesPermitted: result.data.viewReportTemplate
|
||||
templatesPermitted: result.data.createReportTemplate || result.data.viewReportTemplate
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -118,11 +118,12 @@ export default class Share extends Component {
|
|||
}
|
||||
];
|
||||
|
||||
const sharesColumns = [
|
||||
{ data: 0, title: t('Username') },
|
||||
{ data: 1, title: t('Name') },
|
||||
{ data: 2, title: t('Role') }
|
||||
];
|
||||
const sharesColumns = [];
|
||||
sharesColumns.push({ data: 0, title: t('Username') });
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
sharesColumns.push({ data: 1, title: t('Name') });
|
||||
}
|
||||
sharesColumns.push({ data: 2, title: t('Role') });
|
||||
|
||||
|
||||
let usersLabelIndex = 1;
|
||||
|
|
|
@ -33,7 +33,7 @@ export default class CUD extends Component {
|
|||
this.initForm({
|
||||
serverValidation: {
|
||||
url: '/rest/users-validate',
|
||||
changed: ['username', 'email'],
|
||||
changed: mailtrainConfig.isAuthMethodLocal ? ['username', 'email'] : ['username'],
|
||||
extra: ['id']
|
||||
}
|
||||
});
|
||||
|
@ -86,49 +86,51 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
|
||||
const email = state.getIn(['email', 'value']);
|
||||
const emailServerValidation = state.getIn(['email', 'serverValidation']);
|
||||
if (mailtrainConfig.isAuthMethodLocal) {
|
||||
const email = state.getIn(['email', 'value']);
|
||||
const emailServerValidation = state.getIn(['email', 'serverValidation']);
|
||||
|
||||
if (!email) {
|
||||
state.setIn(['email', 'error'], t('Email must not be empty'));
|
||||
} else if (!emailServerValidation || emailServerValidation.invalid) {
|
||||
state.setIn(['email', 'error'], t('Invalid email address.'));
|
||||
} else {
|
||||
state.setIn(['email', 'error'], null);
|
||||
if (!email) {
|
||||
state.setIn(['email', 'error'], t('Email must not be empty'));
|
||||
} else if (!emailServerValidation || emailServerValidation.invalid) {
|
||||
state.setIn(['email', 'error'], t('Invalid email address.'));
|
||||
} else {
|
||||
state.setIn(['email', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
const name = state.getIn(['name', 'value']);
|
||||
|
||||
if (!name) {
|
||||
state.setIn(['name', 'error'], t('Full name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
const password = state.getIn(['password', 'value']) || '';
|
||||
const password2 = state.getIn(['password2', 'value']) || '';
|
||||
|
||||
const passwordResults = this.passwordValidator.test(password);
|
||||
|
||||
let passwordMsgs = [];
|
||||
|
||||
if (!edit && !password) {
|
||||
passwordMsgs.push(t('Password must not be empty'));
|
||||
}
|
||||
|
||||
if (password) {
|
||||
passwordMsgs.push(...passwordResults.errors);
|
||||
}
|
||||
|
||||
if (passwordMsgs.length > 1) {
|
||||
passwordMsgs = passwordMsgs.map((msg, idx) => <div key={idx}>{msg}</div>)
|
||||
}
|
||||
|
||||
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
|
||||
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
|
||||
}
|
||||
|
||||
|
||||
const name = state.getIn(['name', 'value']);
|
||||
|
||||
if (!name) {
|
||||
state.setIn(['name', 'error'], t('Full name must not be empty'));
|
||||
} else {
|
||||
state.setIn(['name', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
const password = state.getIn(['password', 'value']) || '';
|
||||
const password2 = state.getIn(['password2', 'value']) || '';
|
||||
|
||||
const passwordResults = this.passwordValidator.test(password);
|
||||
|
||||
let passwordMsgs = [];
|
||||
|
||||
if (!edit && !password) {
|
||||
passwordMsgs.push(t('Password must not be empty'));
|
||||
}
|
||||
|
||||
if (password) {
|
||||
passwordMsgs.push(...passwordResults.errors);
|
||||
}
|
||||
|
||||
if (passwordMsgs.length > 1) {
|
||||
passwordMsgs = passwordMsgs.map((msg, idx) => <div key={idx}>{msg}</div>)
|
||||
}
|
||||
|
||||
state.setIn(['password', 'error'], passwordMsgs.length > 0 ? passwordMsgs : null);
|
||||
state.setIn(['password2', 'error'], password !== password2 ? t('Passwords must match') : null);
|
||||
|
||||
validateNamespace(t, state);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue