Skip to content

Commit 650b26d

Browse files
committed
Add authorization handlers
1 parent 82c8d1b commit 650b26d

File tree

9 files changed

+218
-38
lines changed

9 files changed

+218
-38
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,22 @@ For spatial support there is an extra set of filters that can be applied on geom
549549

550550
These filters are based on OGC standards and so is the WKT specification in which the geometry columns are represented.
551551

552+
### Authorizing tables and columns
553+
554+
By default all tables are reflected. If you want to hide some tables you may add the 'authorization' middleware and define a 'authorization.tableHandler' function that returns 'false' for hidden tables.
555+
556+
'authorization.tableHandler' => function ($method, $path, $databaseName, $tableName) {
557+
return $tableName != 'license_keys';
558+
},
559+
560+
The above example will hide the table 'license_keys' in all API input and output.
561+
562+
'authorization.columnHandler' => function ($method, $path, $databaseName, $tableName, $columnName) {
563+
return !($tableName == 'users' && $columnName == 'password');
564+
},
565+
566+
The above example will hide the 'password' field from the 'users' table in all API input and output.
567+
552568
### Sanitizing input
553569

554570
By default all input is accepted and sent to the database. If you want to strip (certain) HTML tags before storing you may add the 'sanitation' middleware and define a 'sanitation.handler' function that returns the adjusted value.

api.php

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,15 @@ public function getTableNames(): array
530530
return array_keys($this->tableNames);
531531
}
532532

