Browse Source

Added tenant_filter functionality

Maurits van der Schee 8 years ago
parent
commit
ac098bc2d3
3 changed files with 66 additions and 11 deletions
  1. 7
    4
      README.md
  2. 38
    3
      api.php
  3. 21
    4
      tests/tests.php

+ 7
- 4
README.md View File

@@ -477,12 +477,15 @@ By default a single database is exposed with all it's tables and columns in read
477 477
 a 'table_authorizer' and/or a 'column_authorizer' function that returns a boolean indicating whether or not the table or column is allowed
478 478
 for a specific CRUD action.
479 479
 
480
+## Record filter
481
+
482
+By defining a 'record_filter' function you can apply a forced filter, for instance to implement roles in a database system.
483
+The rule "you cannot view unpublished blog posts unless you have the editor or admin role" can be implemented with this filter.
484
+
480 485
 ## Multi-tenancy
481 486
 
482
-By defining a 'record_filter' function that returns an array of filters you can support a multi-tenant database.
483
-When you add a 'company_id' column to every table and let 'record_filter' function return ```array('company_id,eq,1')```
484
-you can limit access to records from company 1. The returned filter is added to list, read, update and delete commands.
485
-NB: You still have to make sure only allowed values are sent when creating new records.
487
+The 'tenant_function' allows you to expose an API for a multi-tenant database schema. In the simplest model all tables have a column
488
+named 'customer_id' and the 'tenant_function' is defined as ```return array('customer_id,eq,'.$_SESSION['customer_id']);````
486 489
 
487 490
 ## Sanitizing input
488 491
 

+ 38
- 3
api.php View File

@@ -561,7 +561,7 @@ class REST_CRUD_API {
561 561
 		}
562 562
 	}
563 563
 
564
-	protected function applyRecordAuthorizer($callback,$action,$database,$tables,&$filters) {
564
+	protected function applyRecordFilter($callback,$action,$database,$tables,&$filters) {
565 565
 		if (is_callable($callback,true)) foreach ($tables as $i=>$table) {
566 566
 			$f = $this->convertFilters($callback($action,$database,$table));
567 567
 			if ($f) {
@@ -572,6 +572,19 @@ class REST_CRUD_API {
572 572
 		}
573 573
 	}
574 574
 
575
+	protected function applyTenancyFunction($callback,$action,$database,$fields,&$filters) {
576
+		if (is_callable($callback,true)) foreach ($fields as $table=>$keys) {
577
+			foreach ($keys as $field) {
578
+				$v = $callback($action,$database,$table,$field->name);
579
+				if ($v!==null) {
580
+					if (!isset($filters[$table])) $filters[$table] = array();
581
+					if (!isset($filters[$table]['and'])) $filters[$table]['and'] = array();
582
+					$filters[$table]['and'][] = array($field->name,is_array($v)?'IN':'=',$v);
583
+				}
584
+			}
585
+		}
586
+	}
587
+
575 588
 	protected function applyColumnAuthorizer($callback,$action,$database,&$fields) {
576 589
 		if (is_callable($callback,true)) foreach ($fields as $table=>$keys) {
577 590
 			foreach ($keys as $field) {
@@ -582,6 +595,24 @@ class REST_CRUD_API {
582 595
 		}
583 596
 	}
584 597
 
598
+	protected function applyInputTenancy($callback,$action,$database,$table,&$input,$keys) {
599
+		if (is_callable($callback,true)) foreach ((array)$input as $key=>$value) {
600
+			if (isset($keys[$key])) {
601
+				$v = $callback($action,$database,$table,$key);
602
+				if ($v!==null) {
603
+					if (is_array($v)) {
604
+						if (in_array($input->$key,$v)) {
605
+							$v = $input->$key;
606
+						} else {
607
+							$v = null;
608
+						}
609
+					}
610
+					$input->$key = $v;
611
+				}
612
+			}
613
+		}
614
+	}
615
+
585 616
 	protected function applyInputSanitizer($callback,$action,$database,$table,&$input,$keys) {
586 617
 		if (is_callable($callback,true)) foreach ((array)$input as $key=>$value) {
587 618
 			if (isset($keys[$key])) {
@@ -938,14 +969,16 @@ class REST_CRUD_API {
938 969
 
939 970
 		// permissions
940 971
 		if ($table_authorizer) $this->applyTableAuthorizer($table_authorizer,$action,$database,$tables);
941
-		if ($record_filter) $this->applyRecordAuthorizer($record_filter,$action,$database,$tables,$filters);
972
+		if ($record_filter) $this->applyRecordFilter($record_filter,$action,$database,$tables,$filters);
942 973
 		if ($column_authorizer) $this->applyColumnAuthorizer($column_authorizer,$action,$database,$fields);
974
+		if ($tenancy_function) $this->applyTenancyFunction($tenancy_function,$action,$database,$fields,$filters);
943 975
 
944 976
 		if ($post) {
945 977
 			// input
946 978
 			$context = $this->retrieveInput($post);
947 979
 			$input = $this->filterInputByColumns($context,$fields[$tables[0]]);
948 980
 
981
+			if ($tenancy_function) $this->applyInputTenancy($tenancy_function,$action,$database,$tables[0],$input,$fields[$tables[0]]);
949 982
 			if ($input_sanitizer) $this->applyInputSanitizer($input_sanitizer,$action,$database,$tables[0],$input,$fields[$tables[0]]);
950 983
 			if ($input_validator) $this->applyInputValidator($input_validator,$action,$database,$tables[0],$input,$fields[$tables[0]],$context);
951 984
 
@@ -976,6 +1009,7 @@ class REST_CRUD_API {
976 1009
 				$params[] = $filter[0];
977 1010
 				$params[] = $filter[1];
978 1011
 				$params[] = $filter[2];
1012
+				$first = false;
979 1013
 			}
980 1014
 		}
981 1015
 	}
@@ -1172,6 +1206,7 @@ class REST_CRUD_API {
1172 1206
 		$table_authorizer = isset($table_authorizer)?$table_authorizer:null;
1173 1207
 		$record_filter = isset($record_filter)?$record_filter:null;
1174 1208
 		$column_authorizer = isset($column_authorizer)?$column_authorizer:null;
1209
+		$tenancy_function = isset($tenancy_function)?$tenancy_function:null;
1175 1210
 		$input_sanitizer = isset($input_sanitizer)?$input_sanitizer:null;
1176 1211
 		$input_validator = isset($input_validator)?$input_validator:null;
1177 1212
 
@@ -1207,7 +1242,7 @@ class REST_CRUD_API {
1207 1242
 			$db = $this->connectDatabase($hostname,$username,$password,$database,$port,$socket,$charset);
1208 1243
 		}
1209 1244
 
1210
-		$this->settings = compact('method', 'request', 'get', 'post', 'database', 'table_authorizer', 'record_filter', 'column_authorizer', 'input_sanitizer', 'input_validator', 'db');
1245
+		$this->settings = compact('method', 'request', 'get', 'post', 'database', 'table_authorizer', 'record_filter', 'column_authorizer', 'tenancy_function', 'input_sanitizer', 'input_validator', 'db');
1211 1246
 	}
1212 1247
 
1213 1248
 	public static function php_crud_api_transform(&$tables) {

+ 21
- 4
tests/tests.php View File

@@ -37,8 +37,9 @@ class API
37 37
 				'database'=>MySQL_CRUD_API_Config::$database,
38 38
 				// callbacks
39 39
 				'table_authorizer'=>function($action,$database,$table) { return true; },
40
-				'record_filter'=>function($action,$database,$table) { return ($table=='users'&&$action!='list')?array('id,eq,1'):false; },
41 40
 				'column_authorizer'=>function($action,$database,$table,$column) { return !($column=='password'&&$action=='list'); },
41
+				'record_filter'=>function($action,$database,$table) { return ($table=='posts')?array('id,ne,13'):false; },
42
+				'tenancy_function'=>function($action,$database,$table,$column) { return ($table=='users'&&$column=='id')?1:null; },
42 43
 				'input_sanitizer'=>function($action,$database,$table,$column,$type,$value) { return $value===null?null:strip_tags($value); },
43 44
 				'input_validator'=>function($action,$database,$table,$column,$type,$value,$context) { return ($column=='category_id' && !is_numeric($value))?'must be numeric':true; },
44 45
 				// for tests
@@ -305,14 +306,14 @@ class MySQL_CRUD_API_Test extends PHPUnit_Framework_TestCase
305 306
 		  $test->expect(4+$i);
306 307
 		}
307 308
 		$test->get('/posts?page=2,2&order=id');
308
-		$test->expect('{"posts":{"columns":["id","user_id","category_id","content"],"records":[["5","1","1","#1"],["6","1","1","#2"]],"results":12}}');
309
+		$test->expect('{"posts":{"columns":["id","user_id","category_id","content"],"records":[["5","1","1","#1"],["6","1","1","#2"]],"results":11}}');
309 310
 	}
310 311
 
311 312
 	public function testListWithPaginateLastPage()
312 313
 	{
313 314
 		$test = new API($this);
314 315
 		$test->get('/posts?page=3,5&order=id');
315
-		$test->expect('{"posts":{"columns":["id","user_id","category_id","content"],"records":[["13","1","1","#9"],["14","1","1","#10"]],"results":12}}');
316
+		$test->expect('{"posts":{"columns":["id","user_id","category_id","content"],"records":[["14","1","1","#10"]],"results":11}}');
316 317
 	}
317 318
 
318 319
 	public function testListExampleFromReadme()
@@ -438,7 +439,7 @@ class MySQL_CRUD_API_Test extends PHPUnit_Framework_TestCase
438 439
 	{
439 440
 		$test = new API($this);
440 441
 		$test->get('/users,posts,tags');
441
-		$test->expect('{"users":{"columns":["id","username"],"records":[["1","user1"],["2","user2"]]},"posts":{"relations":{"user_id":"users.id"},"columns":["id","user_id","category_id","content"],"records":[["1","1","1","blog started"],["2","1","2","\u20ac Hello world, \u039a\u03b1\u03bb\u03b7\u03bc\u1f73\u03c1\u03b1 \u03ba\u1f79\u03c3\u03bc\u03b5, \u30b3\u30f3\u30cb\u30c1\u30cf"],["5","1","1","#1"],["6","1","1","#2"],["7","1","1","#3"],["8","1","1","#4"],["9","1","1","#5"],["10","1","1","#6"],["11","1","1","#7"],["12","1","1","#8"],["13","1","1","#9"],["14","1","1","#10"]]},"post_tags":{"relations":{"post_id":"posts.id"},"columns":["id","post_id","tag_id"],"records":[["1","1","1"],["2","1","2"],["3","2","1"],["4","2","2"]]},"tags":{"relations":{"id":"post_tags.tag_id"},"columns":["id","name"],"records":[["1","funny"],["2","important"]]}}');
442
+		$test->expect('{"users":{"columns":["id","username"],"records":[["1","user1"]]},"posts":{"relations":{"user_id":"users.id"},"columns":["id","user_id","category_id","content"],"records":[["1","1","1","blog started"],["2","1","2","\u20ac Hello world, \u039a\u03b1\u03bb\u03b7\u03bc\u1f73\u03c1\u03b1 \u03ba\u1f79\u03c3\u03bc\u03b5, \u30b3\u30f3\u30cb\u30c1\u30cf"],["5","1","1","#1"],["6","1","1","#2"],["7","1","1","#3"],["8","1","1","#4"],["9","1","1","#5"],["10","1","1","#6"],["11","1","1","#7"],["12","1","1","#8"],["14","1","1","#10"]]},"post_tags":{"relations":{"post_id":"posts.id"},"columns":["id","post_id","tag_id"],"records":[["1","1","1"],["2","1","2"],["3","2","1"],["4","2","2"]]},"tags":{"relations":{"id":"post_tags.tag_id"},"columns":["id","name"],"records":[["1","funny"],["2","important"]]}}');
442 443
 	}
443 444
 
444 445
 	public function testEditUser()
@@ -448,6 +449,22 @@ class MySQL_CRUD_API_Test extends PHPUnit_Framework_TestCase
448 449
 		$test->expect('1');
449 450
 	}
450 451
 
452
+	public function testEditUserWithId()
453
+	{
454
+		$test = new API($this);
455
+		$test->put('/users/1','{"id":"2","password":"testtest2"}');
456
+		$test->expect('1');
457
+		$test->get('/users/1');
458
+		$test->expect('{"id":"1","username":"user1","password":"testtest2"}');
459
+	}
460
+
461
+	public function testReadOtherUser()
462
+	{
463
+		$test = new API($this);
464
+		$test->get('/users/2');
465
+		$test->expect(false,'Not found (object)');
466
+	}
467
+
451 468
 	public function testEditOtherUser()
452 469
 	{
453 470
 		$test = new API($this);

Loading…
Cancel
Save