Skip to content

Commit

Permalink
implement when condition (#153)
Browse files Browse the repository at this point in the history
* implement when condition

* set query expressions for nested queries (#156)

* implement when condition

* handle privileges for many-to-many associations

* try select an attribute of an associated object

* validate nested object attributes

* 2.6.51
  • Loading branch information
kbarbounakis authored Sep 9, 2024
1 parent 44f7839 commit d590b3c
Show file tree
Hide file tree
Showing 21 changed files with 1,134 additions and 133 deletions.
193 changes: 135 additions & 58 deletions data-attribute-resolver.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion data-object-tag.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {DataModel} from "./data-model";
import {DataObject} from "./data-object";

export declare class DataObjectTag extends DataQueryable {
constructor(target: any, association: DataAssociationMapping | string);
parent: DataObject;
mapping: DataAssociationMapping;
getBaseModel(): DataModel;
Expand All @@ -13,5 +14,5 @@ export declare class DataObjectTag extends DataQueryable {
insert(obj: any): Promise<any>;
remove(obj: any): Promise<any>;
removeAll(): Promise<any>;
migrate(callback: (err?: Error) => void);
migrate(callback: (err?: Error) => void): void;
}
5 changes: 3 additions & 2 deletions data-object.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export declare class DataObject extends SequentialEventEmitter {
getModel(): DataModel;
getAdditionalModel():Promise<DataModel>;
getAdditionalObject():Promise<DataObject|any>;
attr(name: string, callback?:(err?: Error,res?: any) => void);
property(name: string);
attr(name: string, callback?:(err?: Error,res?: any) => void): void;
attr(name: string): any;
property(name: string): any;
}
180 changes: 118 additions & 62 deletions data-permission.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
var {QueryEntity} = require('@themost/query');
var {QueryUtils} = require('@themost/query');
var async = require('async');
var {AccessDeniedError} = require('@themost/common');
var {AccessDeniedError, DataError} = require('@themost/common');
var {DataConfigurationStrategy} = require('./data-configuration');
var _ = require('lodash');
var { at } = require('lodash')
var {DataCacheStrategy} = require('./data-cache');
var Q = require('q');
var {hasOwnProperty} = require('./has-own-property');
var {DataModelFilterParser} = require('./data-model-filter.parser');
var {DataQueryable} = require('./data-queryable');
var {SelectObjectQuery} = require('./select-object-query');

/**
* @class
Expand Down Expand Up @@ -520,6 +522,7 @@ DataPermissionEventListener.prototype.validate = function(event, callback) {
});
}
else if (item.type==='self') {
// #implementWhenExpression
// check if the specified privilege has account attribute
if (typeof item.account !== 'undefined' && item.account !== null && item.account !== '*') {
// if user does not have this account return
Expand All @@ -528,55 +531,35 @@ DataPermissionEventListener.prototype.validate = function(event, callback) {
}
}
if (requestMask===PermissionMask.Create) {
var query = QueryUtils.query(model.viewAdapter);
var fields=[], field;
//cast target
var name, obj = event.target;
model.attributes.forEach(function(x) {
name = typeof x.property === 'string' ? x.property : x.name;
if (hasOwnProperty(obj, name))
{
var mapping = model.inferMapping(name);
if (_.isNil(mapping)) {
field = {};
field[x.name] = { $value: obj[name] };
fields.push(field);
}
else if ((mapping.associationType==='association') && (mapping.childModel===model.name)) {
if (typeof obj[name] === 'object' && obj[name] !== null) {
//set associated key value (event.g. primary key value)
field = {};
field[x.name] = { $value: obj[name][mapping.parentField] };
fields.push(field);
}
else {
//set raw value
field = {};
field[x.name] = { $value: obj[name] };
fields.push(field);
}
}
}
});
//add fields
query.select(fields);
//set fixed query
query.$fixed = true;
model.filter(item.filter, function(err, q) {
var query = new SelectObjectQuery(model).select(event.target);
const filter = item.when || item.filter;
model.filter(filter, function(err, q) {
if (err) {
cb(err);
}
else {
//set where from DataQueryable.query
query.$where = q.query.$prepared;
query.$expand = q.query.$expand;
// get filter params (where and join statements)
var {$where, $prepared, $expand} = q.query;
if ($where === null && $prepared === null) {
return cb(new Error('Where condition cannot be empty while validating object privileges.'));
}
// and assign them to the fixed query produced by the previous step
Object.assign(query, {
$where,
$prepared,
$expand
});
// execute query
model.context.db.execute(query,null, function(err, result) {
if (err) {
return cb(err);
}
else {
if (result.length===1) {
// if user has access
if (result.length === 1) {
// set cancel flag for exiting the loop
cancel=true;
// set result to true
event.result = true;
}
return cb();
Expand All @@ -586,24 +569,79 @@ DataPermissionEventListener.prototype.validate = function(event, callback) {
});
}
else {
//get privilege filter
model.filter(item.filter, function(err, q) {
// get primary key
var { [model.primaryKey]: key } = event.target;
// get privilege filter
var parser = new DataModelFilterParser(model);
// stage 1: parse filter condition
// the "when" statement is a filter condition that should be evaluated before validating the current state of the object
var when = item.when || item.filter;
parser.parse(when, function(err, params) {
if (err) {
return cb(err);
}
else {
//prepare query and append primary key expression
q.where(model.primaryKey).equal(event.target[model.primaryKey]).silent().count(function(err, count) {
if (err) { cb(err); return; }
if (count>=1) {
cancel=true;
event.result = true;
var { $where, $expand } = params;
if ($where === null) {
return cb(new Error('Where condition cannot be empty while validating object privileges.'));
}
var q = new DataQueryable(model);
Object.assign(q.query, {
$where,
$expand
});
// stage 2: query for object and get original data
return q.where(model.primaryKey).equal(key).silent().flatten().getItems().then(
function(results) {
// throw error if more than one result is returned
if (results.length > 1) {
return cb(new DataError('E_PRIMARY_KEY', 'Primary key violation', null, model.name, model.primaryKey));
}
if (results.length === 1) {
// get result
var [result] = results;
// get target object ready for validation
var selectTarget = new SelectObjectQuery(model).map(event.target);
var target = requestMask === PermissionMask.Update ? Object.assign(result, selectTarget) : result;
// get filter condition which is going to be evaluated against the target object
var filter = item.filter || item.when;
return parser.parse(filter, function(err, params) {
if (err) {
return cb(err);
}
var query = new SelectObjectQuery(model).select(target);
// get filter params (where and join statements)
var {$where, $expand} = params;
// and assign them to the fixed query produced by the previous step
// note: a fixed query is a query that contains constant values
// and is being prepared for validating filter conditions defined by the current privilege
Object.assign(query, {
$where,
$expand
});
// execute native query
return model.context.db.execute(query,null, function(err, result) {
if (err) {
return cb(err);
}
if (result.length === 1) {
// user has access
// set cancel flag for exiting the loop
cancel=true;
// set result to true
event.result = true;
}
return cb();
});
});
}
return cb();
})
}
}
).catch(function(err) {
return cb(err);
});
});
}
// #implementWhenExpression
}
else {
//do nothing (unknown permission)
Expand Down Expand Up @@ -938,21 +976,39 @@ DataPermissionEventListener.prototype.beforeExecute = function(event, callback)
});
}
else if (item.type==='parent') {
//get field mapping
// #implementWhenExpression
// is this privilege assignable to the current user -and its groups-?
if (typeof item.account !== 'undefined' && item.account !== null && item.account !== '*') {
if (accounts.findIndex(function(x) { return x.name === item.account; }) < 0) {
return cb();
}
}
// try to get mapping from "property" which should be an attribute of the current model
var mapping = model.inferMapping(item.property);
if (!mapping) {
return cb();
if (mapping == null) {
// if mapping is not found, throw error
return cb(new DataError('Invalid configuration. A parent privilege should refer to an attribute which defines an association.', null, model.name, item.property));
}
if (_.isNil(expr))
if (expr == null) {
expr = QueryUtils.query();
expr.where(entity.select(mapping.childField)).equal(perms1.select('target')).
and(perms1.select('privilege')).equal(mapping.childModel).
and(perms1.select('parentPrivilege')).equal(mapping.parentModel).
and(perms1.select('workspace')).equal(workspace).
and(perms1.select('mask')).bit(requestMask,requestMask).
and(perms1.select('account')).in(accounts.map(function(x) { return x.id; })).prepare(true);
}
//
if (mapping.childModel !== model.name) {
return cb(new DataError('Invalid configuration. A parent privilege mapping should refer to a foreign key association on the current model.', null, model.name, item.property));
}
/**
* @type {number[]}
*/
var values = accounts.map(function(x) { return x.id; });
expr.where(entity.select(mapping.childField)).equal(perms1.select('target'))
.and(perms1.select('privilege')).equal(mapping.childModel)
.and(perms1.select('parentPrivilege')).equal(item.property)
.and(perms1.select('workspace')).equal(workspace)
.and(perms1.select('mask')).bit(requestMask,requestMask)
.and(perms1.select('account')).in(values).prepare(true);
assigned=true;
cb();
return cb();
// #implementWhenExpression
}
else if (item.type==='item') {
if (_.isNil(expr))
Expand Down
20 changes: 16 additions & 4 deletions data-queryable.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ DataAttributeResolver.prototype.orderByNestedAttribute = function(attr) {
return DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr);
};

DataAttributeResolver.prototype.selecteNestedAttribute = function(attr, alias) {
DataAttributeResolver.prototype.selectNestedAttribute = function(attr, alias) {
var expr = DataAttributeResolver.prototype.resolveNestedAttribute.call(this, attr);
if (expr) {
if (_.isNil(alias))
Expand All @@ -57,7 +57,7 @@ DataAttributeResolver.prototype.selecteNestedAttribute = function(attr, alias) {
DataAttributeResolver.prototype.selectAggregatedAttribute = function(aggregation, attribute, alias) {
var self=this, result;
if (DataAttributeResolver.prototype.testNestedAttribute(attribute)) {
result = DataAttributeResolver.prototype.selecteNestedAttribute.call(self,attribute, alias);
result = DataAttributeResolver.prototype.selectNestedAttribute.call(self,attribute, alias);
}
else {
result = self.fieldOf(attribute);
Expand Down Expand Up @@ -489,6 +489,12 @@ DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) {
q =QueryUtils.query(self.viewAdapter).select(['*']);
//init an entity based on association adapter (e.g. GroupMembers as members)
entity = new QueryEntity(mapping.associationAdapter).as(field.name);
Object.defineProperty(entity, 'model', {
configurable: true,
enumerable: false,
writable: true,
value: mapping.associationAdapter
});
//init join expression between association adapter and current data model
//e.g. Group.id = GroupMembers.parent
expr = QueryUtils.query().where(QueryField.select(mapping.parentField).from(self.viewAdapter))
Expand Down Expand Up @@ -519,6 +525,12 @@ DataAttributeResolver.prototype.resolveJunctionAttributeJoin = function(attr) {
//create new join
var alias = field.name + '_' + childModel.name;
entity = new QueryEntity(childModel.viewAdapter).as(alias);
Object.defineProperty(entity, 'model', {
configurable: true,
enumerable: false,
writable: true,
value: mapping.associationAdapter
});
expr = QueryUtils.query().where(QueryField.select(mapping.associationValueField).from(field.name))
.equal(QueryField.select(mapping.childField).from(alias));
//append join
Expand Down Expand Up @@ -1228,7 +1240,7 @@ function select_(arg) {
else {
a = DataAttributeResolver.prototype.testNestedAttribute.call(self,arg);
if (a) {
return DataAttributeResolver.prototype.selecteNestedAttribute.call(self, a.name, a.property);
return DataAttributeResolver.prototype.selectNestedAttribute.call(self, a.name, a.property);
}
else {
a = DataAttributeResolver.prototype.testAttribute.call(self,arg);
Expand Down Expand Up @@ -1342,7 +1354,7 @@ DataQueryable.prototype.select = function(attr) {
else {
b = DataAttributeResolver.prototype.testNestedAttribute.call(self,name);
if (b) {
expr = DataAttributeResolver.prototype.selecteNestedAttribute.call(self, b.name, x.property);
expr = DataAttributeResolver.prototype.selectNestedAttribute.call(self, b.name, x.property);
if (expr) { arr.push(expr); }
}
else {
Expand Down
Loading

0 comments on commit d590b3c

Please sign in to comment.