533+
public function removeTable(String $tableName): bool
534+
{
535+
if (!isset($this->tableNames[$tableName])) {
536+
return false;
537+
}
538+
unset($this->tableNames[$tableName]);
539+
return true;
540+
}
541+
533542
public function serialize()
534543
{
535544
return [
@@ -652,6 +661,15 @@ public function getFksTo(String $tableName): array
652661
return $columns;
653662
}
654663

664+
public function removeColumn(String $columnName): bool
665+
{
666+
if (!isset($this->columns[$columnName])) {
667+
return false;
668+
}
669+
unset($this->columns[$columnName]);
670+
return true;
671+
}
672+
655673
public function serialize()
656674
{
657675
return [
@@ -893,6 +911,17 @@ public function getDatabaseName(): String
893911
{
894912
return $this->database->getName();
895913
}
914+
915+
public function removeTable(String $tableName): bool
916+
{
917+
unset($this->tables[$tableName]);
918+
return $this->database->removeTable($tableName);
919+
}
920+
921+
public function removeColumn(String $tableName, String $columnName): bool
922+
{
923+
return $this->getTable($tableName)->removeColumn($columnName);
924+
}
896925
}
897926

898927
// file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
@@ -2774,34 +2803,79 @@ public function __construct(Router $router, Responder $responder, array $propert
27742803
$this->reflection = $reflection;
27752804
}
27762805

2777-
private function getJoins($all, $list)
2806+
private function handleColumns(String $method, String $path, String $databaseName, String $tableName): void
27782807
{
2779-
$result = array_fill_keys($all, false);
2780-
foreach ($lists as $items) {
2781-
foreach (explode(',', $items) as $item) {
2782-
if (isset($result[$item])) {
2783-
$result[$item] = true;
2808+
$columnHandler = $this->getProperty('columnHandler', '');
2809+
if ($columnHandler) {
2810+
$table = $this->reflection->getTable($tableName);
2811+
foreach ($table->columnNames() as $columnName) {
2812+
$allowed = call_user_func($columnHandler, $method, $path, $databaseName, $tableName, $columnName);
2813+
if (!$allowed) {
2814+
$this->reflection->removeColumn($tableName, $columnName);
27842815
}
27852816
}
27862817
}
2787-
return $result;
2818+
}
2819+
2820+
private function handleTable(String $method, String $path, String $databaseName, String $tableName): void
2821+
{
2822+
if (!$this->reflection->hasTable($tableName)) {
2823+
return;
2824+
}
2825+
$tableHandler = $this->getProperty('tableHandler', '');
2826+
if ($tableHandler) {
2827+
$allowed = call_user_func($tableHandler, $method, $path, $databaseName, $tableName);
2828+
if (!$allowed) {
2829+
$this->reflection->removeTable($tableName);
2830+
} else {
2831+
$this->handleColumns($method, $path, $databaseName, $tableName);
2832+
}
2833+
}
2834+
}
2835+
2836+
private function handleJoinTables(String $method, String $path, String $databaseName, array $joinParameters): void
2837+
{
2838+
$uniqueTableNames = array();
2839+
foreach ($joinParameters as $joinParameter) {
2840+
$tableNames = explode(',', trim($joinParameter));
2841+
foreach ($tableNames as $tableName) {
2842+
$uniqueTableNames[$tableName] = true;
2843+
}
2844+
}
2845+
foreach (array_keys($uniqueTableNames) as $tableName) {
2846+
$this->handleTable($method, $path, $databaseName, trim($tableName));
2847+
}
2848+
}
2849+
2850+
private function handleAllTables(String $method, String $path, String $databaseName): void
2851+
{
2852+
$tableNames = $this->reflection->getTableNames();
2853+
foreach ($tableNames as $tableName) {
2854+
$this->handleTable($method, $path, $databaseName, $tableName);
2855+
}
27882856
}
27892857

27902858
public function handle(Request $request): Response
27912859
{
2860+
$method = $request->getMethod();
27922861
$path = $request->getPathSegment(1);
2793-
$tableName = $request->getPathSegment(2);
2794-
$database = $this->reflection->getDatabase();
2795-
$handler = $this->getProperty('handler', '');
2796-
if ($handler !== '' && $path == 'records' && $database->exists($tableName)) {
2797-
$method = $request->getMethod();
2798-
$tableNames = $database->getTableNames();
2862+
$databaseName = $this->reflection->getDatabaseName();
2863+
if ($path == 'records') {
2864+
$tableName = $request->getPathSegment(2);
2865+
$this->handleTable($method, $path, $databaseName, $tableName);
27992866
$params = $request->getParams();
2800-
$joins = $this->getJoins($tableNames, $params['join']);
2801-
$allowed = call_user_func($handler, $method, $tableName, $joins);
2802-
if (!$allowed) {
2803-
return $this->responder->error(ErrorCode::OPERATION_FORBIDDEN, '');
2867+
if (isset($params['join'])) {
2868+
$this->handleJoinTables($method, $path, $databaseName, $params['join']);
2869+
}
2870+
} elseif ($path == 'columns') {
2871+
$tableName = $request->getPathSegment(2);
2872+
if ($tableName) {
2873+
$this->handleTable($method, $path, $databaseName, $tableName);
2874+
} else {
2875+
$this->handleAllTables($method, $path, $databaseName);
28042876
}
2877+
} elseif ($path == 'openapi') {
2878+
$this->handleAllTables($method, $path, $databaseName);
28052879
}
28062880
return $this->next->handle($request);
28072881
}

src/Tqdev/PhpCrudApi/Api.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Tqdev\PhpCrudApi\Controller\RecordController;
1111
use Tqdev\PhpCrudApi\Controller\Responder;
1212
use Tqdev\PhpCrudApi\Database\GenericDB;
13+
use Tqdev\PhpCrudApi\Middleware\AuthorizationMiddleware;
1314
use Tqdev\PhpCrudApi\Middleware\CorsMiddleware;
1415
use Tqdev\PhpCrudApi\Middleware\FirewallMiddleware;
1516
use Tqdev\PhpCrudApi\Middleware\Router\SimpleRouter;

src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ public function getTableNames(): array
5353
return array_keys($this->tableNames);
5454
}
5555

56+
public function removeTable(String $tableName): bool
57+
{
58+
if (!isset($this->tableNames[$tableName])) {
59+
return false;
60+
}
61+
unset($this->tableNames[$tableName]);
62+
return true;
63+
}
64+
5665
public function serialize()
5766
{
5867
return [

src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ public function getFksTo(String $tableName): array
115115
return $columns;
116116
}
117117

118+
public function removeColumn(String $columnName): bool
119+
{
120+
if (!isset($this->columns[$columnName])) {
121+
return false;
122+
}
123+
unset($this->columns[$columnName]);
124+
return true;
125+
}
126+
118127
public function serialize()
119128
{
120129
return [

src/Tqdev/PhpCrudApi/Column/ReflectionService.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,15 @@ public function getDatabaseName(): String
8181
{
8282
return $this->database->getName();
8383
}
84+
85+
public function removeTable(String $tableName): bool
86+
{
87+
unset($this->tables[$tableName]);
88+
return $this->database->removeTable($tableName);
89+
}
90+
91+
public function removeColumn(String $tableName, String $columnName): bool
92+
{
93+
return $this->getTable($tableName)->removeColumn($columnName);
94+
}
8495
}

src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Tqdev\PhpCrudApi\Controller\Responder;
66
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
77
use Tqdev\PhpCrudApi\Middleware\Router\Router;
8-
use Tqdev\PhpCrudApi\Record\ErrorCode;
98
use Tqdev\PhpCrudApi\Request;
109
use Tqdev\PhpCrudApi\Response;
1110

@@ -19,34 +18,79 @@ public function __construct(Router $router, Responder $responder, array $propert
1918
$this->reflection = $reflection;
2019
}
2120

22-
private function getJoins($all, $list)
21+
private function handleColumns(String $method, String $path, String $databaseName, String $tableName): void
2322
{
24-
$result = array_fill_keys($all, false);
25-
foreach ($lists as $items) {
26-
foreach (explode(',', $items) as $item) {
27-
if (isset($result[$item])) {
28-
$result[$item] = true;
23+
$columnHandler = $this->getProperty('columnHandler', '');
24+
if ($columnHandler) {
25+
$table = $this->reflection->getTable($tableName);
26+
foreach ($table->columnNames() as $columnName) {
27+
$allowed = call_user_func($columnHandler, $method, $path, $databaseName, $tableName, $columnName);
28+
if (!$allowed) {
29+
$this->reflection->removeColumn($tableName, $columnName);
2930
}
3031
}
3132
}
32-
return $result;
33+
}
34+
35+
private function handleTable(String $method, String $path, String $databaseName, String $tableName): void
36+
{
37+
if (!$this->reflection->hasTable($tableName)) {
38+
return;
39+
}
40+
$tableHandler = $this->getProperty('tableHandler', '');
41+
if ($tableHandler) {
42+
$allowed = call_user_func($tableHandler, $method, $path, $databaseName, $tableName);
43+
if (!$allowed) {
44+
$this->reflection->removeTable($tableName);
45+
} else {
46+
$this->handleColumns($method, $path, $databaseName, $tableName);
47+
}
48+
}
49+
}
50+
51+
private function handleJoinTables(String $method, String $path, String $databaseName, array $joinParameters): void
52+
{
53+
$uniqueTableNames = array();
54+
foreach ($joinParameters as $joinParameter) {
55+
$tableNames = explode(',', trim($joinParameter));
56+
foreach ($tableNames as $tableName) {
57+
$uniqueTableNames[$tableName] = true;
58+
}
59+
}
60+
foreach (array_keys($uniqueTableNames) as $tableName) {
61+
$this->handleTable($method, $path, $databaseName, trim($tableName));
62+
}
63+
}
64+
65+
private function handleAllTables(String $method, String $path, String $databaseName): void
66+
{
67+
$tableNames = $this->reflection->getTableNames();
68+
foreach ($tableNames as $tableName) {
69+
$this->handleTable($method, $path, $databaseName, $tableName);
70+
}
3371
}
3472

3573
public function handle(Request $request): Response
3674
{
75+
$method = $request->getMethod();
3776
$path = $request->getPathSegment(1);
38-
$tableName = $request->getPathSegment(2);
39-
$database = $this->reflection->getDatabase();
40-
$handler = $this->getProperty('handler', '');
41-
if ($handler !== '' && $path == 'records' && $database->exists($tableName)) {
42-
$method = $request->getMethod();
43-
$tableNames = $database->getTableNames();
77+
$databaseName = $this->reflection->getDatabaseName();
78+
if ($path == 'records') {
79+
$tableName = $request->getPathSegment(2);
80+
$this->handleTable($method, $path, $databaseName, $tableName);
4481
$params = $request->getParams();
45-
$joins = $this->getJoins($tableNames, $params['join']);
46-
$allowed = call_user_func($handler, $method, $tableName, $joins);
47-
if (!$allowed) {
48-
return $this->responder->error(ErrorCode::OPERATION_FORBIDDEN, '');
82+
if (isset($params['join'])) {
83+
$this->handleJoinTables($method, $path, $databaseName, $params['join']);
84+
}
85+
} elseif ($path == 'columns') {
86+
$tableName = $request->getPathSegment(2);
87+
if ($tableName) {
88+
$this->handleTable($method, $path, $databaseName, $tableName);
89+
} else {
90+
$this->handleAllTables($method, $path, $databaseName);
4991
}
92+
} elseif ($path == 'openapi') {
93+
$this->handleAllTables($method, $path, $databaseName);
5094
}
5195
return $this->next->handle($request);
5296
}

tests/config/base.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33
'database' => 'php-crud-api',
44
'username' => 'php-crud-api',
55
'password' => 'php-crud-api',
6-
'middlewares' => 'cors,validation,sanitation',
6+
'middlewares' => 'cors,authorization,validation,sanitation',
7+
'authorization.tableHandler' => function ($method, $path, $databaseName, $tableName) {
8+
return !($tableName == 'invisibles');
9+
},
10+
'authorization.columnHandler' => function ($method, $path, $databaseName, $tableName, $columnName) {
11+
return !($columnName == 'invisible');
12+
},
713
'sanitation.handler' => function ($method, $tableName, $column, $value) {
814
return is_string($value) ? strip_tags($value) : $value;
915
},

tests/fixtures/blog_mysql.sql

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,22 @@ DROP TABLE IF EXISTS `kunsthåndværk`;
152152
CREATE TABLE `kunsthåndværk` (
153153
`id` varchar(36) NOT NULL,
154154
`Umlauts ä_ö_ü-COUNT` int(11) NOT NULL,
155+
`invisible` varchar(36),
155156
PRIMARY KEY (`id`),
156157
CONSTRAINT `kunsthåndværk_Umlauts ä_ö_ü-COUNT_fkey` UNIQUE (`Umlauts ä_ö_ü-COUNT`)
157158
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
158159

159-
INSERT INTO `kunsthåndværk` (`id`, `Umlauts ä_ö_ü-COUNT`) VALUES
160-
('e42c77c6-06a4-4502-816c-d112c7142e6d', 1);
160+
INSERT INTO `kunsthåndværk` (`id`, `Umlauts ä_ö_ü-COUNT`, `invisible`) VALUES
161+
('e42c77c6-06a4-4502-816c-d112c7142e6d', 1, NULL);
162+
163+
DROP TABLE IF EXISTS `invisibles`;
164+
CREATE TABLE `invisibles` (
165+
`id` varchar(36) NOT NULL,
166+
PRIMARY KEY (`id`)
167+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
168+
169+
INSERT INTO `invisibles` (`id`) VALUES
170+
('e42c77c6-06a4-4502-816c-d112c7142e6d');
161171

162172
SET foreign_key_checks = 1;
163173

0 commit comments

Comments
 (0)