Skip to content

Commit 6b19676

Browse files
Merge pull request #136 from limosa-io/optional-cursor-pagination
Optional cursor pagination configuration #111
2 parents f72d84d + de73ad3 commit 6b19676

File tree

6 files changed

+165
-98
lines changed

6 files changed

+165
-98
lines changed

README.md

Lines changed: 111 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
![](https://github.com/arietimmerman/laravel-scim-server/workflows/CI/badge.svg)
32
[![Latest Stable Version](https://poser.pugx.org/arietimmerman/laravel-scim-server/v/stable)](https://packagist.org/packages/arietimmerman/laravel-scim-server)
43
[![Total Downloads](https://poser.pugx.org/arietimmerman/laravel-scim-server/downloads)](https://packagist.org/packages/arietimmerman/laravel-scim-server)
@@ -7,71 +6,91 @@
76

87
# SCIM 2.0 Server implementation for Laravel
98

10-
Add SCIM 2.0 Server capabilities to your Laravel application with ease. This package requires minimal configuration to get started with basic functionalities.
9+
Add SCIM 2.0 Server capabilities to your Laravel application with ease. This package requires minimal configuration to get started with the core SCIM flows and is powering [The SCIM Playground](https://scim.dev), one of the most widely tested SCIM servers available.
10+
11+
## Why Laravel SCIM Server?
12+
- Battle-tested with real-world providers through the SCIM Playground
13+
- Familiar Laravel tooling and middleware integration
14+
- Fully extensible configuration for resources, attributes, and filtering
15+
- Ships with dockerized demo and an expressive test suite
1116

12-
This implementation is used by [The SCIM Playground](https://scim.dev) and is therefore one of the most widely tested SCIM servers available.
13-
## Docker
17+
## Table of contents
18+
- [Quick start](#quick-start)
19+
- [Installation](#installation)
20+
- [SCIM routes](#scim-routes)
21+
- [Configuration](#configuration)
22+
- [Security & app integration](#security--app-integration)
23+
- [Test server](#test-server)
24+
- [Contributing & support](#contributing--support)
1425

15-
To quickly spin up a SCIM test server using Docker, run:
26+
## Quick start
27+
Spin up a SCIM test server in seconds:
1628

17-
~~~
29+
```bash
1830
docker run -d -p 8000:8000 --name laravel-scim-server ghcr.io/limosa-io/laravel-scim-server:latest
19-
~~~
31+
```
2032

21-
This command will start the server and bind it to port 8000 on your local machine. You can then access the SCIM endpoints at `http://localhost:8000/scim/v2/Users`. Other SCIM endpoints like `/Groups`, `/Schemas`, and `/ResourceTypes` will also be available.
33+
Visit `http://localhost:8000/scim/v2/Users` (or `/Groups`, `/Schemas`, `/ResourceTypes`, etc.) to exercise the API.
2234

2335
## Installation
36+
Add the package to your Laravel app:
2437

25-
Simply run:
26-
27-
~~~
38+
```bash
2839
composer require arietimmerman/laravel-scim-server
29-
~~~
40+
```
3041

31-
And optionally
42+
Optionally publish the config for fine-grained control:
3243

33-
~~~
44+
```bash
3445
php artisan vendor:publish --tag=laravel-scim
35-
~~~
36-
37-
# Routes
38-
39-
| Method | Path |
40-
|--------|------|
41-
| GET\|HEAD | / |
42-
| GET\|HEAD | scim/v1 |
43-
| GET\|HEAD | scim/v1/{fallbackPlaceholder} |
44-
| POST | scim/v2/.search |
45-
| POST | scim/v2/Bulk |
46-
| GET\|HEAD | scim/v2/ResourceTypes |
47-
| GET\|HEAD | scim/v2/ResourceTypes/{id} |
48-
| GET\|HEAD | scim/v2/Schemas |
49-
| GET\|HEAD | scim/v2/Schemas/{id} |
50-
| GET\|HEAD | scim/v2/ServiceProviderConfig |
51-
| GET\|HEAD | scim/v2/{fallbackPlaceholder} |
52-
| GET\|HEAD | scim/v2/{resourceType} |
53-
| POST | scim/v2/{resourceType} |
54-
| POST | scim/v2/{resourceType}/.search |
55-
| GET\|HEAD | scim/v2/{resourceType}/{resourceObject} |
56-
| PUT | scim/v2/{resourceType}/{resourceObject} |
57-
| PATCH | scim/v2/{resourceType}/{resourceObject} |
58-
| DELETE | scim/v2/{resourceType}/{resourceObject} |
59-
60-
61-
# Configuration
62-
63-
The configuration is retrieved from `SCIMConfig::class`.
64-
65-
Extend this class and register your extension in `app/Providers/AppServiceProvider.php` like this.
66-
67-
~~~.php
68-
$this->app->singleton('ArieTimmerman\Laravel\SCIMServer\SCIMConfig', YourCustomSCIMConfig::class);
69-
~~~
70-
71-
## An example override
72-
73-
Here's one way to override the default configuration without copying too much of the SCIMConfig file into your app.
74-
~~~.php
46+
```
47+
48+
## SCIM routes
49+
50+
| Method | Path | Description |
51+
|--------|------|-------------|
52+
| GET | /scim/v1 | SCIM 1.x compatibility message (returns error with upgrade guidance) |
53+
| GET | /scim/v2 | Cross-resource index (alias of `/scim/v2/`) |
54+
| GET | /scim/v2/ | Cross-resource index |
55+
| POST | /scim/v2/.search | Cross-resource search across all types |
56+
| POST | /scim/v2/Bulk | SCIM bulk operations |
57+
| GET | /scim/v2/ResourceTypes | List available resource types |
58+
| GET | /scim/v2/ResourceTypes/{id} | Retrieve a specific resource type |
59+
| GET | /scim/v2/Schemas | List SCIM schemas |
60+
| GET | /scim/v2/Schemas/{id} | Retrieve a specific schema |
61+
| GET | /scim/v2/ServiceProviderConfig | Discover server capabilities |
62+
| GET | /scim/v2/{resourceType} | List resources of a given type |
63+
| POST | /scim/v2/{resourceType} | Create a new resource |
64+
| POST | /scim/v2/{resourceType}/.search | Filter resources of a given type |
65+
| GET | /scim/v2/{resourceType}/{resourceObject} | Retrieve a single resource |
66+
| PUT | /scim/v2/{resourceType}/{resourceObject} | Replace a resource |
67+
| PATCH | /scim/v2/{resourceType}/{resourceObject} | Update a resource |
68+
| DELETE | /scim/v2/{resourceType}/{resourceObject} | Delete a resource |
69+
70+
Optional "Me" routes can be enabled separately:
71+
72+
| Method | Path | Description |
73+
|--------|------|-------------|
74+
| GET | /scim/v2/Me | Retrieve the SCIM resource for the authenticated subject |
75+
| PUT | /scim/v2/Me | Replace the SCIM resource for the authenticated subject |
76+
| POST | /scim/v2/Me | Create the authenticated subject (requires `RouteProvider::meRoutePost()`) |
77+
78+
## Configuration
79+
80+
The package resolves configuration via `SCIMConfig::class`. Extend it to tweak resource definitions, attribute mappings, filters, or pagination defaults.
81+
82+
Register your custom config in `app/Providers/AppServiceProvider.php`:
83+
84+
```php
85+
$this->app->singleton(
86+
\ArieTimmerman\Laravel\SCIMServer\SCIMConfig::class,
87+
YourCustomSCIMConfig::class
88+
);
89+
```
90+
91+
Minimal override example:
92+
93+
```php
7594
<?php
7695

7796
class YourCustomSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
@@ -80,59 +99,61 @@ class YourCustomSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
8099
{
81100
$config = parent::getUserConfig();
82101

83-
// Modify the $config variable however you need...
102+
// Customize $config as needed.
84103

85104
return $config;
86105
}
87106
}
88-
~~~
89-
90-
91-
# Security & App Integration
92-
93-
By default, this package does no security checks on its own. This can be dangerous, in that a functioning SCIM Server can view, add, update, delete, or list users.
94-
You are welcome to implement your own security checks at the middleware layer,
95-
or somehow/somewhere else that makes sense for your application. But make sure to do **something**.
96-
97-
If you want to integrate into _already existing_ middleware, you'll want to take the following steps -
107+
```
98108

99-
## Turn off automatic publishing of routes
109+
### Pagination settings
110+
Cursor-based pagination is enabled by default via the [SCIM cursor pagination draft](https://datatracker.ietf.org/doc/draft-ietf-scim-cursor-pagination/). Publish the config file and update `config/scim.php` to adjust defaults:
100111

101-
Modify `config/scim.php` like this:
102112
```php
103-
<?php
104-
return [
105-
"publish_routes" => false
106-
];
113+
'pagination' => [
114+
'defaultPageSize' => 10,
115+
'maxPageSize' => 100,
116+
'cursorPaginationEnabled' => false,
117+
]
107118
```
108119

109-
## Next, explicitly publish your routes with your choice of middleware
110-
111-
In either your RouteServiceProvider, or in a particular route file, add the following:
120+
## Security & app integration
121+
SCIM grants the ability to view, add, update, and delete users or groups. Make sure you secure the routes before shipping to production.
112122

113-
```php
114-
use ArieTimmerman\Laravel\SCIMServer\RouteProvider as SCIMServerRouteProvider;
123+
1. Disable automatic route publishing if you plan to wrap routes in your own middleware:
115124

116-
SCIMServerRouteProvider::publicRoutes(); // Make sure to add public routes *first*
125+
```php
126+
// config/scim.php
127+
return [
128+
'publish_routes' => false,
129+
];
130+
```
117131

132+
2. Re-register the routes with your preferred middleware stack:
118133

119-
Route::middleware('auth:api')->group(function () { // or any other middleware you choose
120-
SCIMServerRouteProvider::routes(
121-
[
122-
'public_routes' => false // but do not hide public routes (metadata) behind authentication
123-
]
124-
);
134+
```php
135+
use ArieTimmerman\Laravel\SCIMServer\RouteProvider as SCIMServerRouteProvider;
125136

126-
SCIMServerRouteProvider::meRoutes();
127-
});
137+
SCIMServerRouteProvider::publicRoutes();
128138

139+
Route::middleware('auth:api')->group(function () {
140+
SCIMServerRouteProvider::routes([
141+
'public_routes' => false,
142+
]);
129143

130-
```
144+
SCIMServerRouteProvider::meRoutes();
145+
});
146+
```
131147

132-
# Test server
148+
## Test server
149+
Bring up the full demo stack with Docker Compose:
133150

134-
~~~
151+
```bash
135152
docker-compose up
136-
~~~
153+
```
154+
155+
Browse to `http://localhost:18123/scim/v2/Users` to explore the API and run the test suite.
137156

138-
Now visit `http://localhost:18123/scim/v2/Users`.
157+
## Contributing & support
158+
- Issues and pull requests are welcome on [GitHub](https://github.com/arietimmerman/laravel-scim-server)
159+
- Found this package helpful? [Give it a star on GitHub](https://github.com/arietimmerman/laravel-scim-server) so others can discover it faster

config/scim.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
'pagination' => [
99
'defaultPageSize' => 10,
1010
'maxPageSize' => 100,
11+
'cursorPaginationEnabled' => true,
1112
]
1213
];

src/Http/Controllers/ResourceController.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ public function wrongVersion(Request $request)
213213
public function index(Request $request, PolicyDecisionPoint $pdp, ResourceType $resourceType)
214214
{
215215
$query = $resourceType->getQuery();
216+
$cursorPaginationEnabled = (bool) config('scim.pagination.cursorPaginationEnabled', true);
216217

217218
// if both cursor and startIndex are present, throw an exception
218219
if ($request->has('cursor') && $request->has('startIndex')) {
@@ -256,6 +257,9 @@ function (Builder $query) use ($filter, $resourceType) {
256257

257258
$resources = null;
258259
if ($request->has('cursor')) {
260+
if (!$cursorPaginationEnabled) {
261+
throw (new SCIMException('Cursor pagination is disabled.'))->setCode(400)->setScimType('invalidCursor');
262+
}
259263
if($sortBy == null){
260264
$resourceObjects = $resourceObjects->orderBy('id');
261265
}

src/Http/Controllers/ServiceProviderController.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ class ServiceProviderController extends Controller
88
{
99
public function index()
1010
{
11+
$cursorPaginationEnabled = (bool) config('scim.pagination.cursorPaginationEnabled', true);
12+
13+
$pagination = [
14+
"cursor" => $cursorPaginationEnabled,
15+
"index" => true,
16+
"defaultPaginationMethod" => "index",
17+
"defaultPageSize" => config('scim.pagination.defaultPageSize'),
18+
"maxPageSize" => config('scim.pagination.maxPageSize'),
19+
];
20+
21+
if ($cursorPaginationEnabled) {
22+
$pagination["cursorTimeout"] = 3600;
23+
}
24+
1125
return [
1226
"schemas" => ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
1327
"patch" => [
@@ -50,14 +64,7 @@ public function index()
5064
"type" => "httpbasic",
5165
],
5266
],
53-
"pagination" => [
54-
"cursor" => true,
55-
"index" => true,
56-
"defaultPaginationMethod" => "index",
57-
"defaultPageSize" => config('scim.pagination.defaultPageSize'),
58-
"maxPageSize" => config('scim.pagination.maxPageSize'),
59-
"cursorTimeout" => 3600
60-
],
67+
"pagination" => $pagination,
6168
"meta" => [
6269
"location" => route('scim.serviceproviderconfig'),
6370
"resourceType" => "ServiceProviderConfig",

tests/BasicTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,24 @@ public function testCursorPaginationFailureMaxCount()
167167

168168
}
169169

170+
public function testCursorPaginationDisabled()
171+
{
172+
config(['scim.pagination.cursorPaginationEnabled' => false]);
173+
174+
try {
175+
$response = $this->get('/scim/v2/Users?count=60&cursor');
176+
177+
$response->assertStatus(400);
178+
$response->assertJson([
179+
'schemas' => ['urn:ietf:params:scim:api:messages:2.0:Error'],
180+
'status' => '400',
181+
'scimType' => 'invalidCursor'
182+
]);
183+
} finally {
184+
config(['scim.pagination.cursorPaginationEnabled' => true]);
185+
}
186+
}
187+
170188
public function testPagination()
171189
{
172190
$response = $this->get('/scim/v2/Users?startIndex=21&count=20');

tests/ServiceProviderConfigTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,22 @@ public function testGet()
1616
{
1717
$response = $this->get('/scim/v2/ServiceProviderConfig');
1818
$response->assertStatus(200);
19+
$this->assertTrue($response->json('pagination.cursor'));
20+
$this->assertSame(3600, $response->json('pagination.cursorTimeout'));
21+
}
22+
23+
public function testCursorPaginationCanBeDisabled()
24+
{
25+
config(['scim.pagination.cursorPaginationEnabled' => false]);
26+
27+
try {
28+
$response = $this->get('/scim/v2/ServiceProviderConfig');
29+
$response->assertStatus(200);
30+
$this->assertFalse($response->json('pagination.cursor'));
31+
$this->assertArrayNotHasKey('cursorTimeout', $response->json('pagination') ?? []);
32+
} finally {
33+
config(['scim.pagination.cursorPaginationEnabled' => true]);
34+
}
1935
}
2036

2137
}

0 commit comments

Comments
 (0)