Maurits van der Schee 6 years ago
parent
commit
067b93b799

+ 12
- 0
README.md View File

@@ -681,6 +681,18 @@ Then the server will return a '422' HTTP status code and nice error message:
681 681
 
682 682
 You can parse this output to make form fields show up with a red border and their appropriate error message.
683 683
 
684
+### Multi Tenancy support
685
+
686
+You may use the "multiTenancy" middleware when you have a multi-tenant database. 
687
+If your tenants are identified by the "customer_id" column you can use the following handler:
688
+
689
+    'multiTenancy.handler' => function ($operation, $tableName) {
690
+        return ['customer_id' => 12];
691
+    },
692
+
693
+This construct adds a filter requiring "customer_id" to be "12" to every operation (except for "create").
694
+It also sets the column "customer_id" on "create" to "12" and removes the column from any other write operation.
695
+
684 696
 ## OpenAPI specification
685 697
 
686 698
 On the "/openapi" end-point the OpenAPI 3.0 (formerly called "Swagger") specification is served. 

+ 4
- 0
src/Tqdev/PhpCrudApi/Api.php View File

@@ -15,6 +15,7 @@ use Tqdev\PhpCrudApi\Middleware\BasicAuthMiddleware;
15 15
 use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
16 16
 use Tqdev\PhpCrudApi\Middleware\FirewallMiddleware;
17 17
 use Tqdev\PhpCrudApi\Middleware\JwtAuthMiddleware;
18
+use Tqdev\PhpCrudApi\Middleware\MultiTenancyMiddleware;
18 19
 use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;
19 20
 use Tqdev\PhpCrudApi\Middleware\SanitationMiddleware;
20 21
 use Tqdev\PhpCrudApi\Middleware\ValidationMiddleware;
@@ -62,6 +63,9 @@ class Api
62 63
                 case 'sanitation':
63 64
                     new SanitationMiddleware($router, $responder, $properties, $reflection);
64 65
                     break;
66
+                case 'multiTenancy':
67
+                    new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
68
+                    break;
65 69
                 case 'authorization':
66 70
                     new AuthorizationMiddleware($router, $responder, $properties, $reflection);
67 71
                     break;

+ 0
- 4
src/Tqdev/PhpCrudApi/Column/ReflectionService.php View File

@@ -88,8 +88,4 @@ class ReflectionService
88 88
         return $this->database->removeTable($tableName);
89 89
     }
90 90
 
91
-    public function removeColumn(String $tableName, String $columnName): bool
92
-    {
93
-        return $this->getTable($tableName)->removeColumn($columnName);
94
-    }
95 91
 }

+ 17
- 11
src/Tqdev/PhpCrudApi/Database/GenericDB.php View File

@@ -3,7 +3,6 @@ namespace Tqdev\PhpCrudApi\Database;
3 3
 
4 4
 use Tqdev\PhpCrudApi\Column\Reflection\ReflectedTable;
5 5
 use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
6
-use Tqdev\PhpCrudApi\Record\Condition\AndCondition;
7 6
 use Tqdev\PhpCrudApi\Record\Condition\ColumnCondition;
8 7
 use Tqdev\PhpCrudApi\Record\Condition\Condition;
9 8
 
@@ -98,10 +97,17 @@ class GenericDB
98 97
         return $this->definition;
99 98
     }
100 99
 
101
-    private function addAuthorizationCondition(String $tableName, Condition $condition2): Condition
100
+    private function addMiddlewareConditions(String $tableName, Condition $condition): Condition
102 101
     {
103 102
         $condition1 = VariableStore::get("authorization.conditions.$tableName");
104
-        return $condition1 ? AndCondition::fromArray([$condition1, $condition2]) : $condition2;
103
+        if ($condition1) {
104
+            $condition = $condition->_and($condition1);
105
+        }
106
+        $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
107
+        if ($condition2) {
108
+            $condition = $condition->_and($condition2);
109
+        }
110
+        return $condition;
105 111
     }
106 112
 
