Skip to content

Commit 397ee69

Browse files
Support for Bulk Operations (#55)
1 parent f9f48c8 commit 397ee69

File tree

3 files changed

+119
-1
lines changed

3 files changed

+119
-1
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace ArieTimmerman\Laravel\SCIMServer\Http\Controllers;
4+
5+
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Support\Facades\Validator;
8+
9+
class BulkController extends Controller
10+
{
11+
12+
const MAX_PAYLOAD_SIZE = 1048576;
13+
const MAX_OPERATIONS = 10;
14+
15+
/**
16+
* Process SCIM BULK requests.
17+
*
18+
* @param \Illuminate\Http\Request $request
19+
* @return \Illuminate\Http\Response
20+
*/
21+
public function processBulkRequest(Request $request)
22+
{
23+
24+
// get the content size in bytes from raw content (not entirely accurate, but good enough for now)
25+
$contentSize = mb_strlen($request->getContent(), '8bit');
26+
27+
if($contentSize > static::MAX_PAYLOAD_SIZE){
28+
throw (new SCIMException('Payload too large!'))->setCode(413)->setScimType('tooLarge');
29+
}
30+
31+
$validator = Validator::make($request->input(), [
32+
'schemas' => 'required|array',
33+
'schemas.*' => 'required|string|in:urn:ietf:params:scim:api:messages:2.0:BulkRequest',
34+
// TODO: implement failOnErrors
35+
'failOnErrors' => 'nullable|int',
36+
'Operations' => 'required|array',
37+
'Operations.*.method' => 'required|string|in:POST,PUT,PATCH,DELETE',
38+
'Operations.*.path' => 'required|string|in:/Users,/Groups',
39+
'Operations.*.bulkId' => 'nullable|string',
40+
'Operations.*.data' => 'nullable|array',
41+
]);
42+
43+
if ($validator->fails()) {
44+
$e = $validator->errors();
45+
46+
throw (new SCIMException('Invalid data!'))->setCode(400)->setScimType('invalidSyntax')->setErrors($e);
47+
}
48+
49+
$operations = $request->input('Operations');
50+
51+
if(count($operations) > static::MAX_OPERATIONS){
52+
throw (new SCIMException('Too many operations!'))->setCode(413)->setScimType('tooLarge');
53+
}
54+
55+
$bulkIdMapping = [];
56+
$responses = [];
57+
58+
// Remove everything till the last occurence of Bulk, e.g. /scim/v2/Bulk should become /scim/v2/
59+
$prefix = substr($request->path(), 0, strrpos($request->path(), '/Bulk'));
60+
61+
foreach ($operations as $operation) {
62+
63+
$method = $operation['method'];
64+
$bulkId = $operation['bulkId'] ?? null;
65+
66+
// Call internal Laravel route based on method, path and data
67+
$encoded = json_encode($operation['data'] ?? []);
68+
$encoded = str_replace(array_keys($bulkIdMapping), array_values($bulkIdMapping), $encoded);
69+
70+
$request = Request::create(
71+
$prefix . $operation['path'],
72+
$operation['method'],
73+
server: [
74+
'HTTP_Authorization' => $request->header('Authorization'),
75+
'CONTENT_TYPE' => 'application/scim+json',
76+
],
77+
content: $encoded
78+
);
79+
80+
// run request and get response
81+
/** @var \Illuminate\Http\Response */
82+
$response = app()->handle($request);
83+
// Get the JSON content of the response
84+
$jsonContent = $response->getContent();
85+
// Decode the JSON content
86+
$responseData = json_decode($jsonContent, false);
87+
88+
// Store the id attribute
89+
$id = $responseData?->id ?? null;
90+
91+
// Store the id attribute in the bulkIdMapping array
92+
if ($bulkId !== null && $id !== null) {
93+
$bulkIdMapping['bulkId:' . $bulkId] = $id;
94+
}
95+
96+
$responses[] = array_filter([
97+
"location" => $responseData?->meta?->location ?? null,
98+
"method" => $method,
99+
"bulkId" => $bulkId,
100+
"version" => $responseData?->meta?->version ?? null,
101+
"status" => $response->getStatusCode(),
102+
"response" => $response->getStatusCode() >= 400 ? $responseData : null,
103+
]);
104+
}
105+
106+
// Return a response indicating the successful processing of the SCIM BULK request
107+
return response()->json(
108+
[
109+
'schemas' => ['urn:ietf:params:scim:api:messages:2.0:BulkResponse'],
110+
'Operations' =>
111+
$responses])->setStatusCode(200)
112+
->withHeaders(['Content-Type' => 'application/scim+json']);
113+
}
114+
}

src/Http/Controllers/ServiceProviderController.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ public function index()
1414
"supported" => true,
1515
],
1616
"bulk" => [
17-
"supported" => false,
17+
"supported" => true,
18+
"maxPayloadSize" => BulkController::MAX_PAYLOAD_SIZE,
19+
"maxOperations" => BulkController::MAX_OPERATIONS
1820
],
1921
"filter" => [
2022
"supported" => true,

src/RouteProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ private static function allRoutes(array $options = [])
7676
{
7777
Route::post('.search', '\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController@notImplemented');
7878

79+
Route::post("/Bulk", '\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\BulkController@processBulkRequest');
80+
7981
// TODO: Use the attributes parameters ?attributes=userName, excludedAttributes=asdg,asdg (respect "returned" settings "always")
8082
Route::get('/{resourceType}/{resourceObject}', '\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController@show')->name('scim.resource');
8183
Route::get("/{resourceType}", '\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController@index')->name('scim.resources');

0 commit comments

Comments
 (0)