├── haxelib.json ├── src └── grant │ ├── Utils.hx │ ├── Permission.hx │ └── Grant.hx ├── test └── schema.json └── README.md /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Grant", 3 | "url" : "https://github.com/Talaween/Grant", 4 | "license": "MIT", 5 | "tags": ["accesscontrol", "Role-based-access-control"], 6 | "description": "a Role-based Access Control Library for Haxe, works well woth anonomous structure and Haxe Record Macros.", 7 | "version": "1.0.0-alpha", 8 | "classPath": "src/", 9 | "releasenote": "Initial release, basic functionality working as expected.", 10 | "contributors": ["Mahmoud-Awad","Talaween"], 11 | "dependencies": { 12 | 13 | } 14 | } -------------------------------------------------------------------------------- /src/grant/Utils.hx: -------------------------------------------------------------------------------- 1 | package grant; 2 | 3 | class Utils 4 | { 5 | 6 | public static function linearSearch(searchArray:Array, key:Dynamic):Int 7 | { 8 | 9 | if (searchArray != null) 10 | { 11 | var len = searchArray.length; 12 | 13 | for (i in 0...len) 14 | { 15 | if (searchArray[i] == key) 16 | return i; 17 | } 18 | } 19 | 20 | return -1; 21 | } 22 | 23 | public static function maxIndex(array:Array):Int 24 | { 25 | var max:Int; 26 | var index:Int; 27 | var counter:Int; 28 | 29 | max = array[0]; 30 | index = 0; 31 | counter = 0; 32 | 33 | for (num in array) 34 | { 35 | if(num > max) 36 | { 37 | max = num; 38 | index = counter; 39 | } 40 | counter++; 41 | } 42 | 43 | return index; 44 | } 45 | 46 | public static function stripSpaces(str:String):String 47 | { 48 | 49 | var tempStr = ""; 50 | 51 | var len = str.length; 52 | 53 | for (i in 0...len) 54 | { 55 | if(StringTools.isSpace(str, i) == false) 56 | tempStr += str.charAt(i); 57 | } 58 | 59 | return tempStr; 60 | } 61 | 62 | public static function isLetter(char:String):Bool 63 | { 64 | if(char.length > 1) 65 | return false; 66 | 67 | var code = char.toLowerCase().charCodeAt(0); 68 | 69 | if( code > 96 && code < 123) 70 | return true; 71 | 72 | return false; 73 | } 74 | public static function isDigit(char:String):Bool 75 | { 76 | if(char.length > 1) 77 | return false; 78 | 79 | var code = char.toLowerCase().charCodeAt(0); 80 | 81 | if( code > 47 && code < 58) 82 | return true; 83 | 84 | return false; 85 | } 86 | public static function isLetterOrDigit(char:String):Bool 87 | { 88 | return (isLetter(char) || isDigit(char)); 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /test/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "accesscontrol": 3 | [ 4 | { 5 | "role":"author", 6 | "inherits":"guest", 7 | "grant":[ 8 | { 9 | "resource":"Article", 10 | "policies": 11 | [ 12 | { 13 | "action" : "create", 14 | "records": "any", 15 | "fields" : "*", 16 | "limit" : { 17 | "amount": -1, 18 | "rule" :"" 19 | } 20 | }, 21 | { 22 | "action" : "read", 23 | "records": "any", 24 | "fields" : "*", 25 | "limit" : { 26 | "amount": -1, 27 | "rule" :"" 28 | } 29 | }, 30 | { 31 | "action" : "update", 32 | "records": "resource.authorId=user.id", 33 | "fields" : "*", 34 | "limit" : { 35 | "amount": -1, 36 | "rule" :"" 37 | } 38 | }, 39 | { 40 | "action" : "delete", 41 | "records": "resource.authorId=user.id", 42 | "fields" : "*", 43 | "limit" : { 44 | "amount": -1, 45 | "rule" :"" 46 | } 47 | } 48 | ] 49 | 50 | }, 51 | { 52 | "resource":"comments", 53 | "policies": 54 | [ 55 | { 56 | "action" : "create", 57 | "records": "any", 58 | "fields" : "*", 59 | "limit" : { 60 | "amount": -1, 61 | "rule" :"" 62 | } 63 | }, 64 | { 65 | "action" : "read", 66 | "records": "any", 67 | "fields" : "*", 68 | "limit" : { 69 | "amount": -1, 70 | "rule" :"" 71 | } 72 | }, 73 | { 74 | "action" : "update", 75 | "records": "comment.authorId=user.id", 76 | "fields" : "title, body", 77 | "limit" : { 78 | "amount": -1, 79 | "rule" :"" 80 | } 81 | }, 82 | { 83 | "action" : "delete", 84 | "records": "comment.authorId=user.id", 85 | "fields" : "*", 86 | "limit" : { 87 | "amount": -1, 88 | "rule" :"" 89 | } 90 | } 91 | ] 92 | } 93 | ] 94 | } 95 | ] 96 | } -------------------------------------------------------------------------------- /src/grant/Permission.hx: -------------------------------------------------------------------------------- 1 | package grant; 2 | 3 | import sys.db.Connection; 4 | import grant.Grant; 5 | 6 | /** 7 | * ... 8 | * @author Mahmoud Awad 9 | */ 10 | 11 | @:final 12 | @:allow(grant.Grant) 13 | class Permission 14 | { 15 | 16 | //read only properties 17 | public var granted(default, null):Bool; 18 | public var policy(default, null):Policy; 19 | 20 | public var message:String; 21 | public var role:String; 22 | public var resource:String; 23 | 24 | private var policies:Array; 25 | private var currentIndex:Int; 26 | 27 | private function new(role:String, resource:String, policies:Array, ?message:String) 28 | { 29 | this.role = role; 30 | this.policies = policies; 31 | this.resource = resource; 32 | this.message = message; 33 | 34 | currentIndex = 0; 35 | 36 | if(policies != null && policies.length > 0) 37 | { 38 | this.policy = policies[currentIndex]; 39 | this.granted = true; 40 | } 41 | else 42 | this.granted = false; 43 | } 44 | 45 | function get_granted(){ 46 | 47 | return this.granted; 48 | } 49 | 50 | function get_policy(){ 51 | 52 | return this.policy; 53 | } 54 | 55 | public function nextPolicy():Bool 56 | { 57 | currentIndex++; 58 | if(currentIndex < policies.length) 59 | { 60 | this.policy = policies[currentIndex]; 61 | return true; 62 | } 63 | 64 | return false; 65 | } 66 | public function filter(user:Dynamic, resource:Dynamic, connection:Connection):Dynamic 67 | { 68 | if(this.policy == null) 69 | return null; 70 | 71 | var fields = this.policy.fields.split(","); 72 | var field:String; 73 | 74 | var len = fields.length; 75 | 76 | if(fields[0] == "*") 77 | { 78 | if(len == 1) 79 | return resource; 80 | else 81 | { 82 | for(i in 1...len) 83 | { 84 | fields[i] = StringTools.trim(fields[i]); 85 | field = fields[i].substr(1); 86 | Reflect.deleteField(resource, field); 87 | } 88 | return resource; 89 | } 90 | } 91 | else 92 | { 93 | var resourceCopy:Dynamic = {}; 94 | 95 | for(i in 0...len) 96 | { 97 | fields[i] = StringTools.trim(fields[i]); 98 | 99 | if(fields[i].length > 0) 100 | { 101 | if(fields[i].indexOf("^") > 0) 102 | { 103 | var subs = fields[i].split("^"); 104 | if(subs.length == 2) 105 | { 106 | var subObj = Reflect.field(resource, subs[0]); 107 | var grant = Grant.getInstance(); 108 | 109 | //currently it does not work if resource has another same resource type 110 | //as subobject in order to prevent infinite recursions 111 | if(subs[1] != this.resource) 112 | { 113 | var subPermission = grant.mayAccess(this.role, this.policy.action, subs[1]); 114 | 115 | if(subPermission.granted == true) 116 | { 117 | var subObjCopy = grant.access(user, subPermission, subObj, connection); 118 | Reflect.setField(resourceCopy, subs[0], subObjCopy ); 119 | } 120 | } 121 | } 122 | } 123 | //else if(fields[i].charAt(0) != "!") 124 | // Reflect.setField(resourceCopy, fields[i], Reflect.field(resource, fields[i])); 125 | else 126 | { 127 | //field = fields[i].substr(1); 128 | Reflect.setField(resourceCopy, fields[i], Reflect.field(resource, fields[i])); 129 | 130 | } 131 | } 132 | } 133 | 134 | return resourceCopy; 135 | } 136 | } 137 | 138 | public function fields():Array 139 | { 140 | if(this.policy == null) 141 | return null; 142 | 143 | var tmp_fields = this.policy.fields.split(","); 144 | 145 | var len = tmp_fields.length; 146 | 147 | for(i in 0...len) 148 | { 149 | if(tmp_fields[i].indexOf("^") > -1) 150 | tmp_fields[i] = tmp_fields[i].split("^")[0]; 151 | } 152 | 153 | return tmp_fields; 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grant HX 2 | Build Status 3 | 4 | ### WIP currently going through heavy testing 5 | 6 | Role-based Access Control (RBAC) Library for Haxe, that allows you to manage all your RBAC in an easy and flexible way. 7 | 8 | The idea of Grant is that all RBAC should be kept outside the code, it is maintained in a JSON file. RBAC can be easily changed by only modifying the JSON data. however Grant allows you to have your RBAC logic in the code as well. 9 | 10 | Currently the library works with PHP and MYSQL, in future release it will be available for Node.JS, Python, JAVA and C#. 11 | 12 | ## Features 13 | 14 | * all RBAC logic maintained in a single JSON file or a JSON object 15 | * friendly json format and structure 16 | * only few lines of code are needed to be written to manage RBAC in code 17 | * the library will do all the required database operations to check for the permissions 18 | * ability to use the library without database checking 19 | * fine-grained access control to specific records of a table by applying conditions 20 | * fine-grained access control to specific fields of a table by applying filters 21 | * ability to add more than one policy per role to the same table to access different records 22 | * support inheritance of roles 23 | * ability to add limits to policy e.g. how many times to read a resource 24 | * library automatically checks for JSON data validity and warns of any errors 25 | * fast and performs well 26 | * works with any Haxe class including record macros and anonomous structures 27 | * supports PHP and MYSQL 28 | * does not depend on any other Haxe library 29 | * ability to manage sub-object access control as well e.g. resource1.resource2 30 | 31 | ### Installing 32 | 33 | Install through haxe lib 34 | 35 | ``` 36 | haxelib install Grant 37 | ``` 38 | ### how to use 39 | 40 | get grant instance 41 | 42 | ```js 43 | var grant = Grant.getInstance(); 44 | ``` 45 | Get instance has the following signature: 46 | 47 | ```js 48 | public static function getInstance(?params:{user:String,?socket:Null,?port:Null,pass:String,host:String,database:String}):Grant 49 | ``` 50 | It takes the required information to connect to the database as optional parameters. if your policies require verification through databas then you can let Grant access the database, more on this below. 51 | 52 | You can build a policy stored in a json file 53 | 54 | ```js 55 | var textualJsonData = sys.io.File.getContent('/path/to/json/policies/file.json'); 56 | grant.fromJson(textualJsonData); 57 | 58 | ``` 59 | 60 | or can build policy using an object of type Schema, more details are below 61 | 62 | ```js 63 | grant.setSchema(schema); 64 | 65 | ``` 66 | where the function fromJson accepts an anonymous structure of type grant.Schema; 67 | 68 | then to check if a role may access a resource use for example: 69 | 70 | ```js 71 | 72 | public function mayAccess(role:String, action:String, resourceName:String):Permission 73 | 74 | //an example how to use it in the code 75 | var permission = grant.mayAccess('guest', 'read', 'article'); 76 | 77 | ``` 78 | 79 | mayAccess function returns a permission object that holds the RBAC data where the initial result is stored in (permission.granted) property, the value is true if there is a chance that the user may access the resource otherwise the value is false (it is true if the user has access to some records in the table) 80 | please note further database checks maybe needed to confirm access to the resource or the exact record. 81 | 82 | To access the actual resource or record, you will use the permission object with the user object and the resource object. please note that the user object should have a propery called role, if user does not have a role property then an exception is thrown. 83 | 84 | ```js 85 | public function access(user:Dynamic, permission:Permission, resource:Dynamic, connection:Connection):Dynamic 86 | 87 | //an example of how to use it in the code 88 | var accessibleObject = grant.access(user, permission, article, connection); 89 | 90 | ``` 91 | 92 | in case you want to know which fields the user can access for the specified action (read, create, update, delete) 93 | 94 | ```js 95 | public function fields():Array 96 | 97 | //an example how use it in the code 98 | var fields = permsision.fields(); 99 | 100 | ``` 101 | 102 | you can also filter what user can access from the resource, in this case you will need to do the actual check for user permission by yourself for example if the user is the owner. 103 | 104 | ```js 105 | public function filter(user:Dynamic, resource:Dynamic, connection:Connection):Dynamic 106 | 107 | //an example of how to use it in the code 108 | var accessibleObject = permission.filter(user, article); 109 | 110 | ``` 111 | filter function returns a new object that have only what the user can access from the resource based on the defined access policy. 112 | 113 | ### Understanding Access Control Schema 114 | 115 | all access control data are stored in a json file or an anonymous structure in the code, if it is in a json file then it is your resposibility to prevent access or modifications to this file 116 | 117 | The top level object of the schema is as follow, which has only one property called "accesscontrol" which is an arrya of roles. 118 | 119 | ```js 120 | 121 | {"accesscontrol": Array} 122 | 123 | ``` 124 | the Role object has the following properties: 125 | 126 | * role: name of the role 127 | * inherits: an optional field which is the name of the role to extend/inherit from 128 | * grant: an array of Resource objects which the user role can access 129 | 130 | ```js 131 | 132 | {"role": String, "inherits":String, "grant":Array} 133 | 134 | ``` 135 | 136 | the Resource object has the following structure and fields: 137 | 138 | ```js 139 | 140 | {"resource": String, "policies":Array} 141 | 142 | ``` 143 | 144 | * resource: the name of the resource 145 | * policies: an array of Policy objects to be applied on the resource 146 | 147 | the Policy object has the following structure and fields: 148 | 149 | ```js 150 | 151 | {"action": String, "fields":String, "records":String, "limit":Limit} 152 | 153 | ``` 154 | 155 | * action: one of the following create/read/update/delete 156 | * fields: the fields the role can access from this resource, the fields should be separted by comma e.g. "title, mainText, publishedDate". you can also specify all fields "*" and exclude some fields with "!", e.g. "*, !publishedDate" 157 | * records: the condition(s) that allow the role to access this resource, see below for explanations on conditions 158 | * limit: is a Limit object to count how many times a role can perform the specified action on the resource 159 | 160 | You can apply more than one policy for the same rule on the same resource, with each Policy gives the user a different access to a subset of records, for example one Policy allows user to read all the fields of his own articles, another policy to read a subset of fields for articles he does not own. 161 | The order how you add policies on the same resource is important, If you have more than one policy on the same resource, Grant always execute the first policy that give the user access to the record. 162 | 163 | the Limit object has the following structure and fields: 164 | 165 | ```js 166 | 167 | {"amount": Int, "rule":String} 168 | 169 | ``` 170 | 171 | * amount: the number of times the role can perform the action on the resource, use -1 for unlimited, 0 can be used to ban 172 | * rule: the condition need to count the limit, see below for explanations on conditions 173 | 174 | you can specify more than one policy on the same resource for the same role, so that the role can access different records in different manner, the record property of Policy object allows you to decide which rows/records in the table the user can acess. for example an author role will allow the user to read any article and see its title, textBody and publishedDate, and another read policy that is applied to his own created articles where he will be able to see title, textBody, publishedDate and notes. 175 | 176 | ### Sub Object Permissions 177 | 178 | you can control sub-object permission (currently to one level) by Grant. 179 | 180 | for example if you have a resource called Comment which also has an article and user object associated with it, you can ask Grant to apply the available policies for these resources that are available for the current user by identifying the sub-object with '^' operator as follow: 181 | 182 | ```js 183 | { 184 | "action" : "read", 185 | "records": "any", 186 | "fields" : "commentText, publishedDate, author^User, article^Article", 187 | "limit" : { 188 | "amount": -1, 189 | "rule" :"" 190 | } 191 | }, 192 | ``` 193 | 194 | now Grant will look for what User policy is available for the current user and apply it on the author field, and the same for article, if Grant cannot find any policies or the current policy does not allow access the resource then that sub-object will not appear. 195 | 196 | if you do not apply the "^" operator no access control policy will be applied to the sub-object and it will returned as it is if the user is allowed access the sub-object in the current policy. 197 | 198 | ### Conditions 199 | 200 | the conditions is very important part of how Grant works, it allows you to specified how to access a specific record, it is in fact allow you to filter which records/rows of a table a user may access and how. 201 | 202 | conditions can be specified on two fields: 203 | 204 | * records field of a Policy object 205 | * rule field of a Limit object 206 | 207 | you can specify values directly from the user obejct or the resource object, add a select statement to the condition or combine both. 208 | 209 | there are few simple rules how to write your conditions to ensure that Grant can get the correct answer for you: 210 | 211 | if you are checking any values that is on User or the actual resource object then you should use the following syntax: 212 | 213 | e.g. check if the user is the actual author of the article 214 | 215 | ```js 216 | .... 217 | "records" : "$resource.auhthorId=$user.Id", 218 | .... 219 | ``` 220 | note that we have used the word "user" to refer to the actual User obejct currently trying to access the resource, and the resource word to refer the current resource object. 221 | 222 | you can also add static values: 223 | 224 | ```js 225 | .... 226 | "records" : "$resource.price >= 20/i", 227 | .... 228 | ``` 229 | 230 | note that you have to add a tailing flag to indicate the type of the value: 231 | 232 | * /i for integer e.g. 20/i 233 | * /f for float e.g. 13.5/f 234 | * /b for boolean e.g. true/b 235 | * /d for date e.g. 2019-07-14/d 236 | 237 | otherwise the value will be treated as a string 238 | 239 | the comparison operator can be one of the following (>, >=, <, <=, ==, =, !=), where = or == will be the same. 240 | 241 | if you are checking values from any other tables or objects, then you have to use a SQL select statement 242 | 243 | e.g. check if the user can access a picture which only allowed for users who were tagged in it: 244 | 245 | ```js 246 | .... 247 | "records" : "Select count(*) From tags where tags.userId = $user.id And tags.pictureId = $resource.id", 248 | .... 249 | ``` 250 | if the length of result for this SQL is more than 1, it will be evaluated as true otherwise false. 251 | 252 | you can combine both examples of conditions using either | & operators: 253 | 254 | ```js 255 | .... 256 | "records" : "$resource.auhthorId=$user.Id | Select * From tags where tags.userId = $user.id And tags.pictureId = $resource.id", 257 | .... 258 | ``` 259 | ### Limits 260 | 261 | The Limit object provides a condition to count how many time a policy can be valid before it expires, the amount property specifies the maximum number to execute the policy, while in the "rule" property, you should use a SQL SELECT statement that counts the number of times the user role has accessed the object, for example allowing a user to only create maximum 5 articles : 262 | 263 | ```js 264 | .... 265 | "amount" : "5", 266 | "rule" : "SELECT count(*) From Articles where Articles.authorId=$user.id" 267 | .... 268 | ``` 269 | in this example if the SQL statement returns a result less than 5 the user will be able to access and to perform the action on the resource. 270 | 271 | 272 | ### Building the Policy in the code 273 | 274 | if you wish you can build the policy inside the code rather than importing it from a json file. 275 | 276 | you can use the following functions to build your schema: 277 | 278 | * Grant.setSchma(schema:Schema) 279 | * Grant.addRole(role:Role) 280 | * Grant.assignToRole(roleName:String, resource:Resource) 281 | 282 | ### Policies examples 283 | 284 | update all fields on own resource: 285 | 286 | ```js 287 | { 288 | "action" : "update", 289 | "records": "$resource.id=$user.id", 290 | "fields" : "*", 291 | "limit" : { 292 | "amount": -1, 293 | "rule" :"" 294 | } 295 | } 296 | ``` 297 | 298 | read selected fields on an own resource to be read maximum 5 times: 299 | 300 | ```js 301 | { 302 | "action" : "read", 303 | "records": "$resource.ownerId=$user.id", 304 | "fields" : "field1, field2, field3", 305 | "limit" : { 306 | "amount": 5, 307 | "rule" :"SELECT count(*) FROM ResourceTable WHERE ownerId = $user.id" 308 | } 309 | } 310 | ``` 311 | 312 | create a tag on own articles, where articles are stored in a table called Article: 313 | 314 | ```js 315 | { 316 | "action":"create", 317 | "records":"SELECT * FROM Article WHERE Article.Id=$resource.articleId AND Article.authorId=$user.id", "fields":"*", 318 | "limit": { 319 | "amount":-1, 320 | "rule":"$resource.articleId" 321 | } 322 | }; 323 | ``` 324 | 325 | ### Additional policies 326 | 327 | You can let Grant verify a policy that is not part of your schema using 328 | 329 | ```js 330 | public function checkPolicy(user:Dynamic, policy:Policy, resource:Dynamic):Bool 331 | 332 | ``` 333 | 334 | ### Built With 335 | 336 | * [Haxe](http://www.haxe.org/) - The language used 337 | 338 | ### Contributing 339 | 340 | Please feel free to submit pull requests to us. 341 | 342 | 343 | ### Authors 344 | 345 | * **Mahmoud Awad** - *Initial work* - [Talaween](https://github.com/talaween) 346 | 347 | ### License 348 | 349 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 350 | 351 | ### Acknowledgments 352 | 353 | * Haxe Community 354 | 355 | 356 | 357 | 358 | 359 | -------------------------------------------------------------------------------- /src/grant/Grant.hx: -------------------------------------------------------------------------------- 1 | package grant; 2 | /** 3 | * ... 4 | * @author Mahmoud Awad 5 | * 6 | * Allow more than one user role 7 | * allow dynamic user roles creation from outide DB 8 | * 9 | */ 10 | import sys.db.Connection; 11 | import sys.db.ResultSet; 12 | import grant.*; 13 | 14 | typedef Limit = {amount:Int, rule:String}; 15 | typedef Policy = {action:String, records:String, fields:String, limit:Limit}; 16 | typedef Resource = {resource:String, policies:Array}; 17 | typedef Role = {role:String, ?inherits:String, grant:Array}; 18 | typedef Schema = {accesscontrol:Array}; 19 | 20 | class Grant 21 | { 22 | private static var _instance:Grant; 23 | private var schema:Schema; 24 | 25 | public static function getInstance():Grant 26 | { 27 | if (_instance == null) 28 | _instance = new Grant(); 29 | 30 | return _instance; 31 | } 32 | 33 | private function new() 34 | { 35 | 36 | } 37 | 38 | public function setSchema(schema:Schema) 39 | { 40 | this.schema = schema; 41 | } 42 | 43 | public function addRole(obj:Role) 44 | { 45 | schema.accesscontrol.push(obj); 46 | } 47 | public function assignToRole(roleName:String, resource:Resource):Bool 48 | { 49 | var len = schema.accesscontrol.length; 50 | 51 | for(i in 0...len) 52 | { 53 | if(schema.accesscontrol[i].role == roleName) 54 | { 55 | schema.accesscontrol[i].grant.push(resource); 56 | return true; 57 | } 58 | } 59 | 60 | return false; 61 | } 62 | public function mayAccess(role:String, action:String, resourceName:String):Permission 63 | { 64 | 65 | if(schema == null || schema.accesscontrol == null) 66 | return new Permission(role, resourceName, null, "No access control schema is provided"); 67 | 68 | //find the role in the schema 69 | var _thisRole:Role = null; 70 | var _inherited:Role = null; 71 | 72 | //find the desired role in the schema 73 | for(_r in schema.accesscontrol) 74 | { 75 | if(_r.role == role) 76 | { 77 | _thisRole = _r; 78 | break; 79 | } 80 | } 81 | 82 | //if we could not find the role 83 | if(_thisRole == null) 84 | return new Permission(role, resourceName, null, "role was not found"); 85 | 86 | //if the role inherits from another role find it 87 | if(_thisRole.inherits != null && _thisRole.inherits != "") 88 | { 89 | for(_rInh in schema.accesscontrol) 90 | { 91 | if(_rInh.role == _thisRole.inherits) 92 | { 93 | _inherited = _rInh; 94 | break; 95 | } 96 | } 97 | } 98 | 99 | //find all policies on this resource for this role and its inherited ones 100 | var _policies = new Array(); 101 | var zerolimited = true; 102 | 103 | //priority is for the extended policies before the inherited 104 | for(_res in _thisRole.grant) 105 | { 106 | if(_res.resource == resourceName) 107 | { 108 | for (_pol in _res.policies) 109 | { 110 | if(_pol.action == action) 111 | { 112 | if(_pol.limit.amount != 0) 113 | zerolimited = false; 114 | 115 | _policies.push(_pol); 116 | } 117 | } 118 | } 119 | } 120 | if(_inherited != null) 121 | { 122 | for(_resInh in _inherited.grant) 123 | { 124 | if(_resInh.resource == resourceName) 125 | { 126 | for (_polInh in _resInh.policies) 127 | { 128 | if(_polInh.action == action) 129 | { 130 | if(_polInh.limit.amount != 0) 131 | zerolimited = false; 132 | 133 | _policies.push(_polInh); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | var accessMessage = ""; 141 | 142 | if(_policies.length == 0) 143 | accessMessage = 'no ${action} policy was found for role ${role} on resource ${resourceName}'; 144 | 145 | if(zerolimited == true) 146 | { 147 | accessMessage = 'all existing policies found are limited to zero for role ${role} on resource ${resourceName}'; 148 | //because all limited to 0 discard them 149 | _policies = null; 150 | } 151 | 152 | return new Permission(role, resourceName, _policies, accessMessage); 153 | } 154 | 155 | public function access(user:Dynamic, permission:Permission, resource:Dynamic, connection:Connection):Dynamic 156 | { 157 | var allow = false; 158 | 159 | if(permission != null && permission.policy != null && user != null) 160 | { 161 | if(user.role != null && permission.role != null && user.role == permission.role) 162 | { 163 | allow = checkPolicy(user, permission.policy, resource, connection); 164 | 165 | if(allow) 166 | { 167 | allow = checkLimit (user, permission.policy.limit, resource, connection); 168 | if(allow) 169 | { 170 | permission.policy.fields = checkFields(permission.policy.fields); 171 | return permission.filter(user, resource, connection); 172 | } 173 | else 174 | permission.message += ", limit on the resource has been reached."; 175 | } 176 | } 177 | else 178 | permission.message += ", user role and permission does not match"; 179 | } 180 | 181 | if(permission.nextPolicy() == true) 182 | return access(user, permission, resource, connection); 183 | else 184 | return null; 185 | } 186 | 187 | public function checkPolicy(user:Dynamic, policy:Policy, resource:Dynamic, connection:Connection):Bool 188 | { 189 | if(policy.records == null || policy.records == "" || policy.records.toLowerCase() == "none") 190 | return false; 191 | else if(policy.records.toLowerCase() == "any") 192 | return true; 193 | else 194 | return (runCondition(user, policy.records, resource, connection) > 0 ? true : false); 195 | } 196 | 197 | //this function needs revisions, having count (*) of resource.field = user.field doesn't 198 | //mean we can access the current record. we need to explicitly include the current record 199 | private function runCondition(user:Dynamic, condition:String, resource:Dynamic, connection:Connection):Int 200 | { 201 | var operators = new Array(); 202 | var conditions = new Array(); 203 | 204 | var conditionPart = ""; 205 | var len = condition.length; 206 | 207 | //evaluate each sub part of condition by splitting through & and | operators 208 | for(i in 0...len) 209 | { 210 | switch(condition.charAt(i)) 211 | { 212 | case "&": operators.push("&"); 213 | conditionPart = StringTools.trim(conditionPart); 214 | conditions.push(conditionPart); 215 | conditionPart = ""; 216 | 217 | case "|": operators.push("|"); 218 | conditionPart = StringTools.trim(conditionPart); 219 | conditions.push(conditionPart); 220 | conditionPart = ""; 221 | 222 | default: conditionPart += condition.charAt(i); 223 | } 224 | } 225 | 226 | //push the last condition part 227 | conditionPart = StringTools.trim(conditionPart); 228 | conditions.push(conditionPart); 229 | 230 | var finalValue = false; 231 | var counter = 0; 232 | 233 | for(conditionPart in conditions) 234 | { 235 | var retVal = evaluateExpression(user, conditionPart, resource, connection); 236 | 237 | if(counter == 0) 238 | finalValue = (retVal == 1 ? true:false); 239 | else 240 | { 241 | switch (operators[counter-1]) 242 | { 243 | case "&": finalValue = finalValue && (retVal == 1 ? true:false); 244 | case "|": finalValue = finalValue || (retVal == 1 ? true:false); 245 | } 246 | } 247 | } 248 | return (finalValue == true ? 1 : 0); 249 | //case when we use a select statement in the condition 250 | } 251 | 252 | private function evaluateExpression(user:Dynamic, condition:String, resource:Dynamic, connection:Connection):Int 253 | { 254 | if(condition.indexOf("select") == 0 || condition.indexOf("Select") == 0 || condition.indexOf("SELECT") == 0) 255 | { 256 | //we need to embed user and resource values inside the query instead of $user and $resource 257 | 258 | var userField = extractFieldName(condition, "$user"); 259 | var resourceField = extractFieldName(condition, "$resource"); 260 | 261 | if(userField != '') 262 | { 263 | var userValue = Reflect.field(user, userField); 264 | condition = StringTools.replace(condition, "$user." + userField, userValue); 265 | } 266 | 267 | if(resourceField != '') 268 | { 269 | var resourceValue = Reflect.field(resource, resourceField); 270 | condition = StringTools.replace(condition, "$resource." + resourceField , resourceValue); 271 | } 272 | 273 | return executeQuery(condition, connection); 274 | } 275 | //case we use (resource.fieldName = user.fieldName) or (resource.fieldName = someValue) 276 | else if(condition.indexOf("$resource") == 0 || condition.indexOf("$user") == 0) 277 | { 278 | 279 | condition = Utils.stripSpaces(condition); 280 | 281 | var reg = ~/[>=") != -1) 341 | return (firstPartValue >= secondPartValue ? 1:0); 342 | else if(condition.indexOf("<=") != -1) 343 | return (firstPartValue <= secondPartValue ? 1:0); 344 | else if(condition.indexOf(">") != -1) 345 | return (firstPartValue > secondPartValue ? 1:0); 346 | else if(condition.indexOf("<") != -1) 347 | return (firstPartValue < secondPartValue ? 1:0); 348 | else if(condition.indexOf("!=") != -1) 349 | return (firstPartValue != secondPartValue ? 1:0); 350 | else if(condition.indexOf("==") != -1) 351 | return (firstPartValue == secondPartValue ? 1:0); 352 | else if(condition.indexOf("=") != -1) 353 | return (firstPartValue == secondPartValue ? 1:0); 354 | else 355 | throw "error in records expression, wrong operator used in expression"; 356 | } 357 | else 358 | throw "error in records expression, no 2 operands were detected"; 359 | } 360 | return 0; 361 | } 362 | 363 | private function extractFieldName(statement:String, obj:String):String 364 | { 365 | var len1 = statement.indexOf(obj + '.') + (obj + '.').length; 366 | var len2 = statement.length; 367 | var field = ''; 368 | 369 | for(i in len1...len2) 370 | { 371 | if(StringTools.isSpace(statement, i)) 372 | break; 373 | else if(statement.charAt(i) == "\"") 374 | continue; 375 | else 376 | field += statement.charAt(i); 377 | } 378 | 379 | return field; 380 | } 381 | 382 | private function checkLimit(user:Dynamic, limit:Limit, resource:Dynamic, connection:Connection):Bool 383 | { 384 | var allow = false; 385 | if(limit == null || limit.amount == null || limit.amount == -1) //we do not care about limit 386 | allow = true; 387 | else if(limit.amount > 0) 388 | return (runCondition(user, limit.rule, resource, connection) > 0 ? true: false); 389 | else if(limit.amount == 0) 390 | allow = false; 391 | 392 | return allow; 393 | } 394 | 395 | private function executeQuery(sql:String, connection:Connection):Int 396 | { 397 | var records:ResultSet; 398 | try 399 | { 400 | records = connection.request(sql); 401 | } 402 | catch(err:String){ 403 | return 0; 404 | } 405 | if(records == null || records.results().length == null) 406 | return 0; 407 | 408 | return records.results().length; 409 | } 410 | 411 | /* check the order of the fields that are allowed or prohibited to be accessed 412 | * make sure the * ioperator is put at first and no duplication is in there 413 | * and the excluded fields put after the allowed ones 414 | * */ 415 | private static function checkFields(fields:String):String 416 | { 417 | 418 | fields = Utils.stripSpaces(fields); 419 | 420 | var fieldsArray = fields.split(","); 421 | var includedFields = new Array(); 422 | var excludedFields = new Array(); 423 | var finalFields = ""; 424 | 425 | for(field in fieldsArray) 426 | { 427 | if(field == "*") 428 | finalFields = "*, " + finalFields; 429 | else if(field.charAt(0) != '!') 430 | { 431 | if(grant.Utils.linearSearch(includedFields, field) == -1) 432 | { 433 | includedFields.push(field); 434 | finalFields += field + ","; 435 | } 436 | } 437 | else 438 | { 439 | if(Utils.linearSearch(excludedFields, field) == -1) 440 | { 441 | excludedFields.push(field); 442 | finalFields += field + ","; 443 | } 444 | } 445 | } 446 | 447 | return finalFields; 448 | } 449 | } 450 | --------------------------------------------------------------------------------