Skip to content

Commit fb18a55

Browse files
committed
Make Auth work with operations
1 parent aa36d06 commit fb18a55

File tree

11 files changed

+480
-256
lines changed

11 files changed

+480
-256
lines changed

README.md

+30-9
Original file line numberDiff line numberDiff line change
@@ -594,35 +594,56 @@ This example sends the signed claims:
594594

595595
NB: The JWT implementation only supports the hash based algorithms HS256, HS384 and HS512.
596596

597+
## Authorizing operations
598+
599+
The Authorization model acts on "operations". The most important ones are listed here:
600+
601+
GET /records/{table} - list - lists records
602+
POST /records/{table} - create - creates records
603+
GET /records/{table}/{id} - read - reads a record by primary key
604+
PUT /records/{table}/{id} - update - updates columns of a record by primary key
605+
DELETE /records/{table}/{id} - delete - deletes a record by primary key
606+
PATCH /records/{table}/{id} - increment - increments columns of a record by primary key
607+
608+
The "`/openapi`" endpoint has a special "document" operation to allow you to hide tables and columns from the documentation. The OpenAPI specification will show what is allowed in your session.
609+
610+
For endpoints that start with "`/columns`" there are the operations "reflect" and "remodel".
611+
These operations can display or change the definition of the database, table or column.
612+
This functionality is disabled by default and for good reason (be careful!).
613+
Add the "columns" controller in the configuration to enable this functionality.
614+
597615
### Authorizing tables, columns and records
598616

599-
By default all tables are reflected. If you want to restrict access to some tables you may add the 'authorization' middleware
617+
By default all tables and columns are accessible. If you want to restrict access to some tables you may add the 'authorization' middleware
600618
and define a 'authorization.tableHandler' function that returns 'false' for these tables.
601619

