Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SCIM support via laravel-scim-server package, using self-generated routes, secured by current API key system #10802

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
abeb4bb
SCIM integration using the 're-do-the routes' approach, which seems l…
uberbrady Mar 4, 2022
3f5c6bf
Cleaning up routes to match laravel-scim-server's recommended impleme…
uberbrady Mar 8, 2022
1a2967f
Some actually *working* changes for SCIM support?!
uberbrady Mar 8, 2022
055c0e0
Whoops, forgot my route file
uberbrady Mar 8, 2022
d4a59a7
Fix public SCIM routes
uberbrady Mar 9, 2022
111b65f
Removed Ziggy, removed old generated file, yanked Ziggy references
uberbrady Mar 16, 2022
3d003bb
Resolves the first set of comments for SCIM
uberbrady Mar 19, 2022
e70a934
Ensure all /api routes have baseUrl prepended
uberbrady Mar 19, 2022
d3be7e0
Fix the parent:: call to be, uh, actually correct :P
uberbrady Mar 19, 2022
a38deee
Clarify the route-ordering, as it is quite tricky
uberbrady Mar 19, 2022
ed89cf6
This gets it so that users can actually be saved..
uberbrady Mar 22, 2022
18f0186
Work around the lack of callbacks with some inheritance
uberbrady Mar 22, 2022
0d4842f
Mapped a bunch more fields from SCIM into Snipe-IT's user table
uberbrady Mar 22, 2022
5a6c16b
More baseUrl shenanigans since we yanked Ziggy :/
uberbrady Mar 24, 2022
5f4c4a6
Properly map job title and work with some other necessary attributes
uberbrady Mar 24, 2022
884ede6
Map more fields...
uberbrady Mar 28, 2022
65f0796
Finalized basic mapping for core and enterprise namespaces
uberbrady Mar 29, 2022
8f5aadb
Latest tuned settings for SCIM config to work with Azure (and others)
uberbrady Mar 30, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/Models/SCIMUser.php
@@ -0,0 +1,16 @@
<?php

namespace App\Models;

class SCIMUser extends User
{
protected $table = 'users';

protected $throwValidationExceptions = true; // we want model-level validation to fully THROW, not just return false

public function __construct(array $attributes = []) {
$attributes['password'] = "*NO PASSWORD*";
// $attributes['activated'] = 1;
parent::__construct($attributes);
}
}
174 changes: 174 additions & 0 deletions app/Models/SnipeSCIMConfig.php
@@ -0,0 +1,174 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Helper;
use ArieTimmerman\Laravel\SCIMServer\SCIM\Schema;
use ArieTimmerman\Laravel\SCIMServer\Attribute\AttributeMapping;


