diff --git a/app/Models/Group.php b/app/Models/Group.php index 9f4f2e2e5677..b0991f4a9a3f 100755 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -59,6 +59,12 @@ public function users() return $this->belongsToMany(\App\Models\User::class, 'users_groups'); } + /* this is just a shim for SCIM to work */ + public function members() + { + return $this->users(); + } + /** * Get the user that created the group * diff --git a/app/Models/SCIMUser.php b/app/Models/SCIMUser.php index f261fea8b648..87b023dc5843 100644 --- a/app/Models/SCIMUser.php +++ b/app/Models/SCIMUser.php @@ -13,4 +13,11 @@ public function __construct(array $attributes = []) $attributes['password'] = $this->noPassword(); parent::__construct($attributes); } + + // Have to re-define this here because Eloquent will try to 'guess' a foreign key of s_c_i_m_user_id + // from SCIMUser + public function groups() + { + return $this->belongsToMany(\App\Models\Group::class, 'users_groups', 'user_id', 'group_id'); + } } \ No newline at end of file diff --git a/app/Models/SnipeSCIMConfig.php b/app/Models/SnipeSCIMConfig.php index 7387569e103b..a78bd6632c2b 100644 --- a/app/Models/SnipeSCIMConfig.php +++ b/app/Models/SnipeSCIMConfig.php @@ -2,250 +2,478 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Model; -use Helper; +use ArieTimmerman\Laravel\SCIMServer\Helper; +use ArieTimmerman\Laravel\SCIMServer\Parser\Path; use ArieTimmerman\Laravel\SCIMServer\SCIM\Schema; -use ArieTimmerman\Laravel\SCIMServer\Attribute\AttributeMapping; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Attribute; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Collection; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Complex; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Constant; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Eloquent; +use ArieTimmerman\Laravel\SCIMServer\Attribute\JSONCollection; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Meta; +use ArieTimmerman\Laravel\SCIMServer\Attribute\MutableCollection; +use ArieTimmerman\Laravel\SCIMServer\Attribute\Schema as AttributeSchema; +use Illuminate\Database\Eloquent\Model; + +function a($name = null): Attribute +{ + return new Attribute($name); +} +function complex($name = null): Complex +{ + return new Complex($name); +} -class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig +function eloquent($name, $attribute = null): Attribute { - public function getUserConfig() + return new Eloquent($name, $attribute); +} + +class MappedTable extends Attribute +{ + public function __construct( + private string $scim_attribute_name, + private string $relationship_name, + private string $relationship_class, + private string $relationship_id_field, + private string $relationship_field) { - // Much of this is copied verbatim from the library, then adjusted for our needs + parent::__construct($this->scim_attribute_name); + } - /* - more snipe-it attributes I'd like to check out (to map to 'enterprise' maybe?): - - website - - notes? - - remote??? - - location_id ? - - company_id to "organization?" - */ + protected function doRead(&$object, $attributes = []) + { + return $object->{$this->relationship_name}?->{$this->relationship_field}; + } + public function add($value, Model &$object) + { + \Log::error("Structure of 'value' is going to be weird - " . print_r($value, true)); + $object->{$this->relationship_id_field} = $value ? $this->relationship_class::firstOrCreate([$this->relationship_field => $value])->id : null; + } + + public function replace($value, Model &$object, $path = null, $removeIfNotSet = false) + { + $object->{$this->relationship_id_field} = $value ? $this->relationship_class::firstOrCreate([$this->relationship_field => $value])->id : null; + } + + public function patch($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + \Log::error("implementing custom patch for value: '$value' of attribute " . $this->scim_attribute_name); + $object->{$this->relationship_id_field} = $value ? $this->relationship_class::firstOrCreate([$this->relationship_field => $value])->id : null; + } - $user_prefix = 'urn:ietf:params:scim:schemas:core:2.0:User:'; - $enterprise_prefix = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:'; +} + +class UpdatableComplex extends Complex +{ + public function doWrite($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + throw new \Exception("doWrite is not implemented yet for Operation: $operation on attribute " . $this->getFullKey()); + } + + public function add($value, Model &$object) + { + $this->doWrite("add", $value, $object); + } + + public function replace($value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + $this->doWrite("replace", $value, $object, $path, $removeIfNotSet); + } + + public function patch($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + //FIXME - what to do with $operation?!?!!? + // Also - we don't really have a good repeatable way to do this :/ + // so we're probably going to end up just overriding this anyways :( + $this->doWrite("patch", $value, $object, $path, $removeIfNotSet); + } + + public function remove($value, Model &$object, Path $path = null) + { + $this->doWrite("remove", null, $object, $path); + } +} + + +class SnipeSCIMConfig +{ + public function __construct() + { + } + + public function getConfigForResource($name) + { + $result = $this->getConfig(); + return @$result[$name]; + } + + public function getGroupClass() + { + return Group::class; + } + + const ENTERPRISE = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'; + const GROKABILITY = 'urn:ietf:params:scim:schemas:extension:grokability:2.0:User'; + + public function getUserConfig() + { return [ // Set to 'null' to make use of auth.providers.users.model (App\User::class) - 'class' => SCIMUser::class, - - 'validations' => [ - $user_prefix . 'userName' => 'required', - $user_prefix . 'displayName' => 'nullable|string', - $user_prefix . 'name.givenName' => 'required', - $user_prefix . 'name.familyName' => 'nullable|string', - $user_prefix . 'externalId' => 'nullable|string', - $user_prefix . 'emails' => 'nullable|array', - $user_prefix . 'emails.*.value' => 'nullable|email', - $user_prefix . 'active' => 'boolean', - $user_prefix . 'phoneNumbers' => 'nullable|array', - $user_prefix . 'phoneNumbers.*.value' => 'nullable|string', - $user_prefix . 'addresses' => 'nullable|array', - $user_prefix . 'addresses.*.streetAddress' => 'nullable|string', - $user_prefix . 'addresses.*.locality' => 'nullable|string', - $user_prefix . 'addresses.*.region' => 'nullable|string', - $user_prefix . 'addresses.*.postalCode' => 'nullable|string', - $user_prefix . 'addresses.*.country' => 'nullable|string', - $user_prefix . 'title' => 'nullable|string', - $user_prefix . 'preferredLanguage' => 'nullable|string', - - // Enterprise validations: - $enterprise_prefix . 'employeeNumber' => 'nullable|string', - $enterprise_prefix . 'department' => 'nullable|string', - $enterprise_prefix . 'manager' => 'nullable', - $enterprise_prefix . 'manager.value' => 'nullable|string' - ], - + 'class' => ScimUser::class, 'singular' => 'User', - 'schema' => [Schema::SCHEMA_USER], //eager loading 'withRelations' => [], - 'map_unmapped' => false, - // 'unmapped_namespace' => 'urn:ietf:params:scim:schemas:laravel:unmapped', 'description' => 'User Account', - // Map a SCIM attribute to an attribute of the object. - 'mapping' => [ - - 'id' => (new AttributeMapping())->setRead( - function (&$object) { + 'map' => complex()->withSubAttributes( + new class ('schemas', [ + "urn:ietf:params:scim:schemas:core:2.0:User", + self::ENTERPRISE, + self::GROKABILITY + ]) extends Constant { + public function replace($value, &$object, $path = null) + { + // do nothing + $this->dirty = true; + } + }, + (new class ('id', null) extends Constant { // TODO - this 'id' is in the same namespace for objects OR groups? + protected function doRead(&$object, $attributes = []) + { return (string)$object->id; } - )->disableWrite(), - - 'externalId' => AttributeMapping::eloquent('scim_externalid'), // FIXME - I have a PR that changes a lot of this. - - 'meta' => [ - 'created' => AttributeMapping::eloquent("created_at")->disableWrite(), - 'lastModified' => AttributeMapping::eloquent("updated_at")->disableWrite(), - - 'location' => (new AttributeMapping())->setRead( - function ($object) { - return route( - 'scim.resource', - [ - 'resourceType' => 'Users', - 'resourceObject' => $object->id - ] - ); - } - )->disableWrite(), - - 'resourceType' => AttributeMapping::constant("User") - ], - - 'schemas' => AttributeMapping::constant( - [ - 'urn:ietf:params:scim:schemas:core:2.0:User', - 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' - ] - )->ignoreWrite(), - 'urn:ietf:params:scim:schemas:core:2.0:User' => [ - - 'userName' => AttributeMapping::eloquent("username"), + public function remove($value, &$object, $path = null) + { + // do nothing + } + } + ), + new Meta('Users'), + (new AttributeSchema(Schema::SCHEMA_USER, true))->withSubAttributes( + eloquent('userName', 'username')->ensure('required'), + (new class ('active', 'activated') extends Eloquent { + protected function doRead(&$object, $attributes = []) + { + return (bool)$object->activated; // need this extension to force boolean-ness + } + }), + complex('name')->withSubAttributes( + eloquent('givenName', 'first_name')->ensure('required'), + eloquent('familyName', 'last_name'), + ), // ->ensure('required'), It *is* a bit weird, but I would've thought 'name' is required since 'givenName' is required? But apparently not? + eloquent('displayName', 'display_name'), //yes, this is *not* under 'name' - that's the spec + //eloquent('password')->ensure('nullable')->setReturned('never'), + eloquent('externalId', 'scim_externalid'), + + // Email chonk + (new class ('emails') extends UpdatableComplex { + protected function doRead(&$object, $attributes = []) + { + return collect([$object->email])->map(function ($email) { + return [ + 'value' => $email, + 'type' => 'work', //TODO - is this how we always have done it? + 'primary' => true + ]; + })->toArray(); + } - 'name' => [ - 'formatted' => (new AttributeMapping())->ignoreWrite()->setRead( - function (&$object) { - return $object->getFullNameAttribute(); + public function doWrite($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + if ($value) { + $object->email = $value[0]['value']; + } else { + $object->email = null; } - ), - 'familyName' => AttributeMapping::eloquent("last_name"), - 'givenName' => AttributeMapping::eloquent("first_name"), - 'middleName' => null, - 'honorificPrefix' => null, - 'honorificSuffix' => null - ], - - 'displayName' => AttributeMapping::eloquent("display_name"), - 'nickName' => null, - 'profileUrl' => null, - 'title' => AttributeMapping::eloquent('jobtitle'), - 'userType' => null, - 'preferredLanguage' => AttributeMapping::eloquent('locale'), // Section 5.3.5 of [RFC7231] - 'locale' => null, // see RFC5646 - 'timezone' => null, // see RFC6557 - 'active' => (new AttributeMapping())->setAdd( - function ($value, &$object) { - $object->activated = $value; } - )->setReplace( - function ($value, &$object) { - $object->activated = $value; + })->withSubAttributes( + eloquent('value', 'email')->ensure('email'), + new Constant('type', 'work'), + new Constant('primary', true)->ensure('boolean') + )->ensure('array') + ->setMultiValued(true), + + // phone chonk + (new class ('phoneNumbers') extends Complex { + protected function doRead(&$object, $attributes = []) + { + $phones = []; + if ($object->phone) { + $phones[] = [ + 'value' => $object->phone, + 'type' => 'work' + ]; + } + if ($object->mobile) { + $phones[] = [ + 'value' => $object->mobile, + 'type' => 'mobile' + ]; + } + return $phones; } - )->setRead( - // this works as specified. - function (&$object) { - return (bool)$object->activated; + + public function doWrite($operation, $value, Model &$object, Path $path = null) + { + \Log::error("Phones 'value' is: " . print_r($value, true)); + foreach ($value as $phone) { + switch ($phone['type']) { + case 'work': + $object->phone = $phone['value']; + break; + + case 'mobile': + $object->mobile = $phone['value']; + break; + + default: + throw new \Exception("Unknown phone type '{$phone['type']}'"); + } + } } - ), - 'password' => AttributeMapping::eloquent('password')->disableRead(), - - // Multi-Valued Attributes - 'emails' => [[ - "value" => AttributeMapping::eloquent("email"), - "display" => null, - "type" => AttributeMapping::constant("work")->ignoreWrite(), - "primary" => AttributeMapping::constant(true)->ignoreWrite() - ]], - - 'phoneNumbers' => [[ - "value" => AttributeMapping::eloquent("phone"), - "display" => null, - "type" => AttributeMapping::constant("work")->ignoreWrite(), - "primary" => AttributeMapping::constant(true)->ignoreWrite() - ]], - - 'ims' => [[ - "value" => null, - "display" => null, - "type" => null, - "primary" => null - ]], // Instant messaging addresses for the User - - 'photos' => [[ - "value" => null, - "display" => null, - "type" => null, - "primary" => null - ]], - - 'addresses' => [[ - 'type' => AttributeMapping::constant("work")->ignoreWrite(), - 'formatted' => AttributeMapping::constant("n/a")->ignoreWrite(), // TODO - is this right? This doesn't look right. - 'streetAddress' => AttributeMapping::eloquent("address"), - 'locality' => AttributeMapping::eloquent("city"), - 'region' => AttributeMapping::eloquent("state"), - 'postalCode' => AttributeMapping::eloquent("zip"), - 'country' => AttributeMapping::eloquent("country"), - 'primary' => AttributeMapping::constant(true)->ignoreWrite() //this isn't in the example? - ]], - - 'groups' => [[ - 'value' => null, - '$ref' => null, - 'display' => null, - 'type' => null, - ]], - - 'entitlements' => null, - 'roles' => null, - 'x509Certificates' => null - ], - - 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User' => [ - 'employeeNumber' => AttributeMapping::eloquent('employee_num'), - 'department' => (new AttributeMapping())->setAdd( // FIXME parent? - function ($value, &$object) { - $department = Department::where("name", $value)->first(); - if ($department) { - $object->department_id = $department->id; + + public function patch($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + if ($path->getValuePathFilter() != null) { + \Log::error("value object IS: " . print_r($value, true)); + if ((string)$path == 'phoneNumbers[type eq "mobile"].value') { + \Log::error("YAY!!!!! We are patching an mobile fone! We can do this!"); //FIXME + $object->mobile = $value; //I don't know why the value is the raw value, but it is? + return; + } + if ((string)$path == 'phoneNumbers[type eq "work"].value') { + \Log::error("Patching work number!"); + $object->phone = $value; //similar, don't know why, but it is + return; + } + \Log::error("Uh-oh, maybe doing something weirder - path is: $path"); } + parent::patch($operation, $value, $object, $path, $removeIfNotSet); } - )->setReplace( - function ($value, &$object) { - $department = Department::where("name", $value)->first(); - if ($department) { - $object->department_id = $department->id; + })->withSubAttributes( + new Constant('value', 'email')->ensure('string'), + new Constant('type', 'other'), + new Constant('primary', true)->ensure('boolean'), + )->ensure('array') + ->setMultiValued(true), + + // addresses chonk + (new class ('addresses') extends Complex { + static $addressmap = [ + 'streetAddress' => 'address', + 'locality' => 'city', + 'region' => 'state', + 'postalCode' => 'zip', + 'country' => 'country' + ]; + + protected function doRead(&$object, $attributes = []) + { + $address = []; + foreach (self::$addressmap as $scim_field => $db_field) { + if ($object->{$db_field}) { + $address[$scim_field] = $object->{$db_field}; + } } + if (count($address) > 0) { + $address['type'] = 'work'; + $address['primary'] = true; + } + return $address; } - )->setRead( - function (&$object) { - return $object->department ? $object->department->name : null; + + public function patch($operation, $value, Model &$object, Path $path = null, $removeIfNotSet = false) + { + if ($path->getValuePathFilter() != null) { + \Log::error("path for update $path"); + // get the part of the $path that we actually care about - something like: + // addresses[type eq "work"] + $matches = null; + if (!preg_match('/^.+\[type eq "([a-zA-Z]+)"](?:\.([a-zA-Z]+))?$/', (string)$path, $matches)) { + throw new \Exception("Unknown path type '$path'"); + } + $type = $matches[1]; + if ($type != 'work') { + throw new \Exception("Unknown object type '$type'"); + } + $attribute = array_key_exists(2, $matches) ? $matches[2] : null; + if (array_key_exists($attribute, self::$addressmap)) { + $object->{self::$addressmap[$attribute]} = $value; + return; + } + + + throw new \Exception("path for update $path"); + } } + + })->withSubAttributes( + eloquent('streetAddress', 'address'), + eloquent('locality', 'city'), + eloquent('region', 'state'), + eloquent('postalCode', 'zip'), + eloquent('country', 'country'), + new Constant('type', 'other'), + new Constant('primary', true)->ensure('boolean') + )->ensure('array') + ->setMultiValued(true), + + eloquent('title', 'jobtitle'), + eloquent('preferredLanguage', 'locale'), + (new Collection('groups'))->withSubAttributes( + eloquent('value', 'id'), + (new class ('$ref') extends Eloquent { + protected function doRead(&$object, $attributes = []) + { + \Log::error("Checking to see if our 'doRead' even gets a chance to get called?"); + return route( + 'scim.resource', + [ + 'resourceType' => 'Group', + 'resourceObject' => $object->id ?? "not-saved" + ] + ); + } + }), + eloquent('display', 'name') ), - 'manager' => [ - // FIXME - manager writes are disabled. This kinda works but it leaks errors all over the place. Not cool. - // '$ref' => (new AttributeMapping())->ignoreWrite()->ignoreRead(), - // 'displayName' => (new AttributeMapping())->ignoreWrite()->ignoreRead(), - // NOTE: you could probably do a 'plain' Eloquent mapping here, but we don't for future-proofing - 'value' => (new AttributeMapping())->setAdd( - function ($value, &$object) { - $manager = User::find($value); - if ($manager) { - $object->manager_id = $manager->id; - } + (new JSONCollection('roles'))->withSubAttributes( // TODO - what is this? + eloquent('value')->ensure('required', 'min:3', 'alpha_dash:ascii'), + eloquent('display')->ensure('nullable', 'min:3', 'alpha_dash:ascii'), + eloquent('type')->ensure('nullable', 'min:3', 'alpha_dash:ascii'), + eloquent('primary')->ensure('boolean')->default(false) + )->ensure('nullable', 'array', 'max:20') + ), + (new AttributeSchema(self::ENTERPRISE, false))->withSubAttributes( + eloquent('employeeNumber', 'employee_num')->ensure('nullable'), + new MappedTable('department', 'department', Department::class, 'department_id', 'name'), + (new class('manager') extends Complex { + protected function doRead(&$object, $attributes = []) + { + if (!$object->manager) { + return null; } - )->setReplace( - function ($value, &$object) { - $manager = User::find($value); - if ($manager) { - $object->manager_id = $manager->id; - } + return [ + 'value' => $object->manager->id, //TODO - ID's aren't unique like they're supposed to be :/ + '$ref' => route('scim.resource', ['resourceType' => 'User', 'resourceObject' => $object->manager->id]), + 'displayName' => $object->manager->display_name, + ]; + } + + public function add($value, Model &$object) + { + \Log::error("What type of value is value? " . gettype($value)); + if (is_scalar($value)) { + \Log::error("Weird Microsoft mode - set manager to the \$value and move on with life?"); + $object->manager_id = $value; + } else { + //FIXME - do this properly + \Log::error("Non-Microsoft - Trying to 'ADD' for maanger with value: " . print_r($value, true)); + throw new \Exception("dunno how to do this (add manager)"); + } + } + + // TODO - we keep repeating ourselves between add/replace, we should maybe make our own class + // to make this a nicer shorthand? + public function replace($value, Model &$object, $path = null, $removeIfNotSet = false) + { + \Log::error("What type of value is value? " . gettype($value)); + if (is_scalar($value)) { + \Log::error("Weird Microsoft mode - set manager to the \$value and move on with life?"); + $object->manager_id = $value; + } else { + //FIXME - actualy do this? (Try on one of the other platforms) + \Log::error("Non-Microsoft - Trying to 'ADD' for maanger with value: " . print_r($value, true)); + throw new \Exception("dunno how to do this (add manager)"); } - )->setRead( - function (&$object) { - return $object->manager_id; + } + }) // ->withSubAttributes() ... -> ensure() ? + ), + (new AttributeSchema(self::GROKABILITY, false))->withSubAttributes( + new MappedTable('location', 'location', Location::class, 'location_id', 'name'), + new MappedTable('company', 'company', Company::class, 'company_id', 'name'), + ) + ), + ]; + } + + public function getGroupConfig() + { + return [ + + 'class' => $this->getGroupClass(), + 'singular' => 'Group', + + //eager loading + 'withRelations' => [], + 'description' => 'Group', + + 'map' => complex()->withSubAttributes( + new class ('schemas', [ + "urn:ietf:params:scim:schemas:core:2.0:Group", + ]) extends Constant { + public function replace($value, &$object, $path = null) + { + // do nothing + $this->dirty = true; + } + }, + (new class ('id', null) extends Constant { + protected function doRead(&$object, $attributes = []) + { + return (string)$object->id; + } + + public function remove($value, &$object, $path = null) + { + // do nothing + } + } + ), + eloquent('externalId', 'scim_externalid'), + new Meta('Groups'), + (new AttributeSchema(Schema::SCHEMA_GROUP, true))->withSubAttributes( + eloquent('displayName', 'name')->ensure('required', 'min:3', function ($attribute, $value, $fail) { + // check if group does not exist or if it exists, it is the same group + $group = $this->getGroupClass()::where('name', $value)->first(); + if ($group && (request()->route('resourceObject') == null || $group->id != request()->route('resourceObject')->id)) { + $fail('The name has already been taken.'); + } + }), + (new MutableCollection('members'))->withSubAttributes( + eloquent('value', 'id')->ensure('required'), + (new class ('$ref') extends Eloquent { + protected function doRead(&$object, $attributes = []) + { + return route( + 'scim.resource', + [ + 'resourceType' => 'Users', + 'resourceObject' => $object->id ?? "not-saved" + ] + ); } - ), - ] - ] - ] + }), + eloquent('display', 'name') + )->ensure('nullable', 'array') + ) + ), + ]; + } + + public function getConfig() + { + return [ + 'Users' => $this->getUserConfig(), + 'Groups' => $this->getGroupConfig(), ]; } } diff --git a/composer.json b/composer.json index f7b86417562e..0a33a3b0df52 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "ext-mbstring": "*", "ext-pdo": "*", "alek13/slack": "^2.0", - "arietimmerman/laravel-scim-server": "dev-laravel_11_compatibility", + "arietimmerman/laravel-scim-server": "dev-upstream_master", "bacon/bacon-qr-code": "^2.0", "barryvdh/laravel-debugbar": "^3.13", "barryvdh/laravel-dompdf": "^2.0", diff --git a/composer.lock b/composer.lock index f3a06b455651..9069fb03179e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4a00fb4c4b9a2ac161183e9ae9217298", + "content-hash": "1086d1b9a8b59a62d87c4e74a5babf89", "packages": [ { "name": "alek13/slack", @@ -74,29 +74,30 @@ }, { "name": "arietimmerman/laravel-scim-server", - "version": "dev-laravel_11_compatibility", + "version": "dev-upstream_master", "source": { "type": "git", "url": "https://github.com/grokability/laravel-scim-server.git", - "reference": "6c771799090bfe04dcee94a1dc9f82870aed4dbe" + "reference": "da40db79d76cf3b4c7e57cde41df6ecf8119afb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/grokability/laravel-scim-server/zipball/6c771799090bfe04dcee94a1dc9f82870aed4dbe", - "reference": "6c771799090bfe04dcee94a1dc9f82870aed4dbe", + "url": "https://api.github.com/repos/grokability/laravel-scim-server/zipball/da40db79d76cf3b4c7e57cde41df6ecf8119afb7", + "reference": "da40db79d76cf3b4c7e57cde41df6ecf8119afb7", "shasum": "" }, "require": { - "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", - "php": "^7.0|^8.0", + "illuminate/console": "^10.0|^11.0|^12.0", + "illuminate/database": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.0", "tmilos/scim-filter-parser": "^1.3", "tmilos/scim-schema": "^0.1.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.66", "laravel/legacy-factories": "*", - "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0" + "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0" }, "type": "library", "extra": { @@ -130,9 +131,9 @@ ], "description": "Laravel Package for creating a SCIM server", "support": { - "source": "https://github.com/grokability/laravel-scim-server/tree/laravel_11_compatibility" + "source": "https://github.com/grokability/laravel-scim-server/tree/upstream_master" }, - "time": "2025-01-20T14:49:28+00:00" + "time": "2025-08-28T19:24:40+00:00" }, { "name": "aws/aws-crt-php", diff --git a/database/migrations/2025_09_25_124321_add_external_id_to_groups.php b/database/migrations/2025_09_25_124321_add_external_id_to_groups.php new file mode 100644 index 000000000000..c6e58c0cd11a --- /dev/null +++ b/database/migrations/2025_09_25_124321_add_external_id_to_groups.php @@ -0,0 +1,28 @@ +string('scim_externalid')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('permission_groups', function (Blueprint $table) { + $table->dropColumn('scim_externalid'); + }); + } +};