diff --git a/src/components/PermissionsDialog/PermissionsDialog.example.js b/src/components/PermissionsDialog/PermissionsDialog.example.js index 5e43c279ae..1316fc6fbe 100644 --- a/src/components/PermissionsDialog/PermissionsDialog.example.js +++ b/src/components/PermissionsDialog/PermissionsDialog.example.js @@ -74,8 +74,8 @@ class DialogDemo extends React.Component { confirmText='Save ACL' details={Learn more about ACLs and app security} permissions={{ - read: {'*': true}, - write: {'*': true}, + read: {'*': true, 'role:admin': true, 'role:user': true, 's0meU5er1d':true}, + write: {'*': true, 'role:admin':true }, }} validateEntry={validateSimple} onCancel={() => { @@ -93,14 +93,15 @@ class DialogDemo extends React.Component { confirmText='Save CLP' details={Learn more about CLPs and app security} permissions={{ - get: {'*': false, '1234asdf': true, 'role:admin': true}, - find: {'*': true, '1234asdf': true, 'role:admin': true}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField: {'*': true}, - readUserFields: ['owner'], - writeUserFields: ['owner'] + get: {'*': false, '1234asdf': true, 'role:admin': true,}, + find: {'*': true, '1234asdf': true, 'role:admin': true, }, + create: {'*': true, }, + update: {'*': true, pointerFields: ['user']}, + delete: {'*': true, }, + addField: {'*': true, 'requiresAuthentication': true}, + readUserFields: ['owner', 'user'], + writeUserFields: ['owner'], + protectedFields: {'*': ['password', 'email'], 'userField:owner': []} }} validateEntry={validateAdvanced} onCancel={() => { diff --git a/src/components/PermissionsDialog/PermissionsDialog.react.js b/src/components/PermissionsDialog/PermissionsDialog.react.js index bd13051c42..90335ba992 100644 --- a/src/components/PermissionsDialog/PermissionsDialog.react.js +++ b/src/components/PermissionsDialog/PermissionsDialog.react.js @@ -8,7 +8,7 @@ import Button from 'components/Button/Button.react'; import Checkbox from 'components/Checkbox/Checkbox.react'; import Icon from 'components/Icon/Icon.react'; -import { Map } from 'immutable'; +import { Map, fromJS } from 'immutable'; import Pill from 'components/Pill/Pill.react'; import Popover from 'components/Popover/Popover.react'; import Position from 'lib/Position'; @@ -23,109 +23,233 @@ import { let origin = new Position(0, 0); +function resolvePermission(perms, rowId, column){ + + let isPublicRow = rowId === '*'; + let isAuthRow = rowId === 'requiresAuthentication'; // exists only on CLP + let isEntryRow = !isAuthRow && !isPublicRow; + + let publicAccess = perms.get(column).get('*'); + let auth = perms.get(column).get('requiresAuthentication') + let checked = perms.get(column).get(rowId); + + let forceChecked = publicAccess && !auth + let indeterminate = isPublicRow && auth; + // the logic is: + // Checkbox is shown for: + // - Public row: always + // - Authn row: always + // - Entry row: when requires auth OR not Public + let editable = isPublicRow + || isAuthRow + || ( isEntryRow && !forceChecked ) + + return { + checked, editable, indeterminate + } +} + +function resolvePointerPermission(perms, pointerPerms, rowId, column) { + let publicAccess = perms.get(column) && perms.get(column).get("*"); + let auth = perms.get(column).get("requiresAuthentication"); + + // Pointer permission can be grouped as read/write + let permsGroup; + + if (["get", "find", "count"].includes(column)) { + permsGroup = "read"; + } + + if (["create", "update", "delete", "addField"].includes(column)) { + permsGroup = "write"; + } + + let checked = pointerPerms.get(permsGroup) || pointerPerms.get(column); //pointerPerms.get(permsGroup) && pointerPerms.get(permsGroup).get(rowId); + + let forceChecked = publicAccess && !auth; + + // Checkbox is shown for: + // - Public row: always + // - Authn row: always + // - Entry row: when requires auth OR not Public + let editable = !forceChecked; + + return { + checked, + editable, + }; +} + function renderAdvancedCheckboxes(rowId, perms, advanced, onChange) { - const get = perms.get('get').get(rowId) || perms.get('get').get('*'); - const find = perms.get('find').get(rowId) || perms.get('find').get('*'); - const count = perms.get('count').get(rowId) || perms.get('count').get('*'); - const create = perms.get('create').get(rowId) || perms.get('create').get('*'); - const update = perms.get('update').get(rowId) || perms.get('update').get('*'); - const del = perms.get('delete').get(rowId) || perms.get('delete').get('*'); + let get = resolvePermission(perms, rowId, "get"); + let find = resolvePermission(perms, rowId, "find"); + let count = resolvePermission(perms, rowId, "count"); + let create = resolvePermission(perms, rowId, "create"); + let update = resolvePermission(perms, rowId, "update"); + let del = resolvePermission(perms, rowId, "delete"); + let addField = resolvePermission(perms, rowId, "addField"); if (advanced) { return [ -
- {!perms.get('get').get('*') || rowId === '*' ? +
+ {get.editable ? ( onChange(rowId, 'get', value)} /> : - } + label="Get" + checked={get.checked} + onChange={value => onChange(rowId, "get", value)} + /> + ) : ( + + )}
, -
- {!perms.get('find').get('*') || rowId === '*' ? +
+ {find.editable ? ( onChange(rowId, 'find', value)} /> : - } + label="Find" + checked={find.checked} + onChange={value => onChange(rowId, "find", value)} + /> + ) : ( + + )}
, -
- {!perms.get('count').get('*') || rowId === '*' ? +
+ {count.editable ? ( onChange(rowId, 'count', value)} /> : - } + label="Count" + checked={count.checked} + onChange={value => onChange(rowId, "count", value)} + /> + ) : ( + + )}
, -
- {!perms.get('create').get('*') || rowId === '*' ? +
+ {create.editable ? ( onChange(rowId, 'create', value)} /> : - } + label="Create" + checked={create.checked} + onChange={value => onChange(rowId, "create", value)} + /> + ) : ( + + )}
, -
- {!perms.get('update').get('*') || rowId === '*' ? +
+ {update.editable ? ( onChange(rowId, 'update', value)} /> : - } + label="Update" + checked={update.checked} + onChange={value => onChange(rowId, "update", value)} + /> + ) : ( + + )}
, -
- {!perms.get('delete').get('*') || rowId === '*' ? +
+ {del.editable ? ( onChange(rowId, 'delete', value)} /> : - } + label="Delete" + checked={del.checked} + onChange={value => onChange(rowId, "delete", value)} + /> + ) : ( + + )}
, -
- {!perms.get('addField').get('*') || rowId === '*' ? +
+ {addField.editable ? ( onChange(rowId, 'addField', value)} /> : - } -
, + label="Add field" + checked={addField.checked} + onChange={value => onChange(rowId, "addField", value)} + /> + ) : ( + + )} +
]; } - const read = get || find || count; - const write = create || update || del; - const readChecked = get && find && count; - const writeChecked = create && update && del; + let showReadCheckbox = find.editable || get.editable || count.editable; + let showWriteCheckbox = create.editable || update.editable || del.editable; + + let readChecked = find.checked && get.checked && count.checked; + let writeChecked = create.checked && update.checked && del.checked; + + let indeterminateRead = + [get, find, count].some(s => s.checked) && + [get, find, count].some(s => !s.checked); + + let indeterminateWrite = + [create, update, del].some(s => s.checked) && + [create, update, del].some(s => !s.checked); return [ -
- {!(perms.get('get').get('*') && perms.get('find').get('*') && perms.get('count').get('*')) || rowId === '*' ? +
+ {showReadCheckbox ? ( onChange(rowId, ['get', 'find', 'count'], value)} /> : - } + indeterminate={indeterminateRead} + onChange={value => onChange(rowId, ["get", "find", "count"], value)} + /> + ) : ( + + )}
, -
- {!(perms.get('create').get('*') && perms.get('update').get('*') && perms.get('delete').get('*')) || rowId === '*' ? +
+ {showWriteCheckbox ? ( onChange(rowId, ['create', 'update', 'delete'], value)} /> : - } + indeterminate={indeterminateWrite} + onChange={value => + onChange(rowId, ["create", "update", "delete"], value) + } + /> + ) : ( + + )}
, +
+ {addField.editable ? ( + onChange(rowId, ["addField"], value)} + /> + ) : ( + + )} +
]; } function renderSimpleCheckboxes(rowId, perms, onChange) { - let readChecked = perms.get('read').get(rowId) || perms.get('read').get('*'); - let writeChecked = perms.get('write').get(rowId) || perms.get('write').get('*'); + + // Public state + let allowPublicRead = perms.get('read').get('*'); + let allowPublicWrite = perms.get('write').get('*'); + + // requireAuthentication state + let onlyAuthRead = perms.get('read').get('requiresAuthentication'); + let onlyAuthWrite = perms.get('write').get('requiresAuthentication'); + + let isAuthRow = rowId === 'requiresAuthentication'; + let isPublicRow = rowId === '*'; + + + let showReadCheckbox = isAuthRow || (!onlyAuthRead && isPublicRow ) || (!onlyAuthRead && !allowPublicRead) + let showWriteCheckbox = isAuthRow || (!onlyAuthWrite && isPublicRow ) || (!onlyAuthWrite && !allowPublicWrite) + + let readChecked = perms.get('read').get(rowId) || allowPublicRead || isAuthRow ; + let writeChecked = perms.get('write').get(rowId) || allowPublicWrite || isAuthRow ; + return [
- {!perms.get('read').get('*') || rowId === '*' ? + { showReadCheckbox ? }
,
- {!perms.get('write').get('*') || rowId === '*' ? + { showWriteCheckbox ? - {!publicRead ? - onChange(rowId, 'read', value)} /> : - } -
, -
- {!publicWrite ? - onChange(rowId, 'write', value)} /> : - } -
, - ]; - } let cols = []; - if (publicRead) { - cols.push( -
- -
- ); + + if (!advanced) { + // simple view mode + // detect whether public access is enabled + + //for read + let publicReadGrouped = perms.getIn(["read", "*"]); + let publicReadGranular = + perms.getIn(["get", "*"]) && + perms.getIn(["find", "*"]) && + perms.getIn(["count", "*"]); + + // for write + let publicWriteGrouped = perms.getIn(["write", "*"]); + let publicWriteGranular = + perms.getIn(["create", "*"]) && + perms.getIn(["update", "*"]) && + perms.getIn(["delete", "*"]) && + perms.getIn(["addField", "*"]); + + // assume public access is on when it is set either for group or for each operation + let publicRead = publicReadGrouped || publicReadGranular; + let publicWrite = publicWriteGrouped || publicWriteGranular; + + // -------------- + // detect whether auth is required + // for read + let readAuthGroup = perms.getIn(["read", "requiresAuthentication"]); + let readAuthSeparate = + perms.getIn(["get", "requiresAuthentication"]) && + perms.getIn(["find", "requiresAuthentication"]) && + perms.getIn(["count", "requiresAuthentication"]); + + // for write + let writeAuthGroup = perms.getIn(["write", "requiresAuthentication"]); + let writeAuthSeparate = + perms.getIn(["create", "requiresAuthentication"]) && + perms.getIn(["update", "requiresAuthentication"]) && + perms.getIn(["delete", "requiresAuthentication"]) && + perms.getIn(["addField", "requiresAuthentication"]); + + // assume auth is required when it's set either for group or for each operation + let readAuth = readAuthGroup || readAuthSeparate; + let writeAuth = writeAuthGroup || writeAuthSeparate; + + + // when all ops have public access and none requiure auth, show non-editable checked icon + let readForceChecked = publicRead && !readAuth; + let writeForceChecked = publicWrite && !writeAuth; + + // -------------- + // detect whether to show indeterminate checkbox (dash icon) + // in simple view indeterminate happens when: + // {read/write}UserFields is not set and + // not all permissions have same value !(all checked || all unchecked) + let indeterminateRead = + !readUserFields && + [get, find, count].some(s => s.checked) && + [get, find, count].some(s => !s.checked); + + let indeterminateWrite = + !writeUserFields && + [create, update, del, addField].some(s => s.checked) && + [create, update, del, addField].some(s => !s.checked); + cols.push( -
- +
+ {readForceChecked ? ( + + ) : ( + onChange(rowId, "read", value)} + /> + )}
); + + if (writeForceChecked) { + cols.push( +
+ +
, +
+ +
+ ); + } else { + cols.push( +
+
+ onChange(rowId, "write", value)} + /> +
+
+ ); + } + } else { + // in advanced view mode cols.push( -
- +
+ {get.editable ? ( + onChange(rowId, "get", value)} + /> + ) : ( + + )}
); - } else { cols.push( -
-
+
+ {find.editable ? ( onChange(rowId, 'read', value)} /> -
+ label="Find" + checked={find.checked} + onChange={value => onChange(rowId, "find", value)} + /> + ) : ( + + )}
); - } - if (publicWrite) { cols.push( -
- +
+ {count.editable ? ( + onChange(rowId, "count", value)} + /> + ) : ( + + )}
); + cols.push( -
- +
+ {create.editable ? ( + onChange(rowId, "create", value)} + /> + ) : ( + + )}
); cols.push( -
- +
+ {update.editable ? ( + onChange(rowId, "update", value)} + /> + ) : ( + + )}
); cols.push( -
- +
+ {del.editable ? ( + onChange(rowId, "delete", value)} + /> + ) : ( + + )}
); - } else { cols.push( -
-
+
+ {addField.editable ? ( onChange(rowId, 'write', value)} /> -
+ label="Add field" + checked={addField.checked} + onChange={value => onChange(rowId, "addField", value)} + /> + ) : ( + + )}
); } @@ -244,18 +514,39 @@ export default class PermissionsDialog extends React.Component { }) { super(); - let uniqueKeys = ['*']; + + let uniqueKeys = [ + ...(advanced ? ['requiresAuthentication'] : []), + '*' + ]; let perms = {}; for (let k in permissions) { - if (k !== 'readUserFields' && k !== 'writeUserFields') { + if (k !== 'readUserFields' && k !== 'writeUserFields' && k!=='protectedFields' ) { Object.keys(permissions[k]).forEach((key) => { + if(key === 'pointerFields') { + //pointerFields is not a regular entity; processed later + return; + } if (uniqueKeys.indexOf(key) < 0) { uniqueKeys.push(key); } + + // requireAuthentication is only available for CLP + if(advanced){ + if(!permissions[k].requiresAuthentication){ + permissions[k].requiresAuthentication = false; + } + } }); perms[k] = Map(permissions[k]); } } + + let pointerPermsSubset = { + read: permissions.readUserFields || [], + write: permissions.writeUserFields || [], + } + if (advanced) { // Fill any missing fields perms.get = perms.get || Map(); @@ -265,24 +556,29 @@ export default class PermissionsDialog extends React.Component { perms.update = perms.update || Map(); perms.delete = perms.delete || Map(); perms.addField = perms.addField || Map(); + + pointerPermsSubset.get = perms.get.pointerFields || [], + pointerPermsSubset.find = perms.find.pointerFields || [], + pointerPermsSubset.count = perms.count.pointerFields || [], + pointerPermsSubset.create = perms.create.pointerFields || [], + pointerPermsSubset.update = perms.update.pointerFields || [], + pointerPermsSubset.delete = perms.delete.pointerFields || [], + pointerPermsSubset.addField = perms.addField.pointerFields || [] } let pointerPerms = {}; - if (permissions.readUserFields) { - permissions.readUserFields.forEach((f) => { - let p = { read: true }; - if (permissions.writeUserFields && permissions.writeUserFields.indexOf(f) > -1) { - p.write = true; - } - pointerPerms[f] = Map(p); - }); + + // form an object where each pointer-field name holds operations it has access to + // e.g. { [field]: { read: true, create: true}, [field2]: {read: true,} ...} + for(const action in pointerPermsSubset){ + // action holds array of field names + for(const field of pointerPermsSubset[action]){ + pointerPerms[field] = Object.assign({[action]:true},pointerPerms[field]); + } } - if (permissions.writeUserFields) { - permissions.writeUserFields.forEach((f) => { - if (!pointerPerms[f]) { - pointerPerms[f] = Map({ write: true }); - } - }); + // preserve protectedFields + if(permissions.protectedFields){ + perms.protectedFields = permissions.protectedFields; } this.state = { @@ -292,7 +588,7 @@ export default class PermissionsDialog extends React.Component { perms: Map(perms), // Permissions map keys: uniqueKeys, // Permissions row order - pointerPerms: Map(pointerPerms), // Pointer permissions map + pointerPerms: Map(fromJS(pointerPerms)), // Pointer permissions map pointers: Object.keys(pointerPerms), // Pointer order newEntry: '', @@ -315,9 +611,61 @@ export default class PermissionsDialog extends React.Component { }); } - togglePointer(field, type, value) { - this.setState((state) => { - let pointerPerms = state.pointerPerms.setIn([field, type], value); + + togglePointer(field, action, value) { + this.setState(state => { + let pointerPerms = state.pointerPerms; + + // toggle the value clicked + pointerPerms = pointerPerms.setIn([field, action], value); + + const readGroup = ["get", "find", "count"]; + const writeGroup = ["create", "update", "delete", "addField"]; + + // since there're two ways a permission can be granted for field ({read/write}UserFields:['field'] or action: pointerFields:['field'] ) + // both views (advanced/simple) need to be in sync + // e.g. + // read is true (checked in simple view); then 'get' changes true->false in advanced view - 'read' should be also unset in simple view + + // when read/write changes - also update all individual actions with new value + if (action === "read") { + for (const op of readGroup) { + pointerPerms = pointerPerms.setIn([field, op], value); + } + } else if (action === "write") { + for (const op of writeGroup) { + pointerPerms = pointerPerms.setIn([field, op], value); + } + } else { + const groupKey = readGroup.includes(action) ? "read" : "write"; + const group = groupKey === "read" ? readGroup : writeGroup; + + // if granular action changed to true + if (value) { + // if all become checked, unset them as granulars and enable write group instead + if (!group.some(op => !pointerPerms.getIn([field,op]))) { + for (const op of group) { + pointerPerms = pointerPerms.setIn([field, op], false); + } + pointerPerms = pointerPerms.setIn([field, groupKey], true); + } + } + // if granular action changed to false + else { + + // if group was checked on simple view / {read/write}UserFields contained this field + if (pointerPerms.getIn([field, groupKey])) { + // unset value for group + pointerPerms = pointerPerms.setIn([field, groupKey], false); + // and enable all granular actions except the one unchecked + group + .filter(op => op !== action) + .forEach(op => { + pointerPerms = pointerPerms.setIn([field, op], true); + }); + } + } + } return { pointerPerms }; }); } @@ -448,14 +796,25 @@ export default class PermissionsDialog extends React.Component { outputPerms() { let output = {}; - let fields = [ 'read', 'write' ]; + let fields = ["read", "write"]; if (this.props.advanced) { - fields = [ 'get', 'find', 'count', 'create', 'update', 'delete', 'addField' ]; + fields = [ + "get", + "find", + "count", + "create", + "update", + "delete", + "addField" + ]; } - fields.forEach((field) => { + fields.forEach(field => { output[field] = {}; this.state.perms.get(field).forEach((v, k) => { + if (k === "pointerFields") { + return; + } if (v) { output[field][k] = true; } @@ -465,19 +824,34 @@ export default class PermissionsDialog extends React.Component { let readUserFields = []; let writeUserFields = []; this.state.pointerPerms.forEach((perms, key) => { - if (perms.get('read')) { + if (perms.get("read")) { readUserFields.push(key); } - if (perms.get('write')) { + if (perms.get("write")) { writeUserFields.push(key); } + + fields.forEach(op => { + if (perms.get(op)) { + if (!output[op].pointerFields) { + output[op].pointerFields = []; + } + + output[op].pointerFields.push(key); + } + }); }); + if (readUserFields.length) { output.readUserFields = readUserFields; } if (writeUserFields.length) { output.writeUserFields = writeUserFields; } + // should also preserve protectedFields! + if (this.state.perms.get("protectedFields")) { + output.protectedFields = this.state.perms.get("protectedFields"); + } return output; } @@ -548,8 +922,30 @@ export default class PermissionsDialog extends React.Component { return renderSimpleCheckboxes('*', this.state.perms, this.toggleField.bind(this)); } + renderAuthenticatedCheckboxes() { + if (this.state.transitioning) { + return null; + } + if (this.props.advanced) { + return renderAdvancedCheckboxes( + 'requiresAuthentication', + this.state.perms, + this.state.level === 'Advanced', + this.toggleField.bind(this) + ); + } + return null + } + + render() { let classes = [styles.dialog, unselectable]; + + // for 3-column CLP dialog + if(this.props.advanced){ + classes.push(styles.clp); + } + if (this.state.level === 'Advanced') { classes.push(styles.advanced); } @@ -598,18 +994,28 @@ export default class PermissionsDialog extends React.Component {
-
-
-
-
+
+
+
+
+ +
Public
{this.renderPublicCheckboxes()}
- {this.state.keys.slice(1).map((key) => this.renderRow(key))} + {this.props.advanced? +
+
+ Authenticated +
+ {this.renderAuthenticatedCheckboxes()} +
: null} + + {this.state.keys.slice(this.props.advanced?2:1).map((key) => this.renderRow(key))} {this.props.advanced ? this.state.pointers.map((pointer) => this.renderRow(pointer, true)) : null} diff --git a/src/components/PermissionsDialog/PermissionsDialog.scss b/src/components/PermissionsDialog/PermissionsDialog.scss index 841ae6d600..f61dd5a738 100644 --- a/src/components/PermissionsDialog/PermissionsDialog.scss +++ b/src/components/PermissionsDialog/PermissionsDialog.scss @@ -19,6 +19,11 @@ $sumWriteColsWidth: calc(3 * #{$writeColWidth}); $permissionsDialogWidth: calc(#{$labelWidth} + (2 * #{$colWidth}) + #{$deleteColWidth}); $permissionsDialogMaxWidth: calc(#{$labelWidth} + #{$sumReadColsWidth} + #{$sumWriteColsWidth} + #{$addFieldColWidth} + #{$deleteColWidth}); +$simplePointerWriteWidth: calc( #{$colWidth} + #{$addFieldColWidth}); +$pointerWriteWidth: calc( #{$sumWriteColsWidth} + #{$addFieldColWidth}); + +$clpDialogWidth: calc(#{$permissionsDialogWidth} + #{$addFieldColWidth}); + .dialog { @include modalAnimation(); position: absolute; @@ -31,6 +36,24 @@ $permissionsDialogMaxWidth: calc(#{$labelWidth} + #{$sumReadColsWidth} + #{$sumW transition: width 0.3s 0.15s ease-out; } +.clp{ + width: $clpDialogWidth; + + .level{ + width: $clpDialogWidth; + } + // 118px for add field in CLP only + .fourth { + width: $addFieldColWidth; + } +} + +.clp.advanced{ + .fourth { + width: $colWidth; + } +} + .header { height: 50px; background: $blue; @@ -64,9 +87,10 @@ $permissionsDialogMaxWidth: calc(#{$labelWidth} + #{$sumReadColsWidth} + #{$sumW } } + .level { height: 50px; - width: $permissionsDialogWidth; + width: $permissionsDialogWidth; // width: 658px; background: #0E69A1; position: relative; color: white; @@ -239,10 +263,11 @@ $permissionsDialogMaxWidth: calc(#{$labelWidth} + #{$sumReadColsWidth} + #{$sumW width: $sumReadColsWidth; padding: 5px 10px; } + .pointerWrite { + width: $simplePointerWriteWidth; + padding: 0px 10px; display: inline-block; - width: calc(#{$sumWriteColsWidth} + #{$addFieldColWidth}); - padding: 5px 10px; } .checkboxWrap { diff --git a/src/dashboard/Data/Browser/BrowserToolbar.react.js b/src/dashboard/Data/Browser/BrowserToolbar.react.js index 08ce5fe20c..29f2722a0c 100644 --- a/src/dashboard/Data/Browser/BrowserToolbar.react.js +++ b/src/dashboard/Data/Browser/BrowserToolbar.react.js @@ -156,7 +156,7 @@ let BrowserToolbar = ({ if (col === 'objectId' || isUnique && col !== uniqueField) { return; } - if (targetClass === '_User') { + if ((type !=='Relation' && targetClass === '_User') || type === 'Array' ) { userPointers.push(col); } });