SCIM integration using the 're-do-the routes' approach, which seems like a dead-end

Cleaning up routes to match laravel-scim-server's recommended implementation

Some actually *working* changes for SCIM support?!

Whoops, forgot my route file

Fix public SCIM routes

Removed Ziggy, removed old generated file, yanked Ziggy references

Resolves the first set of comments for SCIM

Ensure all /api routes have baseUrl prepended

Fix the parent:: call to be, uh, actually correct :P

Clarify the route-ordering, as it is quite tricky

This gets it so that users can actually be saved..

Work around the lack of callbacks with some inheritance

Mapped a bunch more fields from SCIM into Snipe-IT's user table

More baseUrl shenanigans since we yanked Ziggy :/

Properly map job title and work with some other necessary attributes

Map more fields...

Finalized basic mapping for core and enterprise namespaces

Latest tuned settings for SCIM config to work with Azure (and others)
This commit is contained in:
Brady Wetherington 2022-03-04 14:00:00 -08:00
parent 56ee5c50a9
commit 6756dd193e
13 changed files with 498 additions and 77 deletions

16
app/Models/SCIMUser.php Normal file
View file

@ -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);
}
}

View file

@ -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
$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;
}
}

View file

@ -8,6 +8,7 @@ use App\Models\Component;
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;
@ -81,5 +82,7 @@ class AppServiceProvider extends ServiceProvider
$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
}
}

View file

@ -24,7 +24,7 @@ class RouteServiceProvider extends ServiceProvider
$this->mapWebRoutes();
//
require base_path('routes/scim.php');
});
}

View file

@ -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",
"barryvdh/laravel-dompdf": "^1.0",
@ -61,7 +62,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"
},

322
composer.lock generated
View file

