Merge pull request #10889 from uberbrady/scim_squashed_grok_library

SCIM integration using laravel-scim-server library
This commit is contained in:
snipe 2022-04-05 20:32:16 +01:00 committed by GitHub
commit 7385a0765e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 511 additions and 78 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;
@ -80,6 +81,8 @@ class AppServiceProvider extends ServiceProvider
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
}
}

View file

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

View file

@ -10,6 +10,12 @@
],
"license": "AGPL-3.0-or-later",
"type": "project",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/grokability/laravel-scim-server"
}
],
"require": {
"php": ">=7.4 <8.1",
"ext-curl": "*",
@ -18,6 +24,7 @@
"ext-mbstring": "*",
"ext-pdo": "*",
"alek13/slack": "^2.0",
"arietimmerman/laravel-scim-server": "dev-master",
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-debugbar": "^3.6",
"barryvdh/laravel-dompdf": "^1.0",
@ -61,7 +68,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"
},

330
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": "ad10a039e46761e93b4461865b0e20f0",
"packages": [
{
"name": "alek13/slack",
@ -72,6 +72,69 @@
],
"time": "2021-10-20T22:52:32+00:00"
},
{
"name": "arietimmerman/laravel-scim-server",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://github.com/grokability/laravel-scim-server.git",
"reference": "10be959682d47bb8c78255168262a7cbb7586146"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/grokability/laravel-scim-server/zipball/10be959682d47bb8c78255168262a7cbb7586146",
"reference": "10be959682d47bb8c78255168262a7cbb7586146",
"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"
},
"default-branch": true,
"type": "library",
"extra": {
"laravel": {
"providers": [
"ArieTimmerman\\Laravel\\SCIMServer\\ServiceProvider"
],
"aliases": {
"SCIMServerHelper": "ArieTimmerman\\Laravel\\SCIMServer\\Helper"
}
}
},
"autoload": {
"psr-4": {
"ArieTimmerman\\Laravel\\SCIMServer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ArieTimmerman\\Laravel\\SCIMServer\\Tests\\": "tests"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Arie Timmerman",
"email": "arietimmerman@gmail.com"
}
],
"description": "Laravel Package for creating a SCIM server",
"support": {
"source": "https://github.com/grokability/laravel-scim-server/tree/master"
},
"time": "2022-03-31T19:29:59+00:00"
},
{
"name": "asm89/stack-cors",
"version": "v2.1.1",
@ -11030,72 +11093,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 +11146,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",
@ -13462,7 +13652,9 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"arietimmerman/laravel-scim-server": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@ -13474,5 +13666,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');