107 113
     public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
@@ -131,7 +137,7 @@ class GenericDB
131 137
         $selectColumns = $this->columns->getSelect($table, $columnNames);
132 138
         $tableName = $table->getName();
133 139
         $condition = new ColumnCondition($table->getPk(), 'eq', $id);
134
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
140
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
135 141
         $parameters = array();
136 142
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
137 143
         $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
@@ -153,7 +159,7 @@ class GenericDB
153 159
         $selectColumns = $this->columns->getSelect($table, $columnNames);
154 160
         $tableName = $table->getName();
155 161
         $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
156
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
162
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
157 163
         $parameters = array();
158 164
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
159 165
         $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
@@ -166,7 +172,7 @@ class GenericDB
166 172
     public function selectCount(ReflectedTable $table, Condition $condition): int
167 173
     {
168 174
         $tableName = $table->getName();
169
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
175
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
170 176
         $parameters = array();
171 177
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
172 178
         $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
@@ -178,7 +184,7 @@ class GenericDB
178 184
     {
179 185
         $selectColumns = $this->columns->getSelect($table, $columnNames);
180 186
         $tableName = $table->getName();
181
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
187
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
182 188
         $parameters = array();
183 189
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
184 190
         $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause;
@@ -195,7 +201,7 @@ class GenericDB
195 201
         }
196 202
         $selectColumns = $this->columns->getSelect($table, $columnNames);
197 203
         $tableName = $table->getName();
198
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
204
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
199 205
         $parameters = array();
200 206
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
201 207
         $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
@@ -216,7 +222,7 @@ class GenericDB
216 222
         $updateColumns = $this->columns->getUpdate($table, $columnValues);
217 223
         $tableName = $table->getName();
218 224
         $condition = new ColumnCondition($table->getPk(), 'eq', $id);
219
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
225
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
220 226
         $parameters = array_values($columnValues);
221 227
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
222 228
         $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
@@ -228,7 +234,7 @@ class GenericDB
228 234
     {
229 235
         $tableName = $table->getName();
230 236
         $condition = new ColumnCondition($table->getPk(), 'eq', $id);
231
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
237
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
232 238
         $parameters = array();
233 239
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
234 240
         $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
@@ -245,7 +251,7 @@ class GenericDB
245 251
         $updateColumns = $this->columns->getIncrement($table, $columnValues);
246 252
         $tableName = $table->getName();
247 253
         $condition = new ColumnCondition($table->getPk(), 'eq', $id);
248
-        $condition = $this->addAuthorizationCondition($tableName, $condition);
254
+        $condition = $this->addMiddlewareConditions($tableName, $condition);
249 255
         $parameters = array_values($columnValues);
250 256
         $whereClause = $this->conditions->getWhereClause($condition, $parameters);
251 257
         $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;

+ 1
- 1
src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php View File

@@ -30,7 +30,7 @@ class AuthorizationMiddleware extends Middleware
30 30
             foreach ($table->columnNames() as $columnName) {
31 31
                 $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
32 32
                 if (!$allowed) {
33
-                    $this->reflection->removeColumn($tableName, $columnName);
33
+                    $table->removeColumn($columnName);
34 34
                 }
35 35
             }
36 36
         }

+ 4
- 1
tests/config/base.php View File

@@ -4,7 +4,7 @@ $settings = [
4 4
     'username' => 'php-crud-api',
5 5
     'password' => 'php-crud-api',
6 6
     'controllers' => 'records,columns,cache,openapi',
7
-    'middlewares' => 'cors,jwtAuth,basicAuth,authorization,validation,sanitation',
7
+    'middlewares' => 'cors,jwtAuth,basicAuth,authorization,validation,sanitation,multiTenancy',
8 8
     'jwtAuth.time' => '1538207605',
9 9
     'jwtAuth.secret' => 'axpIrCGNGqxzx2R9dtXLIPUSqPo778uhb8CA0F4Hx',
10 10
     'basicAuth.passwordFile' => __DIR__ . DIRECTORY_SEPARATOR . '.htpasswd',
@@ -23,5 +23,8 @@ $settings = [
23 23
     'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
24 24
         return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
25 25
     },
26
+    'multiTenancy.handler' => function ($operation, $tableName) {
27
+        return ($tableName == 'kunsthåndværk') ? ['user_id' => 1] : [];
28
+    },
26 29
     'debug' => true,
27 30
 ];

+ 6
- 3
tests/fixtures/blog_mysql.sql View File

@@ -152,13 +152,16 @@ DROP TABLE IF EXISTS `kunsthåndværk`;
152 152
 CREATE TABLE `kunsthåndværk` (
153 153
   `id` varchar(36) NOT NULL,
154 154
   `Umlauts ä_ö_ü-COUNT` int(11) NOT NULL,
155
+  `user_id` int(11) NOT NULL,
155 156
   `invisible` varchar(36),
156 157
   PRIMARY KEY (`id`),
157
-  CONSTRAINT `kunsthåndværk_Umlauts ä_ö_ü-COUNT_fkey` UNIQUE (`Umlauts ä_ö_ü-COUNT`)
158
+  CONSTRAINT `kunsthåndværk_Umlauts ä_ö_ü-COUNT_fkey` UNIQUE (`Umlauts ä_ö_ü-COUNT`),
159
+  CONSTRAINT `kunsthåndværk_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
158 160
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
159 161
 
160
-INSERT INTO `kunsthåndværk` (`id`, `Umlauts ä_ö_ü-COUNT`, `invisible`) VALUES
161
-('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, NULL);
162
+INSERT INTO `kunsthåndværk` (`id`, `Umlauts ä_ö_ü-COUNT`, `user_id`, `invisible`) VALUES
163
+('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL),
164
+('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL);
162 165
 
163 166
 DROP TABLE IF EXISTS `invisibles`;
164 167
 CREATE TABLE `invisibles` (

+ 18
- 2
tests/fixtures/blog_pgsql.sql View File

@@ -159,6 +159,7 @@ CREATE TABLE barcodes (
159 159
 CREATE TABLE "kunsthåndværk" (
160 160
   id character varying(36) NOT NULL,
161 161
   "Umlauts ä_ö_ü-COUNT" integer NOT NULL,
162
+  user_id integer NOT NULL,
162 163
   invisible character varying(36)
163 164
 );
164 165
 
@@ -263,8 +264,9 @@ INSERT INTO "barcodes" ("product_id", "hex", "bin") VALUES
263 264
 -- Data for Name: kunsthåndværk; Type: TABLE DATA; Schema: public; Owner: postgres
264 265
 --
265 266
 
266
-INSERT INTO "kunsthåndværk" ("id", "Umlauts ä_ö_ü-COUNT", "invisible") VALUES
267
-('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, NULL);
267
+INSERT INTO "kunsthåndværk" ("id", "Umlauts ä_ö_ü-COUNT", "user_id", "invisible") VALUES
268
+('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL),
269
+('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL);
268 270
 
269 271
 --
270 272
 -- Data for Name: invisibles; Type: TABLE DATA; Schema: public; Owner: postgres
@@ -429,6 +431,13 @@ CREATE INDEX barcodes_product_id_idx ON barcodes USING btree (product_id);
429 431
 CREATE INDEX "kunsthåndværk_Umlauts ä_ö_ü-COUNT_idx" ON "kunsthåndværk" USING btree ("Umlauts ä_ö_ü-COUNT");
430 432
 
431 433
 
434
+--
435
+-- Name: kunsthåndværk_user_id_idx; Type: INDEX; Schema: public; Owner: postgres; Tablespace:
436
+--
437
+
438
+CREATE INDEX "kunsthåndværk_user_id_idx" ON "kunsthåndværk" USING btree (user_id);
439
+
440
+
432 441
 --
433 442
 -- Name: comments_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
434 443
 --
@@ -484,6 +493,13 @@ ALTER TABLE ONLY barcodes
484 493
 ALTER TABLE ONLY "kunsthåndværk"
485 494
     ADD CONSTRAINT "kunsthåndværk_Umlauts ä_ö_ü-COUNT_uc" UNIQUE ("Umlauts ä_ö_ü-COUNT");
486 495
 
496
+--
497
+-- Name: kunsthåndværk_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres
498
+--
499
+
500
+ALTER TABLE ONLY "kunsthåndværk"
501
+    ADD CONSTRAINT "kunsthåndværk_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id);
502
+
487 503
 
488 504
 --
489 505
 -- PostgreSQL database dump complete

+ 10
- 1
tests/fixtures/blog_sqlsrv.sql View File

@@ -224,6 +224,7 @@ GO
224 224
 CREATE TABLE [kunsthåndværk](
225 225
 	[id] [nvarchar](36),
226 226
 	[Umlauts ä_ö_ü-COUNT] [int] NOT NULL,
227
+	[user_id] [int] NOT NULL,
227 228
 	[invisible] [nvarchar](36),
228 229
 	CONSTRAINT [PK_kunsthåndværk]
229 230
 	PRIMARY KEY CLUSTERED([id] ASC)
@@ -294,7 +295,9 @@ GO
294 295
 INSERT [barcodes] ([product_id], [hex], [bin]) VALUES (1, N'00ff01', 0x00ff01)
295 296
 GO
296 297
 
297
-INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [invisible]) VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, NULL)
298
+INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [user_id], [invisible]) VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, 1, NULL)
299
+GO
300
+INSERT [kunsthåndværk] ([id], [Umlauts ä_ö_ü-COUNT], [user_id], [invisible]) VALUES ('e31ecfe6-591f-4660-9fbd-1a232083037f', 2, 2, NULL)
298 301
 GO
299 302
 
300 303
 INSERT [invisibles] ([id]) VALUES ('e42c77c6-06a4-4502-816c-d112c7142e6d')
@@ -341,3 +344,9 @@ GO
341 344
 
342 345
 ALTER TABLE [kunsthåndværk]  WITH CHECK ADD 	CONSTRAINT [UC_kunsthåndværk_Umlauts ä_ö_ü-COUNT] UNIQUE([Umlauts ä_ö_ü-COUNT])
343 346
 GO
347
+
348
+ALTER TABLE [kunsthåndværk]  WITH CHECK ADD 	CONSTRAINT [FK_kunsthåndværk_users] FOREIGN KEY([user_id])
349
+REFERENCES [users] ([id])
350
+GO
351
+ALTER TABLE [kunsthåndværk] CHECK	CONSTRAINT [FK_kunsthåndværk_users]
352
+GO

+ 2
- 2
tests/functional/001_records/063_list_kunsthåndværk.log View File

@@ -2,6 +2,6 @@ GET /records/kunsthåndværk
2 2
 ===
3 3
 200
4 4
 Content-Type: application/json
5
-Content-Length: 86
5
+Content-Length: 98
6 6
 
7
-{"records":[{"id":"e42c77c6-06a4-4502-816c-d112c7142e6d","Umlauts ä_ö_ü-COUNT":1}]}
7
+{"records":[{"id":"e42c77c6-06a4-4502-816c-d112c7142e6d","Umlauts ä_ö_ü-COUNT":1,"user_id":1}]}

+ 1
- 1
tests/functional/001_records/064_add_kunsthåndværk.log View File

@@ -1,6 +1,6 @@
1 1
 POST /records/kunsthåndværk
2 2
 
3
-{"id":"34451583-a747-4417-bdf0-bec7a5eacffa","Umlauts ä_ö_ü-COUNT":2}
3
+{"id":"34451583-a747-4417-bdf0-bec7a5eacffa","Umlauts ä_ö_ü-COUNT":3}
4 4
 ===
5 5
 200
6 6
 Content-Type: application/json

+ 2
- 2
tests/functional/003_columns/001_get_database.log View File

@@ -2,6 +2,6 @@ GET /columns
2 2
 ===
3 3
 200
4 4
 Content-Type: application/json
5
-Content-Length: 2224
5
+Content-Length: 2273
6 6
 
7
-{"name":"php-crud-api","tables":[{"name":"barcodes","columns":[{"name":"id","type":"integer","pk":true},{"name":"product_id","type":"integer","fk":"products"},{"name":"hex","type":"varchar","length":255},{"name":"bin","type":"blob"}]},{"name":"categories","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"icon","type":"blob","nullable":true}]},{"name":"comments","columns":[{"name":"id","type":"integer","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"message","type":"varchar","length":255}]},{"name":"countries","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"shape","type":"geometry"}]},{"name":"events","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"datetime","type":"timestamp"},{"name":"visitors","type":"integer"}]},{"name":"kunsthåndværk","columns":[{"name":"id","type":"varchar","length":36,"pk":true},{"name":"Umlauts ä_ö_ü-COUNT","type":"integer"}]},{"name":"nopk","columns":[{"name":"id","type":"varchar","length":36}]},{"name":"post_tags","columns":[{"name":"id","type":"integer","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"tag_id","type":"integer","fk":"tags"}]},{"name":"posts","columns":[{"name":"id","type":"integer","pk":true},{"name":"user_id","type":"integer","fk":"users"},{"name":"category_id","type":"integer","fk":"categories"},{"name":"content","type":"varchar","length":255}]},{"name":"products","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"price","type":"decimal","precision":10,"scale":2},{"name":"properties","type":"clob"},{"name":"created_at","type":"timestamp"},{"name":"deleted_at","type":"timestamp","nullable":true}]},{"name":"tags","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"is_important","type":"boolean"}]},{"name":"users","columns":[{"name":"id","type":"integer","pk":true},{"name":"username","type":"varchar","length":255},{"name":"password","type":"varchar","length":255},{"name":"location","type":"geometry","nullable":true}]}]}
7
+{"name":"php-crud-api","tables":[{"name":"barcodes","columns":[{"name":"id","type":"integer","pk":true},{"name":"product_id","type":"integer","fk":"products"},{"name":"hex","type":"varchar","length":255},{"name":"bin","type":"blob"}]},{"name":"categories","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"icon","type":"blob","nullable":true}]},{"name":"comments","columns":[{"name":"id","type":"integer","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"message","type":"varchar","length":255}]},{"name":"countries","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"shape","type":"geometry"}]},{"name":"events","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"datetime","type":"timestamp"},{"name":"visitors","type":"integer"}]},{"name":"kunsthåndværk","columns":[{"name":"id","type":"varchar","length":36,"pk":true},{"name":"Umlauts ä_ö_ü-COUNT","type":"integer"},{"name":"user_id","type":"integer","fk":"users"}]},{"name":"nopk","columns":[{"name":"id","type":"varchar","length":36}]},{"name":"post_tags","columns":[{"name":"id","type":"integer","pk":true},{"name":"post_id","type":"integer","fk":"posts"},{"name":"tag_id","type":"integer","fk":"tags"}]},{"name":"posts","columns":[{"name":"id","type":"integer","pk":true},{"name":"user_id","type":"integer","fk":"users"},{"name":"category_id","type":"integer","fk":"categories"},{"name":"content","type":"varchar","length":255}]},{"name":"products","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"price","type":"decimal","precision":10,"scale":2},{"name":"properties","type":"clob"},{"name":"created_at","type":"timestamp"},{"name":"deleted_at","type":"timestamp","nullable":true}]},{"name":"tags","columns":[{"name":"id","type":"integer","pk":true},{"name":"name","type":"varchar","length":255},{"name":"is_important","type":"boolean"}]},{"name":"users","columns":[{"name":"id","type":"integer","pk":true},{"name":"username","type":"varchar","length":255},{"name":"password","type":"varchar","length":255},{"name":"location","type":"geometry","nullable":true}]}]}

Loading…
Cancel
Save