@ -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": "442a6af235589e35cfcaa7e5e39e75ec",
"content-hash": "448c7508ab99eb86eb62e5cac3e9ee59",
"packages": [
{
"name": "alek13/slack",
@ -72,6 +72,65 @@
],
"time": "2021-10-20T22:52:32+00:00"
},
{
"name": "arietimmerman/laravel-scim-server",
"version": "v0.5.5",
"source": {
"type": "git",
"url": "https://github.com/arietimmerman/laravel-scim-server.git",
"reference": "8fb5b1cc0d28ace820b5b38a543d801fd49ada90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/arietimmerman/laravel-scim-server/zipball/8fb5b1cc0d28ace820b5b38a543d801fd49ada90",
"reference": "8fb5b1cc0d28ace820b5b38a543d801fd49ada90",
"shasum": ""
},
"require": {
"illuminate/console": "^6.0|^7.0|^8.0",
"illuminate/database": "^6.0|^7.0|^8.0",
"illuminate/support": "^6.0|^7.0|^8.0",
"php": "^7.0|^8.0",
"tmilos/scim-filter-parser": "^1.3",
"tmilos/scim-schema": "^0.1.0"
},
"require-dev": {
"laravel/legacy-factories": "*",
"orchestra/testbench": "^5.0|^6.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"ArieTimmerman\\Laravel\\SCIMServer\\ServiceProvider"
],
"aliases": {
"SCIMServerHelper": "ArieTimmerman\\Laravel\\SCIMServer\\Helper"
}
}
},
"autoload": {
"psr-4": {
"ArieTimmerman\\Laravel\\SCIMServer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Arie Timmerman",
"email": "arietimmerman@gmail.com"
}
],
"description": "Laravel Package for creating a SCIM server",
"support": {
"issues": "https://github.com/arietimmerman/laravel-scim-server/issues",
"source": "https://github.com/arietimmerman/laravel-scim-server/tree/v0.5.5"
},
"time": "2022-02-03T20:56:31+00:00"
},
{
"name": "asm89/stack-cors",
"version": "v2.1.1",
@ -11030,72 +11089,6 @@
],
"time": "2021-12-31T09:40:23+00:00"
},
{
"name": "tightenco/ziggy",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "147804d5f3e98b897fc1ed15efc2807f1099cf83"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/147804d5f3e98b897fc1ed15efc2807f1099cf83",
"reference": "147804d5f3e98b897fc1ed15efc2807f1099cf83",
"shasum": ""
},
"require": {
"laravel/framework": ">=5.4@dev"
},
"require-dev": {
"orchestra/testbench": "^6.0",
"phpunit/phpunit": "^9.2"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tightenco\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tightenco\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
},
{
"name": "Jake Bathman",
"email": "jake@tighten.co"
},
{
"name": "Jacob Baker-Kretzmar",
"email": "jacob@tighten.co"
}
],
"description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.",
"homepage": "https://github.com/tighten/ziggy",
"keywords": [
"Ziggy",
"javascript",
"laravel",
"routes"
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v1.2.0"
},
"time": "2021-05-24T22:46:59+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.4",
@ -11149,6 +11142,199 @@
},
"time": "2021-12-08T09:12:39+00:00"
},
{
"name": "tmilos/lexer",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/tmilos/lexer.git",
"reference": "e7885595614759f1da2ff79b66e3fb26d7f875fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/lexer/zipball/e7885595614759f1da2ff79b66e3fb26d7f875fa",
"reference": "e7885595614759f1da2ff79b66e3fb26d7f875fa",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "~5.6",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tmilos\\Lexer\\": "src/",
"Tests\\Tmilos\\Lexer\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com"
}
],
"description": "Lexical analyzer with individual token definition with regular expressions",
"keywords": [
"lexer",
"parser"
],
"support": {
"issues": "https://github.com/tmilos/lexer/issues",
"source": "https://github.com/tmilos/lexer/tree/master"
},
"time": "2016-12-21T11:22:39+00:00"
},
{
"name": "tmilos/scim-filter-parser",
"version": "1.3.0",
"source": {
"type": "git",
"url": "https://github.com/tmilos/scim-filter-parser.git",
"reference": "cfd9ba1f33e1e15adcab2481bffd74cb9fb35704"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/scim-filter-parser/zipball/cfd9ba1f33e1e15adcab2481bffd74cb9fb35704",
"reference": "cfd9ba1f33e1e15adcab2481bffd74cb9fb35704",
"shasum": ""
},
"require": {
"tmilos/lexer": "^1.0",
"tmilos/value": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tmilos\\ScimFilterParser\\": "src/",
"Tests\\Tmilos\\ScimFilterParser\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com"
}
],
"description": "System for Cross-domain Identity Management SCIM AST filter parser PHP library",
"keywords": [
"SCIM AST",
"SCIM filter parser",
"SCIM parser",
"ast",
"parser",
"scim",
"simplecloud"
],
"support": {
"issues": "https://github.com/tmilos/scim-filter-parser/issues",
"source": "https://github.com/tmilos/scim-filter-parser/tree/master"
},
"time": "2017-01-19T11:17:42+00:00"
},
{
"name": "tmilos/scim-schema",
"version": "0.1",
"source": {
"type": "git",
"url": "https://github.com/tmilos/scim-schema.git",
"reference": "bb871e667b33080b4cd36d7a9b2ac2cdbf796062"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/scim-schema/zipball/bb871e667b33080b4cd36d7a9b2ac2cdbf796062",
"reference": "bb871e667b33080b4cd36d7a9b2ac2cdbf796062",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.6",
"satooshi/php-coveralls": "^1.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Tmilos\\ScimSchema\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com"
}
],
"description": "SCIM schema library",
"support": {
"issues": "https://github.com/tmilos/scim-schema/issues",
"source": "https://github.com/tmilos/scim-schema/tree/master"
},
"time": "2017-11-25T22:18:16+00:00"
},
{
"name": "tmilos/value",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/tmilos/value.git",
"reference": "9e78ad9c026b14cacec1a27552ee0ada9d7d1c06"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tmilos/value/zipball/9e78ad9c026b14cacec1a27552ee0ada9d7d1c06",
"reference": "9e78ad9c026b14cacec1a27552ee0ada9d7d1c06",
"shasum": ""
},
"require": {
"php": ">=5.5.1"
},
"require-dev": {
"moontoast/math": "~1.1",
"phpunit/phpunit": "~4.8",
"ramsey/uuid": "^3.3",
"satooshi/php-coveralls": "~0.6"
},
"type": "library",
"autoload": {
"psr-0": {
"Tmilos\\Value\\": "src/",
"Tmilos\\Value\\Tests\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Milos Tomic",
"email": "tmilos@gmail.com",
"homepage": "https://github.com/tmilos/",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/tmilos/value/issues",
"source": "https://github.com/tmilos/value/tree/master"
},
"time": "2016-06-06T10:22:16+00:00"
},
{
"name": "unicodeveloper/laravel-password",
"version": "1.0.4",
@ -13474,5 +13660,5 @@
"ext-pdo": "*"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.1.0"
}