class SnipeSCIMConfig extends \ArieTimmerman\Laravel\SCIMServer\SCIMConfig
{
public function getUserConfig()
{
$config = parent::getUserConfig();

// Much of this is copied verbatim from the library, then adjusted for our needs
uberbrady marked this conversation as resolved.
Show resolved Hide resolved
$config['class'] = SCIMUser::class;

unset($config['mapping']['example:name:space']);

$config['map_unmapped'] = false; // anything we don't explicitly map will _not_ show up.

$core_namespace = 'urn:ietf:params:scim:schemas:core:2.0:User';
$core = $core_namespace.':';
$mappings =& $config['mapping'][$core_namespace]; //grab this entire key, we don't want to be repeating ourselves

//username - *REQUIRED*
$config['validations'][$core.'userName'] = 'required';
$mappings['userName'] = AttributeMapping::eloquent('username');

//human name - *FIRST NAME REQUIRED*
$config['validations'][$core.'name.givenName'] = 'required';
$config['validations'][$core.'name.familyName'] = 'string'; //not required

$mappings['name']['familyName'] = AttributeMapping::eloquent("last_name");
$mappings['name']['givenName'] = AttributeMapping::eloquent("first_name");
$mappings['name']['formatted'] = (new AttributeMapping())->ignoreWrite()->setRead(
function (&$object) {
return $object->getFullNameAttribute();
}
);

$config['validations'][$core.'emails'] = 'nullable|array'; // emails are not required in Snipe-IT...
$config['validations'][$core.'emails.*.value'] = 'required|email'; // ...but if you give us one, it better be an email address

$mappings['emails'] = [[
"value" => AttributeMapping::eloquent("email"),
"display" => null,
"type" => AttributeMapping::constant("work")->ignoreWrite(),
"primary" => AttributeMapping::constant(true)->ignoreWrite()
]];

//active
$config['validations'][$core.'active'] = 'boolean';

$mappings['active'] = AttributeMapping::eloquent('activated');

//phone
$config['validations'][$core.'phoneNumbers'] = 'nullable|array';
$config['validations'][$core.'phoneNumbers.*.value'] = 'required';

$mappings['phoneNumbers'] = [[
"value" => AttributeMapping::eloquent("phone"),
"display" => null,
"type" => AttributeMapping::constant("work")->ignoreWrite(),
"primary" => AttributeMapping::constant(true)->ignoreWrite()
]];

//address
$config['validations'][$core.'addresses'] = 'nullable|array';
$config['validations'][$core.'addresses.*.streetAddress'] = 'required';
$config['validations'][$core.'addresses.*.locality'] = 'string';
$config['validations'][$core.'addresses.*.region'] = 'string';
$config['validations'][$core.'addresses.*.postalCode'] = 'string';
$config['validations'][$core.'addresses.*.country'] = 'string';

$mappings['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?
]];

//title
$config['validations'][$core.'title'] = 'string';
$mappings['title'] = AttributeMapping::eloquent('jobtitle');

//Preferred Language
$config['validations'][$core.'preferredLanguage'] = 'string';
$mappings['preferredLanguage'] = AttributeMapping::eloquent('locale');

/*
more snipe-it attributes I'd like to check out (to map to 'enterprise' maybe?):
- website
- notes?
- remote???
- location_id ?
- company_id to "organization?"
*/

$enterprise_namespace = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User';
$ent = $enterprise_namespace.':';

// we remove the 'example' namespace and add the Enterprise one
$config['mapping']['schemas'] = AttributeMapping::constant( [$core_namespace, $enterprise_namespace] )->ignoreWrite();

$config['validations'][$ent.'employeeNumber'] = 'string';
$config['validations'][$ent.'department'] = 'string';
$config['validations'][$ent.'manager'] = 'nullable';
$config['validations'][$ent.'manager.value'] = 'string';

$config['mapping'][$enterprise_namespace] = [
'employeeNumber' => AttributeMapping::eloquent('employee_num'),
'department' =>(new AttributeMapping())->setAdd( // FIXME parent?
function ($value, &$object) {
\Log::error("Department-Add: $value"); //FIXME
$department = Department::where("name", $value)->first();
if ($department) {
$object->department_id = $department->id;
}
}
)->setReplace(
function ($value, &$object) {
\Log::error("Department-Replace: $value"); //FIXME
$department = Department::where("name", $value)->first();
if ($department) {
$object->department_id = $department->id;
}
}
)->setRead(
function (&$object) {
\Log::error("Weird department reader firing..."); //FIXME
return $object->department ? $object->department->name : null;
}
),
'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) {
\Log::error("Manager-Add: $value"); //FIXME
$manager = User::find($value);
if ($manager) {
$object->manager_id = $manager->id;
}
}
)->setReplace(
function ($value, &$object) {
\Log::error("Manager-Replace: $value"); //FIXME
$manager = User::find($value);
if ($manager) {
$object->manager_id = $manager->id;
}
}
)->setRead(
function (&$object) {
\Log::error("Weird manager reader firing..."); //FIXME
return $object->manager_id;
}
),
]
];

return $config;
}

}
3 changes: 3 additions & 0 deletions app/Providers/AppServiceProvider.php
Expand Up @@ -8,6 +8,7 @@
use App\Models\Consumable;
use App\Models\License;
use App\Models\Setting;
use App\Models\SnipeSCIMConfig;
use App\Observers\AccessoryObserver;
use App\Observers\AssetObserver;
use App\Observers\ComponentObserver;
Expand Down Expand Up @@ -80,6 +81,8 @@ public function register()
if ($this->app->environment(['local', 'develop'])) {
$this->app->register(\Laravel\Dusk\DuskServiceProvider::class);
}

$this->app->singleton('ArieTimmerman\Laravel\SCIMServer\SCIMConfig', SnipeSCIMConfig::class); // this overrides the default SCIM configuration with our own

}
}
2 changes: 1 addition & 1 deletion app/Providers/RouteServiceProvider.php
Expand Up @@ -24,7 +24,7 @@ public function boot()

$this->mapWebRoutes();

//
require base_path('routes/scim.php');
});
}

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Expand Up @@ -18,6 +18,7 @@
"ext-mbstring": "*",
"ext-pdo": "*",
"alek13/slack": "^2.0",
"arietimmerman/laravel-scim-server": "^0.5.5",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-debugbar": "^3.6",
"doctrine/cache": "^1.10",
Expand Down Expand Up @@ -60,7 +61,6 @@
"rollbar/rollbar-laravel": "^7.0",
"spatie/laravel-backup": "^6.16",
"tecnickcom/tc-lib-barcode": "^1.15",
"tightenco/ziggy": "v1.2.0",
"unicodeveloper/laravel-password": "^1.0",
"watson/validating": "^6.1"
},
Expand Down