Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/Attribute/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function getValidations()
*/
public function generateSchema()
{
return [
$schema = [
'name' => $this->name,
'type' => $this->getType(),
'mutability' => $this->mutability,
Expand All @@ -75,6 +75,12 @@ public function generateSchema()
'multiValued' => $this->getMultiValued(),
'caseExact' => false
];

if ($this->description !== null) {
$schema['description'] = $this->description;
}

return $schema;
}

public function setMultiValued($multiValued)
Expand Down Expand Up @@ -164,6 +170,13 @@ public function setParent($parent)
return $this;
}

public function setDescription(?string $description)
{
$this->description = $description;

return $this;
}

protected function isRequested($attributes)
{
return empty($attributes) || in_array($this->name, $attributes) || in_array($this->getFullKey(), $attributes) || ($this->parent != null && $this->parent->isRequested($attributes));
Expand Down
2 changes: 2 additions & 0 deletions src/Attribute/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

class Collection extends AbstractComplex
{
protected $type = 'complex';

protected $attribute;
protected $multiValued = true;

Expand Down
41 changes: 41 additions & 0 deletions src/Attribute/Constant.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace ArieTimmerman\Laravel\SCIMServer\Attribute;

use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use ArieTimmerman\Laravel\SCIMServer\Parser\Path;

Expand Down Expand Up @@ -42,4 +43,44 @@ public function patch($operation, $value, Model &$object, ?Path $path = null)
{
throw new SCIMException('Patch operation not supported for constant attributes');
}

public function applyComparison(Builder &$query, Path $path, $parentAttribute = null)
{
$operator = $path->node->operator ?? null;
$value = $path->node->compareValue ?? null;

if ($operator === null) {
throw new SCIMException('Invalid comparison on constant attribute');
}

$constantValue = $this->value;

$matches = $this->valuesAreEqual($constantValue, $value);

switch ($operator) {
case 'pr':
$query->whereRaw('1 = 1');
return;

case 'eq':
$query->whereRaw($matches ? '1 = 1' : '1 = 0');
return;

case 'ne':
$query->whereRaw($matches ? '1 = 0' : '1 = 1');
return;

default:
throw new SCIMException(sprintf('Operator "%s" not supported for constant attributes', $operator));
}
}

private function valuesAreEqual($constantValue, $compareValue): bool
{
if (is_string($constantValue) && is_string($compareValue)) {
return strcasecmp($constantValue, $compareValue) === 0;
}

return $constantValue === $compareValue;
}
}
2 changes: 1 addition & 1 deletion src/Filter/Ast/Disjunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace ArieTimmerman\Laravel\SCIMServer\Filter\Ast;

class Disjunction extends Filter
class Disjunction extends Term
{
/** @var Term[] */
private array $terms = [];
Expand Down
139 changes: 139 additions & 0 deletions src/Http/Controllers/ResourceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ArieTimmerman\Laravel\SCIMServer\Helper;
use ArieTimmerman\Laravel\SCIMServer\Exceptions\SCIMException;
use ArieTimmerman\Laravel\SCIMServer\ResourceType;
use ArieTimmerman\Laravel\SCIMServer\SCIMConfig;
use Illuminate\Database\Eloquent\Model;
use ArieTimmerman\Laravel\SCIMServer\Events\Delete;
use ArieTimmerman\Laravel\SCIMServer\Events\Get;
Expand Down Expand Up @@ -304,6 +305,24 @@ function (Builder $query) use ($filter, $resourceType) {
);
}

public function crossResourceSearch(Request $request, PolicyDecisionPoint $pdp, SCIMConfig $config)
{
$input = $request->json()->all();

if (!is_array($input) || !isset($input['schemas']) || !in_array('urn:ietf:params:scim:api:messages:2.0:SearchRequest', $input['schemas'])) {
throw (new SCIMException('Invalid schema. MUST be "urn:ietf:params:scim:api:messages:2.0:SearchRequest"'))->setCode(400);
}

$request->replace($input);

return $this->runCrossResourceQuery($request, $config);
}

public function crossResourceIndex(Request $request, PolicyDecisionPoint $pdp, SCIMConfig $config)
{
return $this->runCrossResourceQuery($request, $config);
}

public function search(Request $request, PolicyDecisionPoint $pdp, ResourceType $resourceType){

$input = $request->json()->all();
Expand All @@ -319,6 +338,126 @@ public function search(Request $request, PolicyDecisionPoint $pdp, ResourceType
return $this->index($request, $pdp, $resourceType);
}

protected function runCrossResourceQuery(Request $request, SCIMConfig $config): ListResponse
{
if ($request->has('cursor')) {
throw (new SCIMException('Cursor pagination is not supported for cross-resource search'))->setCode(400)->setScimType('invalidCursor');
}

[$attributes, $excludedAttributes] = $this->resolveAttributeParameters($request);

$count = min(max(0, intVal($request->input('count', config('scim.pagination.defaultPageSize')))), config('scim.pagination.maxPageSize'));
$startIndex = max(1, intVal($request->input('startIndex', 1)));

$resourceTypes = $this->resolveResourceTypesForSearch($config);

if (empty($resourceTypes)) {
return new ListResponse(collect(), $startIndex, 0, $attributes, $excludedAttributes);
}

if ($request->filled('sortBy') && count($resourceTypes) > 1) {
throw (new SCIMException('sortBy is only supported when a single resourceType is requested'))->setCode(400)->setScimType('invalidValue');
}

$sortAttribute = null;
$sortDirection = $request->input('sortOrder') === 'descending' ? 'desc' : 'asc';

if ($request->filled('sortBy')) {
$sortAttribute = $resourceTypes[0]->getMapping()->getSortAttributeByPath(ParserParser::parse($request->input('sortBy')));
}

$filter = $request->input('filter');

$resources = collect();
$offset = $startIndex - 1;
$remaining = $count;
$perTypeTotals = [];
$totalResults = 0;

$applyFilter = function (Builder $query, ResourceType $resourceType) use ($filter) {
if ($filter === null) {
return;
}

try {
Helper::scimFilterToLaravelQuery($resourceType, $query, ParserParser::parseFilter($filter));
} catch (ParserFilterException $e) {
throw (new SCIMException($e->getMessage()))->setCode(400)->setScimType('invalidFilter');
}
};

foreach ($resourceTypes as $resourceType) {
$countQuery = $resourceType->getQuery();
$applyFilter($countQuery, $resourceType);

$typeTotal = $countQuery->count();
$perTypeTotals[] = [$resourceType, $typeTotal];
$totalResults += $typeTotal;
}

foreach ($perTypeTotals as [$resourceType, $typeTotal]) {
if ($offset >= $typeTotal) {
$offset -= $typeTotal;
continue;
}

if ($remaining === 0) {
break;
}

$dataQuery = $resourceType->getQuery();
$applyFilter($dataQuery, $resourceType);

$dataQuery = $dataQuery->with($resourceType->getWithRelations());

if ($sortAttribute !== null) {
$dataQuery = $dataQuery->orderBy($sortAttribute, $sortDirection);
}

if ($offset > 0) {
$dataQuery = $dataQuery->skip($offset);
}

if ($remaining > 0) {
$dataQuery = $dataQuery->take($remaining);
}

$items = $dataQuery->get();

$offset = 0;
$remaining -= $items->count();

foreach ($items as $item) {
$resources->push(
Helper::objectToSCIMArray($item, $resourceType, $attributes, $excludedAttributes)
);
}

if ($remaining <= 0) {
break;
}
}

return new ListResponse($resources, $startIndex, $totalResults, $attributes, $excludedAttributes);
}

protected function resolveResourceTypesForSearch(SCIMConfig $config): array
{
$configurations = $config->getConfig();

if (empty($configurations)) {
return [];
}

$resourceTypes = [];

foreach ($configurations as $name => $configuration) {
$resourceTypes[] = new ResourceType($name, $configuration);
}

return $resourceTypes;
}

protected function respondWithResource(Request $request, ResourceType $resourceType, Model $resourceObject, int $status = 200)
{
[$attributes, $excludedAttributes] = $this->resolveAttributeParameters($request);
Expand Down
5 changes: 3 additions & 2 deletions src/RouteProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ public static function publicRoutes(array $options = [])

private static function allRoutes(array $options = [])
{
// TODO: Implement POST /.search for cross-resource queries per RFC 7644 §3.4.3.
Route::post('.search', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'notImplemented']);
Route::get('/', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'crossResourceIndex']);
Route::get('', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'crossResourceIndex']);
Route::post('.search', [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\ResourceController::class, 'crossResourceSearch']);

Route::post("/Bulk", [\ArieTimmerman\Laravel\SCIMServer\Http\Controllers\BulkController::class, 'processBulkRequest']);

Expand Down
4 changes: 3 additions & 1 deletion src/SCIMConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ public function remove($value, &$object, $path = null)
$fail('The name has already been taken.');
}
}),
(new MutableCollection('members'))->withSubAttributes(
(new MutableCollection('members'))
->setDescription('A list of members of the Group.')
->withSubAttributes(
eloquent('value', 'id')->ensure('required'),
(new class ('$ref') extends Eloquent {
protected function doRead(&$object, $attributes = [])
Expand Down
Loading