View file

@ -342,7 +342,6 @@ return [
Laravel\Passport\PassportServiceProvider::class,
Laravel\Tinker\TinkerServiceProvider::class,
Unicodeveloper\DumbPassword\DumbPasswordServiceProvider::class,
Tightenco\Ziggy\ZiggyServiceProvider::class, // Laravel routes in vue
Eduardokum\LaravelMailAutoEmbed\ServiceProvider::class,
/*

5
config/scim.php Normal file
View file

@ -0,0 +1,5 @@
<?php
return [
"publish_routes" => false
];

Binary file not shown.

View file

@ -100,6 +100,7 @@
</template>
<script>
var baseUrl = $('meta[name="baseUrl"]').attr('content');
export default {
props: ['file', 'customFields'],
data() {
@ -266,7 +267,7 @@
}
this.statusType='pending';
this.statusText = "Processing...";
this.$http.post(route('api.imports.importFile', this.file.id), {
this.$http.post(baseUrl + 'api/v1/imports/process/' + this.file.id, {
'import-update': this.options.update,
'send-welcome': this.options.send_welcome,
'import-type': this.options.importType,

View file

@ -2,6 +2,7 @@
<script>
require('blueimp-file-upload');
var baseUrl = $('meta[name="baseUrl"]').attr('content');
export default {
/*
* The component's data.
@ -63,7 +64,7 @@
methods: {
fetchFiles() {
this.$http.get(route('api.imports.index'))
this.$http.get(baseUrl + 'api/v1/imports')
.then( ({data}) => this.files = data, // Success
//Fail
(response) => {
@ -73,7 +74,7 @@
});
},
fetchCustomFields() {
this.$http.get(route('api.customfields.index'))
this.$http.get(baseUrl + 'api/v1/fields')
.then( ({data}) => {
data = data.rows;
data.forEach((item) => {
@ -85,7 +86,7 @@
});
},
deleteFile(file, key) {
this.$http.delete(route('api.imports.destroy', file.id))
this.$http.delete(baseUrl + 'api/v1/imports/' + file.id)
.then(
// Success, remove file from array.
(response) => {

View file

@ -73,8 +73,6 @@
}
};
</script>
<!-- Add laravel routes into javascript Primarily useful for vue.-->
@routes
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<script src="{{ url(asset('js/html5shiv.js')) }}" nonce="{{ csrf_token() }}"></script>

38
routes/scim.php Normal file
View file

@ -0,0 +1,38 @@
<?php
use ArieTimmerman\Laravel\SCIMServer\RouteProvider as SCIMRouteProvider;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| SCIM Routes
|--------------------------------------------------------------------------
|
| These are the routes that we have to explicitly inject from the
| laravel-scim-server project, which gives Snipe-IT SCIM support
|
*/
SCIMRouteProvider::publicRoutes(); // Make sure to load public routes *FIRST*
Route::middleware(['auth:api','authorize:superadmin'])->group(function () {
SCIMRouteProvider::routes(
[
/*
* If we leave public_routes as 'true', the public routes will load *now* and
* be jammed into the same middleware that these private routes are loaded
* with. That's bad, because these routes are *supposed* to be public.
*
* We loaded them a few lines above, *first*, otherwise the various
* fallback routes in the library defined within these *secured* routes
* will "take over" the above routes - and then you will end up losing
* like 4 hours of your life trying to figure out why the public routes
* aren't quite working right. Ask me how I know (BMW, 3/19/2022)
*/
'public_routes' => false
]
);
SCIMRouteProvider::meRoutes();
}); // ->can('superuser');