602-
'authorization.tableHandler' => function ($method, $path, $databaseName, $tableName) {
620+
'authorization.tableHandler' => function ($operation, $tableName) {
603621
return $tableName != 'license_keys';
604622
},
605623

606-
The above example will restrict access to the table 'license_keys' in all API calls.
624+
The above example will restrict access to the table 'license_keys' for all operations.
607625

608-
'authorization.columnHandler' => function ($method, $path, $databaseName, $tableName, $columnName) {
626+
'authorization.columnHandler' => function ($operation, $tableName, $columnName) {
609627
return !($tableName == 'users' && $columnName == 'password');
610628
},
611629

612-
The above example will restrict access to the 'password' field from the 'users' table in all API calls.
630+
The above example will restrict access to the 'password' field of the 'users' table for all operations.
613631

614-
'authorization.recordHandler' => function ($method, $path, $databaseName, $tableName, $columnName) {
632+
'authorization.recordHandler' => function ($operation, $tableName, $columnName) {
615633
return ($tableName == 'users') ? 'filter=username,neq,admin' : '';
616634
},
617635

618-
The above example will disallow viewing the user records where the username is 'admin'. This construct adds a filter to every executed query.
636+
The above example will disallow access to user records where the username is 'admin'.
637+
This construct adds a filter to every executed query.
638+
639+
NB: You need to handle the creation of invalid records with a validation (or sanitation) handler.
619640

620641
### Sanitizing input
621642

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

625-
'sanitation.handler' => function ($method, $tableName, $column, $value) {
646+
'sanitation.handler' => function ($operation, $tableName, $column, $value) {
626647
return is_string($value) ? strip_tags($value) : $value;
627648
},
628649

@@ -633,7 +654,7 @@ The above example will strip all HTML tags from strings in the input.
633654
By default all input is accepted. If you want to validate the input, you may add the 'validation' middleware and define a
634655
'validation.handler' function that returns a boolean indicating whether or not the value is valid.
635656

636-
'validation.handler' => function ($method, $tableName, $column, $value, $context) {
657+
'validation.handler' => function ($operation, $tableName, $column, $value, $context) {
637658
return ($column['name'] == 'post_id' && !is_numeric($value)) ? 'must be numeric' : true;
638659
},
639660

api.php

+219-121
Large diffs are not rendered by default.

src/Tqdev/PhpCrudApi/Config.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Config
1111
'password' => null,
1212
'database' => null,
1313
'middlewares' => 'cors',
14-
'controllers' => 'records,columns,cache,openapi',
14+
'controllers' => 'records,openapi',
1515
'cacheType' => 'TempFile',
1616
'cachePath' => '',
1717
'cacheTime' => 10,

src/Tqdev/PhpCrudApi/Database/GenericDB.php

+10-10
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ public function definition(): GenericDefinition
9898
return $this->definition;
9999
}
100100

101-
private function addAuthorizationCondition(Condition $condition2): Condition
101+
private function addAuthorizationCondition(String $tableName, Condition $condition2): Condition
102102
{
103-
$condition1 = VariableStore::get('authorization.condition');
103+
$condition1 = VariableStore::get("authorization.conditions.$tableName");
104104
return $condition1 ? AndCondition::fromArray([$condition1, $condition2]) : $condition2;
105105
}
106106

@@ -131,7 +131,7 @@ public function selectSingle(ReflectedTable $table, array $columnNames, String $
131131
$selectColumns = $this->columns->getSelect($table, $columnNames);
132132
$tableName = $table->getName();
133133
$condition = new ColumnCondition($table->getPk(), 'eq', $id);
134-
$condition = $this->addAuthorizationCondition($condition);
134+
$condition = $this->addAuthorizationCondition($tableName, $condition);
135135
$parameters = array();
136136
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
137137
$sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
@@ -153,7 +153,7 @@ public function selectMultiple(ReflectedTable $table, array $columnNames, array
153153
$selectColumns = $this->columns->getSelect($table, $columnNames);
154154
$tableName = $table->getName();
155155
$condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
156-
$condition = $this->addAuthorizationCondition($condition);
156+
$condition = $this->addAuthorizationCondition($tableName, $condition);
157157
$parameters = array();
158158
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
159159
$sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
@@ -166,7 +166,7 @@ public function selectMultiple(ReflectedTable $table, array $columnNames, array
166166
public function selectCount(ReflectedTable $table, Condition $condition): int
167167
{
168168
$tableName = $table->getName();
169-
$condition = $this->addAuthorizationCondition($condition);
169+
$condition = $this->addAuthorizationCondition($tableName, $condition);
170170
$parameters = array();
171171
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
172172
$sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
@@ -178,7 +178,7 @@ public function selectAllUnordered(ReflectedTable $table, array $columnNames, Co
178178
{
179179
$selectColumns = $this->columns->getSelect($table, $columnNames);
180180
$tableName = $table->getName();
181-
$condition = $this->addAuthorizationCondition($condition);
181+
$condition = $this->addAuthorizationCondition($tableName, $condition);
182182
$parameters = array();
183183
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
184184
$sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause;
@@ -195,7 +195,7 @@ public function selectAll(ReflectedTable $table, array $columnNames, Condition $
195195
}
196196
$selectColumns = $this->columns->getSelect($table, $columnNames);
197197
$tableName = $table->getName();
198-
$condition = $this->addAuthorizationCondition($condition);
198+
$condition = $this->addAuthorizationCondition($tableName, $condition);
199199
$parameters = array();
200200
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
201201
$orderBy = $this->columns->getOrderBy($table, $columnOrdering);
@@ -216,7 +216,7 @@ public function updateSingle(ReflectedTable $table, array $columnValues, String
216216
$updateColumns = $this->columns->getUpdate($table, $columnValues);
217217
$tableName = $table->getName();
218218
$condition = new ColumnCondition($table->getPk(), 'eq', $id);
219-
$condition = $this->addAuthorizationCondition($condition);
219+
$condition = $this->addAuthorizationCondition($tableName, $condition);
220220
$parameters = array_values($columnValues);
221221
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
222222
$sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
@@ -228,7 +228,7 @@ public function deleteSingle(ReflectedTable $table, String $id)
228228
{
229229
$tableName = $table->getName();
230230
$condition = new ColumnCondition($table->getPk(), 'eq', $id);
231-
$condition = $this->addAuthorizationCondition($condition);
231+
$condition = $this->addAuthorizationCondition($tableName, $condition);
232232
$parameters = array();
233233
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
234234
$sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
@@ -245,7 +245,7 @@ public function incrementSingle(ReflectedTable $table, array $columnValues, Stri
245245
$updateColumns = $this->columns->getIncrement($table, $columnValues);
246246
$tableName = $table->getName();
247247
$condition = new ColumnCondition($table->getPk(), 'eq', $id);
248-
$condition = $this->addAuthorizationCondition($condition);
248+
$condition = $this->addAuthorizationCondition($tableName, $condition);
249249
$parameters = array_values($columnValues);
250250
$whereClause = $this->conditions->getWhereClause($condition, $parameters);
251251
$sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;

src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php

+20-48
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tqdev\PhpCrudApi\Middleware\Communication\VariableStore;
88
use Tqdev\PhpCrudApi\Middleware\Router\Router;
99
use Tqdev\PhpCrudApi\Record\FilterInfo;
10+
use Tqdev\PhpCrudApi\Record\RequestUtils;
1011
use Tqdev\PhpCrudApi\Request;
1112
use Tqdev\PhpCrudApi\Response;
1213

@@ -18,99 +19,70 @@ public function __construct(Router $router, Responder $responder, array $propert
1819
{
1920
parent::__construct($router, $responder, $properties);
2021
$this->reflection = $reflection;
22+
$this->utils = new RequestUtils($reflection);
2123
}
2224

23-
private function handleColumns(String $method, String $path, String $databaseName, String $tableName) /*: void*/
25+
private function handleColumns(String $operation, String $tableName) /*: void*/
2426
{
2527
$columnHandler = $this->getProperty('columnHandler', '');
2628
if ($columnHandler) {
2729
$table = $this->reflection->getTable($tableName);
2830
foreach ($table->columnNames() as $columnName) {
29-
$allowed = call_user_func($columnHandler, $method, $path, $databaseName, $tableName, $columnName);
31+
$allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
3032
if (!$allowed) {
3133
$this->reflection->removeColumn($tableName, $columnName);
3234
}
3335
}
3436
}
3537
}
3638

37-
private function handleTable(String $method, String $path, String $databaseName, String $tableName) /*: void*/
39+
private function handleTable(String $operation, String $tableName) /*: void*/
3840
{
3941
if (!$this->reflection->hasTable($tableName)) {
4042
return;
4143
}
4244
$tableHandler = $this->getProperty('tableHandler', '');
4345
if ($tableHandler) {
44-
$allowed = call_user_func($tableHandler, $method, $path, $databaseName, $tableName);
46+
$allowed = call_user_func($tableHandler, $operation, $tableName);
4547
if (!$allowed) {
4648
$this->reflection->removeTable($tableName);
4749
} else {
48-
$this->handleColumns($method, $path, $databaseName, $tableName);
50+
$this->handleColumns($operation, $tableName);
4951
}
5052
}
5153
}
5254

53-
private function handleJoinTables(String $method, String $path, String $databaseName, array $joinParameters) /*: void*/
54-
{
55-
$uniqueTableNames = array();
56-
foreach ($joinParameters as $joinParameter) {
57-
$tableNames = explode(',', trim($joinParameter));
58-
foreach ($tableNames as $tableName) {
59-
$uniqueTableNames[$tableName] = true;
60-
}
61-
}
62-
foreach (array_keys($uniqueTableNames) as $tableName) {
63-
$this->handleTable($method, $path, $databaseName, trim($tableName));
64-
}
65-
}
66-
67-
private function handleAllTables(String $method, String $path, String $databaseName) /*: void*/
68-
{
69-
$tableNames = $this->reflection->getTableNames();
70-
foreach ($tableNames as $tableName) {
71-
$this->handleTable($method, $path, $databaseName, $tableName);
72-
}
73-
}
74-
75-
private function handleRecords(String $method, String $path, String $databaseName, String $tableName) /*: void*/
55+
private function handleRecords(String $operation, String $tableName) /*: void*/
7656
{
7757
if (!$this->reflection->hasTable($tableName)) {
7858
return;
7959
}
8060
$recordHandler = $this->getProperty('recordHandler', '');
8161
if ($recordHandler) {
82-
$query = call_user_func($recordHandler, $method, $path, $databaseName, $tableName);
62+
$query = call_user_func($recordHandler, $operation, $tableName);
8363
$filters = new FilterInfo();
8464
$table = $this->reflection->getTable($tableName);
8565
$query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
8666
parse_str($query, $params);
8767
$condition = $filters->getCombinedConditions($table, $params);
88-
VariableStore::set('authorization.condition', $condition);
68+
VariableStore::set("authorization.conditions.$tableName", $condition);
8969
}
9070
}
9171

9272
public function handle(Request $request): Response
9373
{
94-
$method = $request->getMethod();
9574
$path = $request->getPathSegment(1);
96-
$databaseName = $this->reflection->getDatabaseName();
97-
if ($path == 'records') {
98-
$tableName = $request->getPathSegment(2);
99-
$this->handleTable($method, $path, $databaseName, $tableName);
100-
$params = $request->getParams();
101-
if (isset($params['join'])) {
102-
$this->handleJoinTables($method, $path, $databaseName, $params['join']);
103-
}
104-
$this->handleRecords($method, $path, $databaseName, $tableName);
105-
} elseif ($path == 'columns') {
106-
$tableName = $request->getPathSegment(2);
107-
if ($tableName) {
108-
$this->handleTable($method, $path, $databaseName, $tableName);
109-
} else {
110-
$this->handleAllTables($method, $path, $databaseName);
75+
$operation = $this->utils->getOperation($request);
76+
$tableNames = $this->utils->getTableNames($request);
77+
foreach ($tableNames as $tableName) {
78+
$this->handleTable($operation, $tableName);
79+
if ($path == 'records') {
80+
$this->handleRecords($operation, $tableName);
11181
}
112-
} elseif ($path == 'openapi') {
113-
$this->handleAllTables($method, $path, $databaseName);
82+
}
83+
if ($path == 'openapi') {
84+
VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
85+
VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
11486
}
11587
return $this->next->handle($request);
11688
}

src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php

+21-20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Tqdev\PhpCrudApi\Controller\Responder;
77
use Tqdev\PhpCrudApi\Middleware\Base\Middleware;
88
use Tqdev\PhpCrudApi\Middleware\Router\Router;
9+
use Tqdev\PhpCrudApi\Record\RequestUtils;
910
use Tqdev\PhpCrudApi\Request;
1011
use Tqdev\PhpCrudApi\Response;
1112

@@ -17,43 +18,43 @@ public function __construct(Router $router, Responder $responder, array $propert
1718
{
1819
parent::__construct($router, $responder, $properties);
1920
$this->reflection = $reflection;
21+
$this->utils = new RequestUtils($reflection);
2022
}
2123

22-
private function callHandler($handler, $record, String $method, ReflectedTable $table) /*: object */
24+
private function callHandler($handler, $record, String $operation, ReflectedTable $table) /*: object */
2325
{
2426
$context = (array) $record;
2527
$tableName = $table->getName();
2628
foreach ($context as $columnName => &$value) {
2729
if ($table->exists($columnName)) {
2830
$column = $table->get($columnName);
29-
$value = call_user_func($handler, $method, $tableName, $column->serialize(), $value);
31+
$value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
3032
}
3133
}
3234
return (object) $context;
3335
}
3436

3537
public function handle(Request $request): Response
3638
{
37-
$path = $request->getPathSegment(1);
38-
$tableName = $request->getPathSegment(2);
39-
$record = $request->getBody();
40-
if ($path == 'records' && $this->reflection->hasTable($tableName) && $record !== null) {
41-
$table = $this->reflection->getTable($tableName);
42-
$method = $request->getMethod();
43-
$handler = $this->getProperty('handler', '');
44-
if ($handler !== '') {
45-
if (is_array($record)) {
46-
foreach ($record as &$r) {
47-
$r = $this->callHandler($handler, $r, $method, $table);
39+
$operation = $this->utils->getOperation($request);
40+
if (in_array($operation, ['create', 'update', 'increment'])) {
41+
$tableName = $request->getPathSegment(2);
42+
if ($this->reflection->hasTable($tableName)) {
43+
$record = $request->getBody();
44+
if ($record !== null) {
45+
$handler = $this->getProperty('handler', '');
46+
if ($handler !== '') {
47+
$table = $this->reflection->getTable($tableName);
48+
if (is_array($record)) {
49+
foreach ($record as &$r) {
50+
$r = $this->callHandler($handler, $r, $operation, $table);
51+
}
52+
} else {
53+
$record = $this->callHandler($handler, $record, $operation, $table);
54+
}
55+
$request->setBody($record);
4856
}
49-
} else {
50-
$record = $this->callHandler($handler, $record, $method, $table);
5157
}
52-
$path = $request->getPath();
53-
$query = urldecode(http_build_query($request->getParams()));
54-
$headers = $request->getHeaders();
55-
$body = json_encode($record);
56-
$request = new Request($method, $path, $query, $headers, $body);
5758
}
5859
}
5960
return $this->next->handle($request);

0 commit comments

Comments
 (0)