Publish new version of scim-server-php

- refactored SCIM 2.0 server core library
- new Domain SCIM resource
- simple JWT implementation
- enhanced documentation
- split out PostfixAdmin SCIM API
This commit is contained in:
Julien Schneider 2022-11-03 15:22:36 +01:00
parent 693b732bd2
commit 10fa524540
64 changed files with 3478 additions and 2446 deletions

3
.htaccess Normal file
View file

@ -0,0 +1,3 @@
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]

View file

@ -33,12 +33,7 @@ lint:
.PHONY: api_test .PHONY: api_test
api_test: api_test:
# If we pass the PFA_API_TEST variable with value 1, we can run the PFA API tests
ifeq ($(PFA_API_TEST),1)
newman run test/postman/scim-opf-pfa.postman_collection.json -e test/postman/scim-env.postman_environment.json
else
newman run test/postman/scim-opf.postman_collection.json -e test/postman/scim-env.postman_environment.json newman run test/postman/scim-opf.postman_collection.json -e test/postman/scim-env.postman_environment.json
endif
.PHONY: unit_test .PHONY: unit_test
unit_test: unit_test:

195
README.md
View file

@ -1,6 +1,33 @@
# scim-server-php # scim-server-php
**scim-server-php** is a PHP library making it easy to implement [SCIM v2.0](https://datatracker.ietf.org/wg/scim/documents/) server endpoints for various systems. This is the Open Provisioning Framework project by audriga which makes use of the [SCIM](http://www.simplecloud.info/) protocol.
---
# Table of Contents
1. [Info](#info)
1. [Related projects](#related-projects)
1. [Capabilities](#capabilities)
1. [Prerequisites](#prerequisites)
1. [Usage](#usage)
1. [Get it as a composer dependency](#get-it-as-a-composer-dependency)
1. [Try out the embedded mock server](#try-out-the-embedded-mock-server)
1. [Enable JWT authentication](#enable-jwt-authentication)
1. [Use scim-server-php for your own project](#use-scim-server-php-for-your-own-project)
1. [SCIM resources](#scim-resources)
1. [SCIM server](#scim-server)
1. [Authentication/Authorization](#authenticationauthorization)
1. [Define your authentication/authorization logic](#define-your-authenticationauthorization-logic)
1. [Define your authentication/authorization middleware](#define-your-authenticationauthorization-middleware)
1. [Add your authentication/authorization middleware to the SCIM server](#add-your-authenticationauthorization-middleware-to-the-scim-server)
1. [Full example](#full-example)
1. [Acknowledgements](#acknowledgements)
---
## Info
**scim-server-php** is a PHP library which makes it easy to implement [SCIM v2.0](https://datatracker.ietf.org/wg/scim/documents/) server endpoints for various systems.
It is built on the following IETF approved RFCs: [RFC7642](https://datatracker.ietf.org/doc/html/rfc7642), [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644) It is built on the following IETF approved RFCs: [RFC7642](https://datatracker.ietf.org/doc/html/rfc7642), [RFC7643](https://datatracker.ietf.org/doc/html/rfc7643) and [RFC7644](https://datatracker.ietf.org/doc/html/rfc7644)
@ -9,74 +36,146 @@ This is a **work in progress** project. It already works pretty well but some fe
The **scim-server-php** project currently includes the following: The **scim-server-php** project currently includes the following:
* A SCIM 2.0 server core library * A SCIM 2.0 server core library
* A [Postfix Admin](https://github.com/postfixadmin/postfixadmin) SCIM API * An integrated Mock SCIM server based on a SQLite database.
**scim-server-php** also comes with an integrated Mock SCIM server based on a SQLite database. ## Related projects
## SCIM 2.0 server core library * A [Postfix Admin](https://github.com/postfixadmin/postfixadmin) SCIM API based on **scim-server-php** is available at https://github.com/audriga/postfixadmin-scim-api
* The [Nextcloud SCIM](https://lab.libreho.st/libre.sh/scim/scimserviceprovider) application provides a SCIM API to [NextCloud](https://nextcloud.com/) and uses **scim-server-php** for its SCIM resource models
## Capabilities
This library provides: This library provides:
* Standard SCIM resources implementations (*Core User*, *Enterprise User* and *Groups*) * Standard SCIM resources implementations (*Core User*, *Enterprise User* and *Groups*)
* Custom SCIM resource *Provisioning User* implementation * Custom SCIM resource *Provisioning User* implementation
* Standard CRUD operation on above SCIM resources * Custom SCIM resource *Domain* implementation
* Standard CRUD operations on above SCIM resources
* A HTTP server handling requests and responses on defined endpoints, based on the [Slim](https://www.slimframework.com/) framework * A HTTP server handling requests and responses on defined endpoints, based on the [Slim](https://www.slimframework.com/) framework
* A very simple JWT implementation * A simple JWT implementation
* When enabled, a JWT token is generated on the `/jwt` endpoint. You **must** therefore protect this endpoint.
* When enabled, this JWT token needs to be provided in all requests using the Bearer schema (`Authorization: Bearer <token>`) * When enabled, this JWT token needs to be provided in all requests using the Bearer schema (`Authorization: Bearer <token>`)
* You can generate a token with the script located at `bin/generate_jwt.php`
* The secret you use *must* be also defined in your `config/config.php` file
* An easily reusable code architecture for implementing SCIM servers * An easily reusable code architecture for implementing SCIM servers
## Postfix Admin SCIM API Note that you can of course use the standard and custom SCIM resources implementations with your own HTTP server if you don't want to use the one provided by **scim-server-php**.
The [Postfix Admin](https://github.com/postfixadmin/postfixadmin) API enables SCIM server capabilities for [Postfix Admin](https://github.com/postfixadmin/postfixadmin). It uses the core library above.
It supports standard GET, POST, PUT and DELETE operations on SCIM *Provisioning User* resources, which are translated in the corresponding operations on the [Postfix Admin](https://github.com/postfixadmin/postfixadmin) mailboxes.
Example (null values removed for readability):
```
$ curl https://my.postfix.admin.url/Users/aaaa@bli.fr -H 'Authorization: Bearer <token>'
{
"schemas":[
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User"
],
"id":"aaaa@bli.fr",
"meta":{
"resourceType":"User",
"created":"2022-05-27 12:45:08",
"location":"https://my.postfix.admin.url/Users/aaaa@bli.fr",
"updated":"2022-06-15 13:07:30"
},
"userName":"aaaa@bli.fr",
"name":{
"formatted":"Aaaa"
},
"displayName":"Aaaa",
"active":"1",
"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User":{
"sizeQuota":51200000
}
}
```
## Prerequisites ## Prerequisites
* **scim-server-php** requires PHP 7.4 * **scim-server-php** requires PHP 7.4
* Dependencies are managed with [composer](https://getcomposer.org/) * Dependencies are managed with [composer](https://getcomposer.org/)
## Installation ## Usage
### Local installation
* Run `make install` to automatically install dependencies
### Configuration ### Get it as a [composer](https://getcomposer.org/) dependency
* To switch from the Mock SCIM server to the Postfix Admin SCIM API, you simply need to adapt the `public/index.php` file (include **one** of the following):
* You can add the following to your `composer.json` file to get it with [composer](https://getcomposer.org/)
``` ```
// Set up system-specific dependencies "repositories": {
$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php'; // include that line if you want to use the integrated mock SCIM server "scim": {
$dependencies = require dirname(__DIR__) . '/src/Dependencies/pfa-dependencies.php'; // include that line if you want to use the Postfix Admin SCIM API "type": "vcs",
"url": "git@bitbucket.org:audriga/scim-server-php.git"
}
},
"require": {
"audriga/scim-server-php": "dev-master"
},
``` ```
* We plan to publish to [packagist](https://packagist.org/) in the future
### Try out the embedded mock server
* To help you use and understand this library, a mock server is provided
* Clone this repository
* Run `make install` to automatically install dependencies and setup a mock database
* Run `make start-server` to start a local mock SCIM server accessible on `localhost:8888`
* Send your first SCIM requests! For example, try out `curl http://localhost:8888/Users`
* It supports all basic CRUD operations on SCIM Core Users and Groups
#### Enable JWT authentication
* A very simple JWT authentication is provided
* Enable it for the embedded mock server by uncommenting the 2 following lines in `public/index.php` and restart it
```
$scimServerPhpAuthMiddleware = 'AuthMiddleware';
$scimServer->setMiddleware(array($scimServerPhpAuthMiddleware));
```
* You will now need to send a valid JWT token with all your requests to the mock server
* A JWT token will be considered as valid by the mock server if its secret is identical to the secret set in the `jwt` section of `config/config[.default].php`
* To generate a token, use the script located at `bin/generate_jwt.php`
* Note that this script generates a JWT token including a `user` claim set by the `--user` parameter. You can use any value here in the mock server case.
### Use scim-server-php for your own project
#### SCIM resources
* You can directly reuse the SCIM resources implementation from the `src/Models/SCIM/` folder in any PHP project
* Here are the provided resources implementations
* `src/Models/SCIM/Standard/Users/CoreUser.php` implements the Core User resource from the SCIM standard
* `src/Models/SCIM/Standard/Users/EnterpriseUser.php` implements the Enterprise User extension from the SCIM standard
* `src/Models/SCIM/Standard/Groups/CoreGroup.php` implements the Core Group resource from the SCIM standard
* `src/Models/SCIM/Custom/Domains/Domain.php` implements the custom Domain resource
* `src/Models/SCIM/Custom/Users/ProvisioningUser.php` implements the custom Provisioning User extension of the Core User
#### SCIM server
* You can use **scim-server-php** to easily create a full-fledged SCIM server for your own data source
* **scim-server-php** uses the [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html) and the [Adapter Pattern](https://en.wikipedia.org/wiki/Adapter_pattern) in order to be as flexible and portable to different systems for provisioning as possible
* You can use the embedded mock server implementation as an example ;)
* Concretelly, you will need to implement the following for each resource type of your data source
* `Model` classes representing your resources
* See e.g. `src/Models/Mock/MockUsers`
* `DataAccess` classes defining how to access your data source
* See e.g. `src/DataAccess/Users/MockUserDataAccess.php`
* `Adapter` classes, extending `AbstractAdapter` and defining how to convert your resources to/from SCIM resources
* See e.g. `src/Adapters/Users/MockUserAdapter.php`
* `Repository` classes, extending `Opf\Repositories\Repository` and defining the operations available on your resources
* See e.g. `src/Repositories/Users/MockUsersRepository.php`
* If you want to define new SCIM resources, you will also need to implement new `Controllers` (see `src/Controllers`) and SCIM `Model`s (see `src/Models/SCIM`)
* **scim-server-php** uses [Dependency Injection Container](https://php-di.org/) internally
* Create a `dependencies` file reusing the pattern of `src/Dependencies/mock-dependencies.php`
* The "Auth middleware" and "Authenticators" sections are explained in the [Authentication/Authorization](#authenticationauthorization) section bellow
* Your `Repository` classes will get the corresponding `DataAccess` and `Adapter` classes through the **scim-server-php** container
* Instantiate a `ScimServer` and feed it with your `dependencies` file as shown in `public/index.php`
* The "Authentication Middleware" section is explained in the [Authentication/Authorization](#authenticationauthorization) section bellow
### Authentication/Authorization
#### Define your authentication/authorization logic
* Authentication is mostly delegated to the system using **scim-server-php**
* A basic JWT based authentication implementation is provided as an example in `src/Util/Authentication/SimpleBearerAuthenticator`
* Define your own `Authenticator` class(es) by implementing the `AuthenticatorInterface` available in `Util/Authentication`
* A script generating a JWT token containing a single `user` claim is provided in `bin/generate_jwt.php`
* Authorization is delegated to the system using **scim-server-php**
#### Define your authentication/authorization middleware
* The **scim-server-php** HTTP server is based on the [Slim](https://www.slimframework.com/) framework and reuses its [Middleware](https://www.slimframework.com/docs/v4/concepts/middleware.html) concept
* Authentication and authorization should therefore be implemented as "Middleware(s)"
* This means implementing the `MiddlewareInterface`
* The authentication middleware should then delegate the actual authentication process to your `Authenticator`
* The authorization implementation is up to you
* You can either integrate it in the `Authenticator` (and so, in the authentication middleware)
* Or you can implement an independent authentication middleware
* You can use `src/Middleware/SimpleAuthMiddleware` as an example
#### Add your authentication/authorization middleware to the SCIM server
* Add your middleware to your dependencies file
* You can use `src/Dependencies/mock-dependencies.php` as an example
* Note that the mock `SimpleAuthMiddleware` also uses the **scim-server-php** container to gets the authenticator to use
* Hence `src/Dependencies/mock-dependencies.php` defines a `'BearerAuthenticator'` which is then used in `SimpleAuthMiddleware`
### Full example
* We advise to use https://github.com/audriga/postfixadmin-scim-api as a full **scim-server-php** implementation example
## Acknowledgements ## Acknowledgements
This software is part of the [Open Provisioning Framework](https://www.audriga.com/en/User_provisioning/Open_Provisioning_Framework) project that has received funding from the European Union's Horizon 2020 research and innovation program under grant agreement No. 871498. This software is part of the [Open Provisioning Framework](https://www.audriga.com/en/User_provisioning/Open_Provisioning_Framework) project that has received funding from the European Union's Horizon 2020 research and innovation program under grant agreement No. 871498.

72
bin/generate_jwt.php Executable file
View file

@ -0,0 +1,72 @@
#!/usr/bin/env php
<?php
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
} else {
require __DIR__ . '/../../../../vendor/autoload.php';
}
use Firebase\JWT\JWT;
/**
* A function that prints the usage help message to standard output
*/
function showUsage()
{
fwrite(
STDOUT,
"Usage:
generate_jwt.php -u=<username> -s=<secret>
generate_jwt.php --username=<username> --secret=<secret>
generate_jwt.php (-h | --help)\n"
);
}
/**
* Generate a JWT for a given user
*
* @param string $username The username of the user we generate a JWT for
* @param string $secret The JWT secret signing key
* @return string The JWT of the user
*/
function generateJwt(string $username, string $secret): string
{
$jwtPayload = array(
"user" => $username
);
return JWT::encode($jwtPayload, $secret, "HS256");
}
// Specify the CLI options, passed to getopt()
$shortOptions = "hu:s:";
$longOptions = ["help", "username:", "secret:"];
// Obtain the CLI args, passed to the script via getopt()
$cliOptions = getopt($shortOptions, $longOptions);
// If there was some issue with the CLI args, we show the help message
if ($cliOptions === false) {
showUsage();
exit(1);
}
// We check if a username was provided
if (
(isset($cliOptions["u"]) || isset($cliOptions["username"]))
&& (isset($cliOptions["s"]) || isset($cliOptions["secret"]))
) {
$username = isset($cliOptions["u"]) ? $cliOptions["u"] : $cliOptions["username"];
$secret = isset($cliOptions["s"]) ? $cliOptions["s"] : $cliOptions["secret"];
} else {
// If no username or secret was provided, we let the user know
fwrite(STDERR, "A username and a secret JWT key must be provided\n");
showUsage();
exit(1);
}
$jwt = generateJwt($username, $secret);
fwrite(STDOUT, "$jwt\n");
exit(0);

View file

@ -1,6 +1,6 @@
{ {
"name": "audriga/scim-opf", "name": "audriga/scim-server-php",
"description": "An open provisioning framework using the SCIM protocol", "description": "An open library for SCIM servers implementation",
"type": "library", "type": "library",
"require": { "require": {
"slim/slim": "^4.10", "slim/slim": "^4.10",
@ -20,8 +20,8 @@
}, },
"authors": [ "authors": [
{ {
"name": "Stanimir Bozhilov", "name": "audriga",
"email": "stanimir@audriga.com" "email": "opensource@audriga.com"
} }
], ],
"require-dev": { "require-dev": {

View file

@ -0,0 +1,88 @@
{
"id": "urn:ietf:params:scim:schema:audriga:core:2.0:Domain",
"name": "Domain",
"description": "Domain",
"attributes": [
{
"name": "domainName",
"type": "string",
"multiValued": false,
"description": "The name of the domain. REQUIRED.",
"required": true,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "server"
},
{
"name": "description",
"type": "string",
"multiValued": false,
"description": "A description of the domain. OPTIONAL.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "maxAliases",
"type": "int",
"multiValued": false,
"description": "The maximum number of aliases of the domain. OPTIONAL.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "maxMailboxes",
"type": "int",
"multiValued": false,
"description": "The maximum number of mailboxes the domain can have. OPTIONAL.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "maxQuota",
"type": "int",
"multiValued": false,
"description": "The maximum quota, allowed for mailboxes of the domain (in MB). OPTIONAL.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "usedQuota",
"type": "int",
"multiValued": false,
"description": "The currently used quota by the mailboxes of the domain (in MB). OPTIONAL.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "active",
"type": "bool",
"multiValued": false,
"description": "A flag indicating whether the domain is currently active. REQUIRED.",
"required": true,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
}
],
"meta": {
"resourceType": "Schema",
"location": "/v2/Schemas/urn:ietf:params:scim:schema:audriga:core:2.0:Domain"
}
}

View file

@ -2,23 +2,23 @@
return [ return [
'isInProduction' => false, // Set to true when deploying in production 'isInProduction' => false, // Set to true when deploying in production
'basePath' => null, // If you want to specify a base path for the Slim app, add it here (e.g., '/test/scim') 'basePath' => '', // If you want to specify a base path for the Slim app, add it here (e.g., '/test/scim')
'supportedResourceTypes' => ['User', 'Group'], // Specify all the supported SCIM ResourceTypes by their names here 'supportedResourceTypes' => ['User', 'Group'], // Specify all the supported SCIM ResourceTypes by their names here
// SQLite DB settings // SQLite DB settings
'db' => [ 'db' => [
'driver' => 'sqlite', // Type of DB 'driver' => 'sqlite', // Type of DB
'database' => 'db/scim-mock.sqlite' // DB name 'databaseFile' => 'db/scim-mock.sqlite' // DB name
], ],
// PFA MySQL DB settings // MySQL DB settings
//'db' => [ //'db' => [
// 'driver' => 'sqlite', // Type of DB // 'driver' => 'mysql', // Type of DB
// 'host' => 'localhost', // DB host // 'host' => 'localhost', // DB host
// 'port' => '3306', // Port on DB host // 'port' => '3306', // Port on DB host
// 'database' => 'postfix', // DB name // 'database' => 'db_name', // DB name
// 'user' => 'postfix', // DB user // 'user' => 'db_user', // DB user
// 'password' => 'postfix123' // DB user's password // 'password' => 'db_password' // DB user's password
//], //],
// Monolog settings // Monolog settings
@ -30,7 +30,6 @@ return [
// Bearer token settings // Bearer token settings
'jwt' => [ 'jwt' => [
'secure' => false,
'secret' => 'secret' 'secret' => 'secret'
] ]
]; ];

4
public/.htaccess Normal file
View file

@ -0,0 +1,4 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

View file

@ -1,78 +1,26 @@
<?php <?php
use DI\ContainerBuilder; use Opf\ScimServer;
use Opf\Handlers\HttpErrorHandler;
use Opf\Util\Util;
use Slim\Factory\AppFactory;
require dirname(__DIR__) . '/vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
session_start();
// Instantiate the PHP-DI ContainerBuilder // Obtain the root of the project
$containerBuilder = new ContainerBuilder(); $scimServerPhpRoot = dirname(__DIR__);
$config = Util::getConfigFile(); // Create a new ScimServer instance and give it the project root
if ($config['isInProduction']) { $scimServer = new ScimServer($scimServerPhpRoot);
$containerBuilder->enableCompilation(__DIR__ . '/../var/cache');
}
// Set up a few Slim-related settings // Take the config file path and pass it to the scimServer instance
$settings = [ $configFilePath = __DIR__ . '/../config/config.php';
'settings' => [ $scimServer->setConfig($configFilePath);
'determineRouteBeforeAppMiddleware' => false,
'displayErrorDetails' => true, // set to false in production
'addContentLengthHeader' => false, // Allow the web server to send the content-length header
]
];
$containerBuilder->addDefinitions($settings);
// Set up common dependencies // Obtain custom dependencies (if any) and pass them to the scimServer instance
$dependencies = require dirname(__DIR__) . '/src/Dependencies/dependencies.php'; $dependencies = require __DIR__ . '/../src/Dependencies/mock-dependencies.php';
$dependencies($containerBuilder); $scimServer->setDependencies($dependencies);
// Set up system-specific dependencies // Set the Authentication Middleware configured in the dependencies files above to the scimServer instance
$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php'; //$scimServerPhpAuthMiddleware = 'AuthMiddleware';
$dependencies($containerBuilder); //$scimServer->setMiddleware(array($scimServerPhpAuthMiddleware));
// Build PHP-DI Container instance // Start the scimServer
$container = $containerBuilder->build(); $scimServer->run();
// Instantiate the app
AppFactory::setContainer($container);
$app = AppFactory::create();
$callableResolver = $app->getCallableResolver();
$responseFactory = $app->getResponseFactory();
// Set our app's base path if it's configured
if (isset($config['basePath']) && !empty($config['basePath'])) {
$app->setBasePath($config['basePath']);
}
// Set up the ORM
$eloquent = require dirname(__DIR__) . '/src/eloquent.php';
$eloquent($app);
// Register routes
$routes = require dirname(__DIR__) . '/src/routes.php';
$routes($app);
// Add Routing Middleware
$app->addRoutingMiddleware();
$app->addBodyParsingMiddleware();
// Add JWT middleware
$app->addMiddleware($container->get(JwtAuthentication::class));
// Instantiate our custom Http error handler that we need further down below
$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
// Add error middleware
$errorMiddleware = $app->addErrorMiddleware(
$config['isInProduction'] ? false : true,
true,
true
);
$errorMiddleware->setDefaultErrorHandler($errorHandler);
// Run app
$app->run();

View file

@ -3,76 +3,69 @@
namespace Opf\Adapters\Groups; namespace Opf\Adapters\Groups;
use Opf\Adapters\AbstractAdapter; use Opf\Adapters\AbstractAdapter;
use Opf\DataAccess\Groups\MockGroupDataAccess; use Opf\Models\Mock\MockGroup;
use Opf\Models\SCIM\Standard\Groups\CoreGroup;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
class MockGroupAdapter extends AbstractAdapter class MockGroupAdapter extends AbstractAdapter
{ {
/** @var Opf\Models\MockGroup $group */ public function getCoreGroup(?MockGroup $mockGroup): ?CoreGroup
private $group;
public function getGroup()
{ {
return $this->group; if (!isset($mockGroup)) {
} return null;
public function setGroup(MockGroupDataAccess $group)
{
$this->group = $group;
}
public function getId()
{
if (isset($this->group->id) && !empty($this->group->id)) {
return $this->group->id;
} }
$coreGroup = new CoreGroup();
$coreGroup->setId($mockGroup->getId());
$coreGroupMeta = new Meta();
$coreGroupMeta->setResourceType("Group");
$coreGroupMeta->setCreated($mockGroup->getCreatedAt());
$coreGroupMeta->setLastModified($mockGroup->getUpdatedAt());
$coreGroup->setMeta($coreGroupMeta);
$coreGroup->setDisplayName($mockGroup->getDisplayName());
if ($mockGroup->getMembers() !== null && !empty($mockGroup->getMembers())) {
$coreGroupMembers = [];
foreach ($mockGroup->getMembers() as $mockGroupMember) {
$coreGroupMember = new MultiValuedAttribute();
$coreGroupMember->setValue($mockGroupMember);
$coreGroupMembers[] = $coreGroupMember;
}
$coreGroup->setMembers($coreGroupMembers);
}
return $coreGroup;
} }
public function setId($id) public function getMockGroup(?CoreGroup $coreGroup): ?MockGroup
{ {
if (isset($id) && !empty($id)) { if (!isset($coreGroup)) {
$this->group->id = $id; return null;
} }
}
public function getCreatedAt() $mockGroup = new MockGroup();
{ $mockGroup->setId($coreGroup->getId());
if (isset($this->group->created_at) && !empty($this->group->created_at)) {
return $this->group->created_at;
}
}
public function setCreatedAt($createdAt) if ($coreGroup->getMeta() !== null) {
{ $mockGroup->setCreatedAt($coreGroup->getMeta()->getCreated());
if (isset($createdAt) && !empty($createdAt)) { $mockGroup->setUpdatedAt($coreGroup->getMeta()->getLastModified());
$this->group->created_at = $createdAt;
} }
}
public function getDisplayName() $mockGroup->setDisplayName($coreGroup->getDisplayName());
{
if (isset($this->group->displayName) && !empty($this->group->displayName)) {
return $this->group->displayName;
}
}
public function setDisplayName($displayName) if ($coreGroup->getMembers() !== null && !empty($coreGroup->getMembers())) {
{ $mockGroupMembers = [];
if (isset($displayName) && !empty($displayName)) { foreach ($coreGroup->getMembers() as $coreGroupMember) {
$this->group->displayName = $displayName; $mockGroupMembers[] = $coreGroupMember->getValue();
} }
}
public function getMembers() $mockGroup->setMembers($mockGroupMembers);
{
if (isset($this->group->members) && !empty($this->group->members)) {
return $this->group->members;
} }
}
public function setMembers($members) return $mockGroup;
{
if (isset($members) && !empty($members)) {
$this->group->members = $members;
}
} }
} }

View file

@ -3,104 +3,54 @@
namespace Opf\Adapters\Users; namespace Opf\Adapters\Users;
use Opf\Adapters\AbstractAdapter; use Opf\Adapters\AbstractAdapter;
use Opf\DataAccess\Users\MockUserDataAccess; use Opf\Models\Mock\MockUser;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\Users\CoreUser;
class MockUserAdapter extends AbstractAdapter class MockUserAdapter extends AbstractAdapter
{ {
/** @var Opf\Models\MockUser $user */ public function getCoreUser(?MockUser $mockUser): ?CoreUser
private $user;
public function getUser()
{ {
return $this->user; if (!isset($mockUser)) {
} return null;
public function setUser(MockUserDataAccess $user)
{
$this->user = $user;
}
public function getId()
{
if (isset($this->user->id) && !empty($this->user->id)) {
return $this->user->id;
} }
$coreUser = new CoreUser();
$coreUser->setId($mockUser->getId());
$coreUser->setExternalId($mockUser->getExternalId());
$coreUserMeta = new Meta();
$coreUserMeta->setResourceType("User");
$coreUserMeta->setCreated($mockUser->getCreatedAt());
$coreUserMeta->setLastModified($mockUser->getUpdatedAt());
$coreUser->setMeta($coreUserMeta);
$coreUser->setUserName($mockUser->getUserName());
$coreUser->setActive(boolval($mockUser->getActive()));
$coreUser->setProfileUrl($mockUser->getProfileUrl());
return $coreUser;
} }
public function setId($id) public function getMockUser(?CoreUser $coreUser): ?MockUser
{ {
if (isset($id) && !empty($id)) { if (!isset($coreUser)) {
$this->user->id = $id; return null;
} }
}
public function getUserName() $mockUser = new MockUser();
{ $mockUser->setId($coreUser->getId());
if (isset($this->user->userName) && !empty($this->user->userName)) {
return $this->user->userName;
}
}
public function setUserName($userName) if ($coreUser->getMeta() !== null) {
{ $mockUser->setCreatedAt($coreUser->getMeta()->getCreated());
if (isset($userName) && !empty($userName)) { $mockUser->setUpdatedAt($coreUser->getMeta()->getLastModified());
$this->user->userName = $userName;
} }
}
public function getCreatedAt() $mockUser->setUserName($coreUser->getUserName());
{ $mockUser->setActive(boolval($coreUser->getActive()));
if (isset($this->user->created_at) && !empty($this->user->created_at)) { $mockUser->setExternalId($coreUser->getExternalId());
return $this->user->created_at; $mockUser->setProfileUrl($coreUser->getProfileUrl());
}
}
public function setCreatedAt($createdAt) return $mockUser;
{
if (isset($createdAt) && !empty($createdAt)) {
$this->user->created_at = $createdAt;
}
}
public function getActive()
{
if (isset($this->user->active) && !empty($this->user->active)) {
return boolval($this->user->active);
}
}
public function setActive($active)
{
if (isset($active) && !empty($active)) {
$this->user->active = $active;
}
}
public function getExternalId()
{
if (isset($this->user->externalId) && !empty($this->user->externalId)) {
return $this->user->externalId;
}
}
public function setExternalId($externalId)
{
if (isset($externalId) && !empty($externalId)) {
$this->user->externalId = $externalId;
}
}
public function getProfileUrl()
{
if (isset($this->user->profileUrl) && !empty($this->user->profileUrl)) {
return $this->user->profileUrl;
}
}
public function setProfileUrl($profileUrl)
{
if (isset($profileUrl) && !empty($profileUrl)) {
$this->user->profileUrl = $profileUrl;
}
} }
} }

View file

@ -1,100 +0,0 @@
<?php
namespace Opf\Adapters\Users;
use Exception;
use Opf\Adapters\AbstractAdapter;
use Opf\Models\SCIM\Custom\Users\ProvisioningUser;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
use Opf\Models\PFA\PfaUser;
use Opf\Models\SCIM\Standard\Users\Name;
use Opf\Util\Util;
class PfaUserAdapter extends AbstractAdapter
{
public function getProvisioningUser(?PfaUser $pfaUser): ?ProvisioningUser
{
if ($pfaUser === null) {
return null;
}
$provisioningUser = new ProvisioningUser();
$provisioningUser->setId($pfaUser->getUserName());
$provisioningUser->setUserName($pfaUser->getUserName());
$scimUserMeta = new Meta();
$scimUserMeta->setResourceType("User");
$scimUserMeta->setCreated($pfaUser->getCreated());
$scimUserMeta->setLastModified($pfaUser->getModified());
$provisioningUser->setMeta($scimUserMeta);
$name = new Name();
$name->setFormatted($pfaUser->getName());
$provisioningUser->setName($name);
$provisioningUser->setDisplayName($pfaUser->getName());
$provisioningUser->setActive($pfaUser->getActive());
$scimUserPhoneNumbers = new MultiValuedAttribute();
$scimUserPhoneNumbers->setValue($pfaUser->getPhone());
$provisioningUser->setPhoneNumbers(array($scimUserPhoneNumbers));
$provisioningUser->setSizeQuota($pfaUser->getQuota());
return $provisioningUser;
}
public function getPfaUser(?ProvisioningUser $provisioningUser): ?PfaUser
{
if ($provisioningUser === null) {
return null;
}
$pfaUser = new PfaUser();
if (filter_var($provisioningUser->getUserName(), FILTER_VALIDATE_EMAIL)) {
$pfaUser->setUserName($provisioningUser->getUserName());
} elseif (filter_var($provisioningUser->getId(), FILTER_VALIDATE_EMAIL)) {
$pfaUser->setUserName($provisioningUser->getId());
} else {
$pfaUser->setUserName($provisioningUser->getEmails()[0]->getValue());
}
$pfaUser->setPassword($provisioningUser->getPassword());
if ($provisioningUser->getName() !== null) {
if (!empty($provisioningUser->getName()->getFormatted())) {
$pfaUser->setName($provisioningUser->getName()->getFormatted());
} else {
$formattedName = "";
if (!empty($provisioningUser->getName()->getHonorificPrefix())) {
$formattedName = $formattedName . $provisioningUser->getName()->getHonorificPrefix() . " ";
}
if (!empty($provisioningUser->getName()->getGivenName())) {
$formattedName = $formattedName . $provisioningUser->getName()->getGivenName() . " ";
}
if (!empty($provisioningUser->getName()->getFamilyName())) {
$formattedName = $formattedName . $provisioningUser->getName()->getFamilyName() . " ";
}
if (!empty($provisioningUser->getName()->getHonorificSuffix())) {
$formattedName = $formattedName . $provisioningUser->getName()->getHonorificSuffix();
}
$formattedName = trim($formattedName);
if (!empty($formattedName)) {
$pfaUser->setName($formattedName);
} else {
$pfaUser->setName($provisioningUser->getDisplayName());
}
}
} else {
$pfaUser->setName($provisioningUser->getDisplayName());
}
$pfaUser->setMaildir(Util::getDomainFromEmail($pfaUser->getUserName()) . "/"
. Util::getLocalPartFromEmail($pfaUser->getUserName()) . "/");
// We default PFA quota to 0 (unlimited) if not set
$pfaUser->setQuota($provisioningUser->getSizeQuota());
$pfaUser->setLocalPart(Util::getLocalPartFromEmail($pfaUser->getUserName()));
$pfaUser->setDomain(Util::getDomainFromEmail($pfaUser->getUserName()));
$pfaUser->setActive($provisioningUser->getActive());
if (isset($provisioningUser->getPhoneNumbers()[0])) {
$pfaUser->setPhone($provisioningUser->getPhoneNumbers()[0]->getValue());
}
return $pfaUser;
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Opf\Controllers\Domains;
use Exception;
use Opf\Controllers\Controller;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class CreateDomainAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('DomainsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("CREATE Domain");
$this->logger->info($request->getBody());
$uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
try {
$domain = $this->repository->create($request->getParsedBody());
if (isset($domain) && !empty($domain)) {
$this->logger->info("Created domain / domain=" . $domain->getId());
$scimDomain = $domain->toSCIM(false, $baseUrl);
$responseBody = json_encode($scimDomain, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 201);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
} else {
$this->logger->error("Error creating domain");
$errorResponseBody = json_encode(
["Errors" => ["description" => "Error creating domain", "code" => 400]]
);
$response = new Response($status = 400);
$response->getBody()->write($errorResponseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
} catch (Exception $e) {
$this->logger->error("Error creating domain: " . $e->getMessage());
$errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]);
$response = new Response($status = 400);
$response->getBody()->write($errorResponseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Opf\Controllers\Domains;
use Opf\Controllers\Controller;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class DeleteDomainAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('DomainsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("DELETE Domain");
$id = $request->getAttribute('id');
$this->logger->info("ID: " . $id);
$deleteRes = $this->repository->delete($id);
if (!$deleteRes) {
$this->logger->info("Not found");
return $response->withStatus(404);
}
$this->logger->info("Domain deleted");
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response->withStatus(204);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Opf\Controllers\Domains;
use Opf\Controllers\Controller;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class GetDomainAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('DomainsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("GET Domain");
$uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$id = $request->getAttribute('id');
$this->logger->info("ID: " . $id);
$domain = $this->repository->getOneById($id);
if (!isset($domain) || empty($domain)) {
$this->logger->info("Not found");
return $response->withStatus(404);
}
$scimDomain = $domain->toSCIM(false, $baseUrl);
$responseBody = json_encode($scimDomain, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Opf\Controllers\Domains;
use Opf\Controllers\Controller;
use Opf\Models\SCIM\Standard\CoreCollection;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class ListDomainsAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('DomainsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("GET Domains");
$filter = '';
if (!empty($request->getQueryParams()['filter'])) {
$this->logger->info("Filter --> " . $request->getQueryParams()['filter']);
$filter = $request->getQueryParams()['filter'];
}
$uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$domains = [];
$domains = $this->repository->getAll($filter);
$scimDomains = [];
if (!empty($domains)) {
foreach ($domains as $domain) {
$scimDomains[] = $domain->toSCIM(false, $baseUrl);
}
}
$scimDomainCollection = (new CoreCollection($scimDomains))->toSCIM(false);
$responseBody = json_encode($scimDomainCollection, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Opf\Controllers\Domains;
use Exception;
use Opf\Controllers\Controller;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class UpdateDomainAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('DomainsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("UPDATE Domain");
$this->logger->info($request->getBody());
$uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$id = $request->getAttribute('id');
$this->logger->info("ID: " . $id);
// Try to find a domain with the supplied ID
// and if it doesn't exist, return a 404
$domain = $this->repository->getOneById($id);
if (!isset($domain) || empty($domain)) {
$this->logger->info("Not found");
return $response->withStatus(404);
}
try {
$domain = $this->repository->update($id, $request->getParsedBody());
if (isset($domain) && !empty($domain)) {
$scimDomain = $domain->toSCIM(false, $baseUrl);
$responseBody = json_encode($scimDomain, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
} else {
$this->logger->error("Error updating domain");
$errorResponseBody = json_encode(
["Errors" => ["decription" => "Error updating domain", "code" => 400]]
);
$response = new Response($status = 400);
$response->getBody()->write($errorResponseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
} catch (Exception $e) {
$this->logger->error("Error updating domain: " . $e->getMessage());
$errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]);
$response = new Response($status = 400);
$response->getBody()->write($errorResponseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}
}

View file

@ -20,12 +20,16 @@ final class ListGroupsAction extends Controller
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{ {
$this->logger->info("GET Groups"); $this->logger->info("GET Groups");
$filter = '';
if (!empty($request->getQueryParams()['filter'])) {
$this->logger->info("Filter --> " . $request->getQueryParams()['filter']);
$filter = $request->getQueryParams()['filter'];
}
$uri = $request->getUri(); $uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$groups = []; $groups = [];
$groups = $this->repository->getAll(); $groups = $this->repository->getAll($filter);
$scimGroups = []; $scimGroups = [];
if (!empty($groups)) { if (!empty($groups)) {

View file

@ -1,28 +0,0 @@
<?php
namespace Opf\Controllers\JWT;
use Firebase\JWT\JWT;
use Opf\Controllers\Controller;
use Opf\Util\Util;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
class GenerateJWTAction extends Controller
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
// TODO: It'd be good practice to maybe protect this endpoint with basic auth
$config = Util::getConfigFile();
$settings = $config['jwt'];
$token = JWT::encode(["user" => "admin", "password" => "admin"], $settings['secret']);
$responseBody = json_encode(['Bearer' => $token], JSON_UNESCAPED_SLASHES);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/json');
return $response;
}
}

View file

@ -21,51 +21,7 @@ final class ListResourceTypesAction extends Controller
$uri = $request->getUri(); $uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
// Check which resource types are supported via the config file and in this method further down below $scimResourceTypes = Util::getResourceTypes($baseUrl);
// make sure to only return those that are indeed supported
$config = Util::getConfigFile();
$supportedResourceTypes = $config['supportedResourceTypes'];
$scimResourceTypes = [];
if (in_array('User', $supportedResourceTypes)) {
$userResourceType = new CoreResourceType();
$userResourceType->setId("User");
$userResourceType->setName("User");
$userResourceType->setEndpoint("/Users");
$userResourceType->setDescription("User Account");
$userResourceType->setSchema(Util::USER_SCHEMA);
if (in_array('EnterpriseUser', $supportedResourceTypes)) {
$enterpriseUserSchemaExtension = new CoreSchemaExtension();
$enterpriseUserSchemaExtension->setSchema(Util::ENTERPRISE_USER_SCHEMA);
$enterpriseUserSchemaExtension->setRequired(true);
$userResourceType->setSchemaExtensions(array($enterpriseUserSchemaExtension));
}
if (in_array('ProvisioningUser', $supportedResourceTypes)) {
$provisioningUserSchemaExtension = new CoreSchemaExtension();
$provisioningUserSchemaExtension->setSchema(Util::PROVISIONING_USER_SCHEMA);
$provisioningUserSchemaExtension->setRequired(true);
$userResourceType->setSchemaExtensions(array($provisioningUserSchemaExtension));
}
$scimResourceTypes[] = $userResourceType->toSCIM(false, $baseUrl);
}
if (in_array('Group', $supportedResourceTypes)) {
$groupResourceType = new CoreResourceType();
$groupResourceType->setId("Group");
$groupResourceType->setName("Group");
$groupResourceType->setEndpoint("/Groups");
$groupResourceType->setDescription("Group");
$groupResourceType->setSchema("urn:ietf:params:scim:schemas:core:2.0:Group");
$groupResourceType->setSchemaExtensions([]);
$scimResourceTypes[] = $groupResourceType->toSCIM(false, $baseUrl);
}
$scimResourceTypeCollection = (new CoreCollection($scimResourceTypes))->toSCIM(false); $scimResourceTypeCollection = (new CoreCollection($scimResourceTypes))->toSCIM(false);
$responseBody = json_encode($scimResourceTypeCollection, JSON_UNESCAPED_SLASHES); $responseBody = json_encode($scimResourceTypeCollection, JSON_UNESCAPED_SLASHES);

View file

@ -15,20 +15,10 @@ final class ListSchemasAction extends Controller
{ {
$this->logger->info("GET Schemas"); $this->logger->info("GET Schemas");
$config = Util::getConfigFile(); $scimSchemas = Util::getSchemas();
$supportedSchemas = $config['supportedResourceTypes'];
$mandatorySchemas = ['Schema', 'ResourceType'];
$scimSchemas = []; // If there were no schemas found, return 404
if (is_null($scimSchemas)) {
// We store the schemas that the SCIM server supports in separate JSON files
// That's why we try to read them here and add them to $scimSchemas, which
// in turn is then put into the SCIM response body
$pathToSchemasDir = dirname(__DIR__, 3) . '/config/Schema';
$schemaFiles = scandir($pathToSchemasDir, SCANDIR_SORT_NONE);
// If scandir() failed (i.e., it returned false), then return 404 (is this spec-compliant?)
if ($schemaFiles === false) {
$this->logger->info("No Schemas found"); $this->logger->info("No Schemas found");
$response = new Response($status = 404); $response = new Response($status = 404);
$response = $response->withHeader('Content-Type', 'application/scim+json'); $response = $response->withHeader('Content-Type', 'application/scim+json');
@ -36,18 +26,6 @@ final class ListSchemasAction extends Controller
return $response; return $response;
} }
foreach ($schemaFiles as $schemaFile) {
if (!in_array($schemaFile, array('.', '..'))) {
$scimSchemaJsonDecoded = json_decode(file_get_contents($pathToSchemasDir . '/' . $schemaFile), true);
// Only return schemas that are either mandatory (like the 'Schema' and 'ResourceType' ones)
// or supported by the server
if (in_array($scimSchemaJsonDecoded['name'], array_merge($supportedSchemas, $mandatorySchemas))) {
$scimSchemas[] = $scimSchemaJsonDecoded;
}
}
}
$scimSchemasCollection = (new CoreCollection($scimSchemas))->toSCIM(false); $scimSchemasCollection = (new CoreCollection($scimSchemas))->toSCIM(false);
$responseBody = json_encode($scimSchemasCollection, JSON_UNESCAPED_SLASHES); $responseBody = json_encode($scimSchemasCollection, JSON_UNESCAPED_SLASHES);

View file

@ -3,6 +3,7 @@
namespace Opf\Controllers\ServiceProviders; namespace Opf\Controllers\ServiceProviders;
use Opf\Controllers\Controller; use Opf\Controllers\Controller;
use Opf\Util\Util;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response; use Slim\Psr7\Response;
@ -13,12 +14,9 @@ final class ListServiceProviderConfigurationsAction extends Controller
{ {
$this->logger->info("GET ServiceProviderConfigurations"); $this->logger->info("GET ServiceProviderConfigurations");
$pathToServiceProviderConfigurationFile = $scimServiceProviderConfiguration = Util::getServiceProviderConfig();
dirname(__DIR__, 3) . '/config/ServiceProviderConfig/serviceProviderConfig.json';
$scimServiceProviderConfigurationFile = file_get_contents($pathToServiceProviderConfigurationFile); if (is_null($scimServiceProviderConfiguration)) {
if ($scimServiceProviderConfigurationFile === false) {
$this->logger->info("No ServiceProviderConfiguration found"); $this->logger->info("No ServiceProviderConfiguration found");
$response = new Response($status = 404); $response = new Response($status = 404);
$response = $response->withHeader('Content-Type', 'application/scim+json'); $response = $response->withHeader('Content-Type', 'application/scim+json');
@ -26,7 +24,7 @@ final class ListServiceProviderConfigurationsAction extends Controller
return $response; return $response;
} }
$responseBody = $scimServiceProviderConfigurationFile; $responseBody = $scimServiceProviderConfiguration;
$this->logger->info($responseBody); $this->logger->info($responseBody);
$response = new Response($status = 200); $response = new Response($status = 200);
$response->getBody()->write($responseBody); $response->getBody()->write($responseBody);

View file

@ -21,25 +21,17 @@ final class ListUsersAction extends Controller
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{ {
$this->logger->info("GET Users"); $this->logger->info("GET Users");
$filter = '';
if (!empty($request->getQueryParams()['filter'])) { if (!empty($request->getQueryParams()['filter'])) {
$this->logger->info("Filter --> " . $request->getQueryParams()['filter']); $this->logger->info("Filter --> " . $request->getQueryParams()['filter']);
$filter = $request->getQueryParams()['filter'];
} }
$uri = $request->getUri(); $uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$userName = null; $userName = null;
$users = []; $users = [];
if (!empty($request->getQueryParams('filter'))) { $users = $this->repository->getAll($filter);
$userName = Util::getUserNameFromFilter($request->getQueryParams()['filter']);
if (!empty($userName)) {
$user = $this->repository->getOneByUserName();
if (isset($user) && !empty($user)) {
$users[] = $user;
}
}
} else {
$users = $this->repository->getAll();
}
$scimUsers = []; $scimUsers = [];
if (!empty($users)) { if (!empty($users)) {

View file

@ -2,59 +2,211 @@
namespace Opf\DataAccess\Groups; namespace Opf\DataAccess\Groups;
use Illuminate\Database\Eloquent\Model; use Exception;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Opf\Models\Mock\MockGroup;
use Opf\Util\Util; use Opf\Util\Util;
use PDO;
use PDOException;
class MockGroupDataAccess extends Model class MockGroupDataAccess
{ {
protected $table = 'groups'; /** @var PDO */
protected $fillable = ['id', 'displayName', 'members', 'created_at']; private $dbConnection;
public $incrementing = false;
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0:Group"]; /** @var \Monolog\Logger */
private $baseLocation; private $logger;
public function fromArray($data) public function __construct()
{ {
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); // Instantiate our logger
$this->displayName = $data['displayName']; $this->logger = new Logger(MockGroupDataAccess::class);
$this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']); $this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG));
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW');
// Try to obtain a DSN via the Util class and complain with an Exception if there's no DSN
$dsn = Util::buildDbDsn();
if (!isset($dsn)) {
throw new Exception("Can't obtain DSN to connect to DB");
}
// Create the DB connection with PDO (no need to pass username or password for mock DB)
$this->dbConnection = new PDO($dsn, null, null);
// Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations
$this->dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} }
public function fromSCIM($data) public function getAll(): ?array
{ {
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); if (isset($this->dbConnection)) {
$this->displayName = $data['displayName']; $selectStatement = $this->dbConnection->query("SELECT * from groups");
$this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']); if ($selectStatement) {
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW'); $mockGroups = [];
$mockGroupsRaw = $selectStatement->fetchAll(PDO::FETCH_ASSOC);
foreach ($mockGroupsRaw as $group) {
$mockGroup = new MockGroup();
$mockGroup->mapFromArray($group);
$mockGroups[] = $mockGroup;
}
return $mockGroups;
}
$this->logger->error("Couldn't read all groups from mock DB. SELECT query to DB failed");
return null;
}
} }
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') public function getOneById($id): ?MockGroup
{ {
$data = [ if (isset($id) && !empty($id)) {
'schemas' => $this->schemas, if (isset($this->dbConnection)) {
'id' => $this->id, try {
'displayName' => $this->displayName, $selectOnePreparedStatement = $this->dbConnection->prepare(
'members' => [], "SELECT * FROM groups WHERE id = ?"
'meta' => [ );
'created' => Util::dateTime2string($this->created_at),
'location' => $baseLocation . '/Groups/' . $this->id
]
];
if (!empty($this->members)) { $selectRes = $selectOnePreparedStatement->execute([$id]);
$data['members'] = explode(',', $this->members);
if ($selectRes) {
$mockGroupsRaw = $selectOnePreparedStatement->fetchAll(PDO::FETCH_ASSOC);
if ($mockGroupsRaw) {
$mockGroup = new MockGroup();
$mockGroup->mapFromArray($mockGroupsRaw[0]);
return $mockGroup;
} else {
return null;
}
} else {
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
}
} }
if (isset($this->updated_at)) { $this->logger->error(
$data['meta']['updated'] = Util::dateTime2string($this->updated_at); "Argument provided to getOneById in class " . MockGroupDataAccess::class . " is not set or empty"
} );
return null;
}
if ($encode) { public function create(MockGroup $groupToCreate): ?MockGroup
$data = json_encode($data); {
} $dateNow = date('Y-m-d H:i:s');
return $data; if (isset($this->dbConnection)) {
try {
$insertStatement = $this->dbConnection->prepare(
"INSERT INTO groups
(id, displayName, members, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)"
);
$groupToCreate->setId(Util::genUuid());
$insertRes = $insertStatement->execute([
$groupToCreate->getId(),
$groupToCreate->getDisplayName(),
$groupToCreate->getMembers() !== null && !empty($groupToCreate->getMembers())
? $groupToCreate->getMembers() : "",
$dateNow,
$dateNow
]);
if ($insertRes) {
$this->logger->info("Created group " . $groupToCreate->getDisplayName());
return $groupToCreate;
} else {
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("DB connection not available");
}
$this->logger->error("Error creating group");
return null;
}
public function update(string $id, MockGroup $groupToUpdate): ?MockGroup
{
$dateNow = date('Y-m-d H:i:s');
if (isset($this->dbConnection)) {
try {
$query = "";
$values = array();
if ($groupToUpdate->getDisplayName() !== null) {
$query = $query . "displayName = ?, ";
$values[] = $groupToUpdate->getDisplayName();
}
if ($groupToUpdate->getMembers() !== null) {
$query = $query . "members = ?, ";
// We need to transform the string array of user IDs to a single string
$values[] = implode(",", $groupToUpdate->getMembers());
}
if (empty($query)) {
$this->logger->error("No group properties to update");
return null;
}
$query = $query . "updated_at = ? ";
$values[] = $dateNow;
$values[] = $id;
$updateStatement = $this->dbConnection->prepare(
"UPDATE groups SET " . $query . " WHERE id = ?"
);
$updateRes = $updateStatement->execute($values);
if ($updateRes) {
$this->logger->info("Updated group " . $id);
return $this->getOneById($id);
} else {
$this->logger->error("Error updating group " . $id);
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("Error updating group " . $id . " - DB connection unavailable");
}
$this->logger->error("Error updating group " . $id);
return null;
}
public function delete($id): bool
{
if (isset($this->dbConnection)) {
try {
$deleteStatement = $this->dbConnection->prepare(
"DELETE FROM groups WHERE id = ?"
);
$deleteRes = $deleteStatement->execute([$id]);
// In case the delete was successful, return true
if ($deleteRes) {
$this->logger->info("Deleted group " . $id);
return true;
} else {
return false;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("Error deleting group " . $id . " - DB connection unavailable");
}
$this->logger->error("Error deleting group " . $id);
return false;
} }
} }

View file

@ -2,67 +2,220 @@
namespace Opf\DataAccess\Users; namespace Opf\DataAccess\Users;
use Illuminate\Database\Eloquent\Model; use Exception;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Opf\Models\Mock\MockUser;
use Opf\Util\Util; use Opf\Util\Util;
use PDO;
use PDOException;
class MockUserDataAccess extends Model class MockUserDataAccess
{ {
protected $table = 'users'; /** @var PDO */
protected $fillable = ['id', 'userName', 'created_at', 'active', private $dbConnection;
'externalId', 'profileUrl'];
public $incrementing = false;
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"]; /** @var \Monolog\Logger */
private $baseLocation; private $logger;
public function fromArray($data) public function __construct()
{ {
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid()); // Instantiate our logger
$this->userName = isset($data['userName']) ? $data['userName'] : null; $this->logger = new Logger(MockUserDataAccess::class);
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) $this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG));
: (isset($this->created_at) ? $this->created_at : new \DateTime('NOW'));
$this->active = isset($data['active']) ? $data['active'] : true;
$this->externalId = isset($data['externalId']) ? $data['externalId'] : null; // Try to obtain a DSN via the Util class and complain with an Exception if there's no DSN
$this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null; $dsn = Util::buildDbDsn();
} if (!isset($dsn)) {
throw new Exception("Can't obtain DSN to connect to DB");
public function fromSCIM($data)
{
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid());
$this->userName = isset($data['userName']) ? $data['userName'] : null;
$this->created_at = isset($data['meta']) && isset($data['meta']['created'])
? Util::string2dateTime($data['meta']['created'])
: (isset($this->created_at) ? $this->created_at : new \DateTime('NOW'));
$this->active = isset($data['active']) ? $data['active'] : true;
$this->externalId = isset($data['externalId']) ? $data['externalId'] : null;
$this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null;
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'schemas' => $this->schemas,
'id' => $this->id,
'externalId' => $this->externalId,
'meta' => [
'created' => Util::dateTime2string($this->created_at),
'location' => $baseLocation . '/Users/' . $this->id
],
'userName' => $this->userName,
'profileUrl' => $this->profileUrl,
'active' => (bool) $this->active
];
if (isset($this->updated_at)) {
$data['meta']['updated'] = Util::dateTime2string($this->updated_at);
} }
if ($encode) { // Create the DB connection with PDO (no need to pass username or password for mock DB)
$data = json_encode($data); $this->dbConnection = new PDO($dsn, null, null);
// Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations
$this->dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function getAll(): ?array
{
if (isset($this->dbConnection)) {
$selectStatement = $this->dbConnection->query("SELECT * from users");
if ($selectStatement) {
$mockUsers = [];
$mockUsersRaw = $selectStatement->fetchAll(PDO::FETCH_ASSOC);
foreach ($mockUsersRaw as $user) {
$mockUser = new MockUser();
$mockUser->mapFromArray($user);
$mockUsers[] = $mockUser;
}
return $mockUsers;
}
$this->logger->error("Couldn't read all users from mock DB. SELECT query to DB failed");
return null;
}
}
public function getOneById($id): ?MockUser
{
if (isset($id) && !empty($id)) {
if (isset($this->dbConnection)) {
try {
$selectOnePreparedStatement = $this->dbConnection->prepare(
"SELECT * FROM users WHERE id = ?"
);
$selectRes = $selectOnePreparedStatement->execute([$id]);
if ($selectRes) {
$mockUsersRaw = $selectOnePreparedStatement->fetchAll(PDO::FETCH_ASSOC);
if ($mockUsersRaw) {
$mockUser = new MockUser();
$mockUser->mapFromArray($mockUsersRaw[0]);
return $mockUser;
} else {
return null;
}
} else {
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
}
} }
return $data; $this->logger->error(
"Argument provided to getOneById in class " . MockUserDataAccess::class . " is not set or empty"
);
return null;
}
public function create(MockUser $userToCreate): ?MockUser
{
$dateNow = date('Y-m-d H:i:s');
if (isset($this->dbConnection)) {
try {
$insertStatement = $this->dbConnection->prepare(
"INSERT INTO users
(id, userName, active, externalId, profileUrl, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)"
);
$userToCreate->setId(Util::genUuid());
$insertRes = $insertStatement->execute([
$userToCreate->getId(),
$userToCreate->getUserName(),
$userToCreate->getActive(),
$userToCreate->getExternalId(),
$userToCreate->getProfileUrl(),
$dateNow,
$dateNow
]);
if ($insertRes) {
$this->logger->info("Created user " . $userToCreate->getUserName());
return $userToCreate;
} else {
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("DB connection not available");
}
$this->logger->error("Error creating user");
return null;
}
public function update(string $id, MockUser $userToUpdate): ?MockUser
{
$dateNow = date('Y-m-d H:i:s');
if (isset($this->dbConnection)) {
try {
$query = "";
$values = array();
if ($userToUpdate->getUserName() !== null) {
$query = $query . "userName = ?, ";
$values[] = $userToUpdate->getUserName();
}
if ($userToUpdate->getActive() !== null) {
$query = $query . "active = ?, ";
$values[] = $userToUpdate->getActive();
}
if ($userToUpdate->getProfileUrl() !== null) {
$query = $query . "profileUrl = ?, ";
$values[] = $userToUpdate->getProfileUrl();
}
if ($userToUpdate->getExternalId() !== null) {
$query = $query . "externalId = ?, ";
$values[] = $userToUpdate->getExternalId();
}
if (empty($query)) {
$this->logger->error("No user properties to update");
return null;
}
$query = $query . "updated_at = ? ";
$values[] = $dateNow;
$values[] = $id;
$updateStatement = $this->dbConnection->prepare(
"UPDATE users SET " . $query . " WHERE id = ?"
);
$updateRes = $updateStatement->execute($values);
if ($updateRes) {
$this->logger->info("Updated user " . $id);
return $this->getOneById($id);
} else {
$this->logger->error("Error updating user " . $id);
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("Error updating user " . $id . " - DB connection unavailable");
}
$this->logger->error("Error updating user " . $id);
return null;
}
public function delete($id): bool
{
if (isset($this->dbConnection)) {
try {
$deleteStatement = $this->dbConnection->prepare(
"DELETE FROM users WHERE id = ?"
);
$deleteRes = $deleteStatement->execute([$id]);
// In case the delete was successful, return true
if ($deleteRes) {
$this->logger->info("Deleted user " . $id);
return true;
} else {
return false;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("Error deleting user " . $id . " - DB connection unavailable");
}
$this->logger->error("Error deleting user " . $id);
return false;
} }
} }

View file

@ -1,319 +0,0 @@
<?php
namespace Opf\DataAccess\Users;
use Exception;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Opf\Util\Util;
use Opf\Models\PFA\PfaUser;
use PDO;
use PDOException;
class PfaUserDataAccess
{
/** @var array */
private $config;
/** @var PDO */
private $dbConnection;
/** @var \Monolog\Logger */
private $logger;
public function __construct()
{
// Instantiate our logger
$this->logger = new Logger(PfaUserDataAccess::class);
$this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG));
// Try to obtain a DSN via the Util class and complain with an Exception if there's no DSN
$dsn = Util::buildDbDsn();
if (!isset($dsn)) {
throw new Exception("Can't obtain DSN to connect to DB");
}
$this->config = Util::getConfigFile();
if (isset($this->config) && !empty($this->config)) {
if (isset($this->config['db']) && !empty($this->config['db'])) {
if (
isset($this->config['db']['user'])
&& !empty($this->config['db']['user'])
&& isset($this->config['db']['password'])
&& !empty($this->config['db']['password'])
) {
$dbUsername = $this->config['db']['user'];
$dbPassword = $this->config['db']['password'];
} else {
throw new Exception("No DB username and/or password provided to connect to DB");
}
}
}
// Create the DB connection with PDO
$this->dbConnection = new PDO($dsn, $dbUsername, $dbPassword);
// Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations
$this->dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
public function getAll(): ?array
{
if (isset($this->dbConnection)) {
// TODO: should we use a variable for the table name?
// PFA users are 'mailbox' entries (see also https://web.audriga.com/mantis/view.php?id=5806#c28866):
// - with a corresponding 'alias' entry
// - with the 'alias.address' value being part of the 'alias.goto' value
$selectStatement = $this->dbConnection->query("SELECT mailbox.* FROM mailbox INNER JOIN alias
WHERE mailbox.username = alias.address
AND alias.goto LIKE CONCAT('%', alias.address, '%')");
if ($selectStatement) {
$pfaUsers = [];
$pfaUsersRaw = $selectStatement->fetchAll(PDO::FETCH_ASSOC);
foreach ($pfaUsersRaw as $user) {
$pfaUser = new PfaUser();
$pfaUser->mapFromArray($user);
$pfaUsers[] = $pfaUser;
}
return $pfaUsers;
}
$this->logger->error("Couldn't read all users from PFA. SELECT query to DB failed");
}
$this->logger->error("Couldn't connect to DB while attempting to read all users from PFA");
return null;
}
// TODO: In the case of PFA, it maybe makes sense to rename this to something like getOneByUsername,
// since username is the distinguishing property between users (mailboxes) in PFA and not ID
public function getOneById($id): ?PfaUser
{
if (isset($id) && !empty($id)) {
if (isset($this->dbConnection)) {
try {
// TODO: should we use a variable for the table name?
$selectOnePreparedStatement = $this->dbConnection->prepare(
"SELECT mailbox.* FROM mailbox INNER JOIN alias
WHERE mailbox.username = alias.address
AND alias.goto LIKE CONCAT('%', alias.address, '%')
AND mailbox.username = ?"
);
$selectRes = $selectOnePreparedStatement->execute([$id]);
if ($selectRes) {
$pfaUsersRaw = $selectOnePreparedStatement->fetchAll(PDO::FETCH_ASSOC);
if ($pfaUsersRaw) {
$pfaUser = new PfaUser();
$pfaUser->mapFromArray($pfaUsersRaw[0]);
return $pfaUser;
} else {
return null;
}
} else {
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
}
}
$this->logger->error(
"Argument provided to getOneById in class " . PfaUserDataAccess::class . " is not set or empty"
);
return null;
}
public function create(PfaUser $userToCreate): ?PfaUser
{
$dateNow = date('Y-m-d H:i:s');
$date2000 = '2000-01-01 00:00:00';
if (isset($this->dbConnection)) {
try {
// We want to commit both insert (in mailbox and in alias) in one single commit,
// and we want to abort both if something fails
$this->dbConnection->beginTransaction();
// TODO: should we use a variable for the table name?
$insertStatement = $this->dbConnection->prepare(
"INSERT INTO mailbox
(username, password, name, maildir, quota, local_part, domain, created, modified, active, phone,
email_other, token, token_validity, password_expiry)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
// When performing an INSERT into the mailbox table, maildir, domain and local_part
// as columns don't have default values.
// That's why here in the prepared statement we currently just give them the value of the empty string,
// so as to avoid the issue of MySQL complaining
// that nothing is provided for them when they have no default value
$insertRes1 = $insertStatement->execute([
$userToCreate->getUserName(),
$userToCreate->getPassword() !== null ? $userToCreate->getPassword() : '',
$userToCreate->getName() !== null ? $userToCreate->getName() : '',
$userToCreate->getMaildir(),
$userToCreate->getQuota() !== null ? $userToCreate->getQuota() : 0,
$userToCreate->getLocalPart(),
$userToCreate->getDomain(),
$dateNow,
$dateNow,
$userToCreate->getActive(),
$userToCreate->getPhone() !== null ? $userToCreate->getPhone() : '',
$userToCreate->getEmailOther() !== null ? $userToCreate->getEmailOther() : '',
$userToCreate->getToken() !== null ? $userToCreate->getToken() : '',
$userToCreate->getTokenValidity() !== null ? $userToCreate->getTokenValidity() : $date2000,
$userToCreate->getPasswordExpiry() !== null ? $userToCreate->getPasswordExpiry() : $date2000
]);
$insertStatement = $this->dbConnection->prepare(
"INSERT INTO alias
(address, goto, domain, created, modified, active)
VALUES (?, ?, ?, ?, ?, ?)"
);
// When performing an INSERT into the mailbox table, maildir, domain and local_part
// as columns don't have default values.
// That's why here in the prepared statement we currently just give them the value of the empty string,
// so as to avoid the issue of MySQL complaining
// that nothing is provided for them when they have no default value
$insertRes2 = $insertStatement->execute([
$userToCreate->getUserName(),
$userToCreate->getUserName(),
$userToCreate->getDomain(),
$dateNow,
$dateNow,
$userToCreate->getActive()
]);
// In case the write was successful, return the user that was just written
if ($insertRes1 && $insertRes2) {
$this->dbConnection->commit();
$this->logger->info("Created user " . $userToCreate->getUserName());
return $this->getOneById($userToCreate->getUserName());
//return $userToCreate;
} else {
// Otherwise, rollback and just return null
$this->dbConnection->rollBack();
return null;
}
} catch (PDOException $e) {
$this->dbConnection->rollBack();
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("DB connection not available");
}
$this->logger->error("Error creating user");
return null;
}
public function update(string $username, PfaUser $userToUpdate): ?PfaUser
{
$dateNow = date('Y-m-d H:i:s');
if (isset($this->dbConnection)) {
try {
$query = "";
$values = array();
if ($userToUpdate->getPassword() !== null) {
$query = $query . "password = ?, ";
$values[] = $userToUpdate->getPassword();
}
if ($userToUpdate->getName() !== null) {
$query = $query . "name = ?, ";
$values[] = $userToUpdate->getName();
}
if ($userToUpdate->getQuota() !== null) {
$query = $query . "quota = ?, ";
$values[] = $userToUpdate->getQuota();
}
if ($userToUpdate->getActive() !== null) {
$query = $query . "active = ?, ";
$values[] = $userToUpdate->getActive();
}
if ($userToUpdate->getEmailOther() !== null) {
$query = $query . "email_other = ?, ";
$values[] = $userToUpdate->getEmailOther();
}
if (empty($query)) {
$this->logger->error("No user properties to update");
return null;
}
$query = $query . "modified = ? ";
$values[] = $dateNow;
$values[] = $username;
// Since in PFA the username column in the mailbox table is the primary and is unique,
// we use username in this case as an id that serves the purpose of a unique identifier
// TODO: should we use a variable for the table name?
$updateStatement = $this->dbConnection->prepare(
"UPDATE mailbox SET " . $query . " WHERE username = ?"
);
$updateRes = $updateStatement->execute($values);
// In case the update was successful, return the user that was just updated
if ($updateRes) {
$this->logger->info("Updated user " . $username);
return $this->getOneById($username);
} else {
$this->logger->error("Error updating user " . $username);
return null;
}
} catch (PDOException $e) {
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("Error updating user " . $username . " - DB connection unavailable");
}
$this->logger->error("Error updating user " . $username);
return null;
}
public function delete($username): bool
{
if (isset($this->dbConnection)) {
try {
// We want to commit both delete (in mailbox and in alias) in one single commit,
// and we want to abort both if something fails
$this->dbConnection->beginTransaction();
// TODO: should we use a variable for the table name?
$deleteStatement = $this->dbConnection->prepare(
"DELETE FROM mailbox WHERE username = ?"
);
$deleteRes1 = $deleteStatement->execute([$username]);
// TODO: should we use a variable for the table name?
$deleteStatement = $this->dbConnection->prepare(
"DELETE FROM alias WHERE address = ?"
);
$deleteRes2 = $deleteStatement->execute([$username]);
// In case the delete was successful, return true
if ($deleteRes1 && $deleteRes2) {
$this->dbConnection->commit();
$this->logger->info("Deleted user " . $username);
return true;
//return $userToCreate;
} else {
// Otherwise, rollback and just return false
$this->dbConnection->rollBack();
return false;
}
} catch (PDOException $e) {
$this->dbConnection->rollBack();
$this->logger->error($e->getMessage());
}
} else {
$this->logger->error("Error deleting user " . $username . " - DB connection unavailable");
}
$this->logger->error("Error deleting user " . $username);
return false;
}
}

View file

@ -2,58 +2,55 @@
declare(strict_types=1); declare(strict_types=1);
use DI\ContainerBuilder;
use Opf\Controllers\Controller; use Opf\Controllers\Controller;
use Opf\Util\Util; use Opf\Util\Util;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Tuupola\Middleware\JwtAuthentication; use Tuupola\Middleware\JwtAuthentication;
return function (ContainerBuilder $containerBuilder) { return [
$containerBuilder->addDefinitions([ // Monolog
// Monolog Monolog\Logger::class => function () {
Monolog\Logger::class => function (ContainerInterface $c) { $config = Util::getConfigFile();
$config = Util::getConfigFile(); $settings = $config['logger'];
$settings = $config['logger']; $logger = new Monolog\Logger($settings['name']);
$logger = new Monolog\Logger($settings['name']); $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level'])); return $logger;
return $logger; },
},
// JWT // JWT
'JwtAuthentication' => function (ContainerInterface $c) { 'JwtAuthentication' => function (ContainerInterface $c) {
$config = Util::getConfigFile(); $config = Util::getConfigFile();
$settings = $config['jwt']; $settings = $config['jwt'];
$settings["logger"] = $c->get(Monolog\Logger::class); $settings["logger"] = $c->get(Monolog\Logger::class);
$settings["attribute"] = "jwt"; $settings["attribute"] = "jwt";
// Don't ask for JWT when trying to obtain one // Don't ask for JWT when trying to obtain one
$basePath = ""; $basePath = "";
if (isset($config) && !empty($config)) { if (isset($config) && !empty($config)) {
if (isset($config["basePath"]) && !empty($config["basePath"])) { if (isset($config["basePath"]) && !empty($config["basePath"])) {
$basePath = $config["basePath"]; $basePath = $config["basePath"];
}
} }
$settings["ignore"] = [$basePath . "/jwt"];
if (!isset($settings['error'])) {
$settings["error"] = function (
ResponseInterface $response,
$arguments
) {
$data["status"] = "error";
$data["message"] = $arguments["message"];
return $response
->withHeader("Content-Type", "application/json")
->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
};
}
return new JwtAuthentication($settings);
},
// Controllers
Controller::class => function (ContainerInterface $c) {
return new Controller($c);
} }
]); $settings["ignore"] = [$basePath . "/jwt"];
};
if (!isset($settings['error'])) {
$settings["error"] = function (
ResponseInterface $response,
$arguments
) {
$data["status"] = "error";
$data["message"] = $arguments["message"];
return $response
->withHeader("Content-Type", "application/json")
->getBody()->write(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
};
}
return new JwtAuthentication($settings);
},
// Controllers
Controller::class => function (ContainerInterface $c) {
return new Controller($c);
}
];

View file

@ -2,45 +2,51 @@
declare(strict_types=1); declare(strict_types=1);
use DI\ContainerBuilder;
use Opf\Adapters\Groups\MockGroupAdapter; use Opf\Adapters\Groups\MockGroupAdapter;
use Opf\Adapters\Users\MockUserAdapter; use Opf\Adapters\Users\MockUserAdapter;
use Opf\Controllers\Controller;
use Opf\DataAccess\Groups\MockGroupDataAccess; use Opf\DataAccess\Groups\MockGroupDataAccess;
use Opf\DataAccess\Users\MockUserDataAccess; use Opf\DataAccess\Users\MockUserDataAccess;
use Opf\Middleware\SimpleAuthMiddleware;
use Opf\Repositories\Groups\MockGroupsRepository; use Opf\Repositories\Groups\MockGroupsRepository;
use Opf\Repositories\Users\MockUsersRepository; use Opf\Repositories\Users\MockUsersRepository;
use Opf\Util\Authentication\SimpleBearerAuthenticator;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Tuupola\Middleware\JwtAuthentication;
return function (ContainerBuilder $containerBuilder) { return [
$containerBuilder->addDefinitions([ // Repositories
// Repositories 'UsersRepository' => function (ContainerInterface $c) {
'UsersRepository' => function (ContainerInterface $c) { return new MockUsersRepository($c);
return new MockUsersRepository($c); },
},
'GroupsRepository' => function (ContainerInterface $c) { 'GroupsRepository' => function (ContainerInterface $c) {
return new MockGroupsRepository($c); return new MockGroupsRepository($c);
}, },
// Data access classes // Data access classes
'UsersDataAccess' => function () { 'UsersDataAccess' => function () {
return new MockUserDataAccess(); return new MockUserDataAccess();
}, },
'GroupsDataAccess' => function () { 'GroupsDataAccess' => function () {
return new MockGroupDataAccess(); return new MockGroupDataAccess();
}, },
// Adapters // Adapters
'UsersAdapter' => function () { 'UsersAdapter' => function () {
return new MockUserAdapter(); return new MockUserAdapter();
}, },
'GroupsAdapter' => function () { 'GroupsAdapter' => function () {
return new MockGroupAdapter(); return new MockGroupAdapter();
} },
]);
}; // Auth middleware
'AuthMiddleware' => function (ContainerInterface $c) {
return new SimpleAuthMiddleware($c);
},
// Authenticators (used by SimpleAuthMiddleware)
'BearerAuthenticator' => function (ContainerInterface $c) {
return new SimpleBearerAuthenticator($c);
}
];

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
use DI\ContainerBuilder;
use Opf\Adapters\Users\PfaUserAdapter;
use Opf\DataAccess\Users\PfaUserDataAccess;
use Opf\Repositories\Users\PfaUsersRepository;
use Psr\Container\ContainerInterface;
return function (ContainerBuilder $containerBuilder) {
$containerBuilder->addDefinitions([
// Repositories
'UsersRepository' => function (ContainerInterface $c) {
return new PfaUsersRepository($c);
},
// Data access classes
'UsersDataAccess' => function () {
return new PfaUserDataAccess();
},
// Adapters
'UsersAdapter' => function () {
return new PfaUserAdapter();
}
]);
};

View file

@ -69,6 +69,7 @@ class HttpErrorHandler extends ErrorHandler
$payload = json_encode($error, JSON_PRETTY_PRINT); $payload = json_encode($error, JSON_PRETTY_PRINT);
$response = $this->responseFactory->createResponse($statusCode); $response = $this->responseFactory->createResponse($statusCode);
$response = $response->withHeader('Content-Type', 'application/scim+json');
$response->getBody()->write($payload); $response->getBody()->write($payload);
return $response; return $response;

View file

@ -0,0 +1,56 @@
<?php
namespace Opf\Middleware;
use Opf\Util\Authentication\SimpleBearerAuthenticator;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use Slim\Routing\RouteContext;
class SimpleAuthMiddleware implements MiddlewareInterface
{
/** @var \Opf\Util\Authentication\SimpleBearerAuthenticator */
private $bearerAuthenticator;
public function __construct(ContainerInterface $container)
{
$this->bearerAuthenticator = $container->get('BearerAuthenticator');
}
public function process(Request $request, RequestHandler $handler): Response
{
// If no 'Authorization' header supplied, we directly return a 401
if (!$request->hasHeader('Authorization')) {
return new Response(401);
}
// $request->getHeader() gives back a string array, hence the need for [0]
$authHeader = $request->getHeader('Authorization')[0];
// Obtain the auth type and the supplied credentials
$authHeaderSplit = explode(' ', $authHeader);
$authType = $authHeaderSplit[0];
$authCredentials = $authHeaderSplit[1];
// This is a flag that tracks whether auth succeeded or not
$isAuthSuccessful = false;
$authorizationInfo = [];
// Call the right authenticator, based on the auth type
if (strcmp($authType, 'Bearer') === 0) {
$isAuthSuccessful = $this->bearerAuthenticator->authenticate($authCredentials, $authorizationInfo);
}
// If everything went fine, let the request pass through
if ($isAuthSuccessful) {
return $handler->handle($request);
}
// If something didn't go right so far, then return a 401
return new Response(401);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Opf\Models\Mock;
class MockCommonEntity
{
/** @var string|null $id */
protected $id;
/** @var string|null $createdAt */
protected $createdAt;
/** @var string|null $updatedAt */
protected $updatedAt;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getCreatedAt()
{
return $this->createdAt;
}
public function setCreatedAt($createdAt)
{
$this->createdAt = $createdAt;
}
public function getUpdatedAt()
{
return $this->updatedAt;
}
public function setUpdatedAt($updatedAt)
{
$this->updatedAt = $updatedAt;
}
}

View file

@ -0,0 +1,69 @@
<?php
namespace Opf\Models\Mock;
class MockGroup extends MockCommonEntity
{
/** @var string|null $displayName */
private $displayName;
/** @var array<string>|null $members */
private $members;
public function mapFromArray($properties = null): bool
{
$result = true;
if ($properties !== null) {
foreach ($properties as $key => $value) {
if (strcasecmp($key, 'id') === 0) {
$this->id = $value;
continue;
}
if (strcasecmp($key, 'created_at') === 0) {
$this->createdAt = $value;
continue;
}
if (strcasecmp($key, 'updated_at') === 0) {
$this->updatedAt = $value;
continue;
}
if (strcasecmp($key, 'displayName') === 0) {
$this->displayName = $value;
continue;
}
if (strcasecmp($key, 'members') === 0) {
$this->members = $value;
continue;
}
$result = false;
}
} else {
$result = false;
}
return $result;
}
public function getDisplayName()
{
return $this->displayName;
}
public function setDisplayName($displayName)
{
$this->displayName = $displayName;
}
public function getMembers()
{
return $this->members;
}
public function setMembers($members)
{
$this->members = $members;
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Opf\Models\Mock;
class MockUser extends MockCommonEntity
{
/** @var string|null $userName */
private $userName;
/** @var bool $active */
private $active;
/** @var string|null $externalId */
private $externalId;
/** @var string|null $profileUrl */
private $profileUrl;
public function mapFromArray($properties = null): bool
{
$result = true;
if ($properties !== null) {
foreach ($properties as $key => $value) {
if (strcasecmp($key, 'id') === 0) {
$this->id = $value;
continue;
}
if (strcasecmp($key, 'created_at') === 0) {
$this->createdAt = $value;
continue;
}
if (strcasecmp($key, 'updated_at') === 0) {
$this->updatedAt = $value;
continue;
}
if (strcasecmp($key, 'userName') === 0) {
$this->userName = $value;
continue;
}
if (strcasecmp($key, 'active') === 0) {
if ($value === "1") {
$this->active = true;
} elseif ($value === "0") {
$this->active = false;
} else {
$this->active = $value;
}
continue;
}
if (strcasecmp($key, 'externalId') === 0) {
$this->externalId = $value;
continue;
}
if (strcasecmp($key, 'profileUrl') === 0) {
$this->profileUrl = $value;
continue;
}
$result = false;
}
} else {
$result = false;
}
return $result;
}
public function getUserName()
{
return $this->userName;
}
public function setUserName($userName)
{
$this->userName = $userName;
}
public function getActive()
{
return $this->active;
}
public function setActive($active)
{
$this->active = $active;
}
public function getExternalId()
{
return $this->externalId;
}
public function setExternalId($externalId)
{
$this->externalId = $externalId;
}
public function getProfileUrl()
{
return $this->profileUrl;
}
public function setProfileUrl($profileUrl)
{
$this->profileUrl = $profileUrl;
}
}

View file

@ -1,364 +0,0 @@
<?php
namespace Opf\Models\PFA;
class PfaUser
{
/** @var string|null $userName */
private ?string $userName = null;
/** @var string|null $password */
private ?string $password = null;
/** @var string|null $name */
private ?string $name = null;
/** @var string|null $maildir */
private ?string $maildir = null;
/** @var int|null $quota */
private ?int $quota = null;
/** @var string|null $localPart */
private ?string $localPart = null;
/** @var string|null $domain */
private ?string $domain = null;
/** @var string|null $created */
private ?string $created = null;
/** @var string|null $modified */
private ?string $modified = null;
/** @var string|null $active */
private ?string $active = null;
/** @var string|null $phone */
private ?string $phone = null;
/** @var string|null $emailOther */
private ?string $emailOther = null;
/** @var string|null $token */
private ?string $token = null;
/** @var string|null $tokenValidity */
private ?string $tokenValidity = null;
/** @var string|null $passwordExpiry */
private ?string $passwordExpiry = null;
public function mapFromArray($properties = null): bool
{
$result = true;
if ($properties !== null) {
foreach ($properties as $key => $value) {
if (strcasecmp($key, 'userName') === 0) {
$this->userName = $value;
continue;
}
if (strcasecmp($key, 'password') === 0) {
$this->password = $value;
continue;
}
if (strcasecmp($key, 'name') === 0) {
$this->name = $value;
continue;
}
if (strcasecmp($key, 'maildir') === 0) {
$this->maildir = $value;
continue;
}
if (strcasecmp($key, 'quota') === 0) {
$this->quota = $value;
continue;
}
if (strcasecmp($key, 'localpart') === 0) {
$this->localpart = $value;
continue;
}
if (strcasecmp($key, 'domain') === 0) {
$this->domain = $value;
continue;
}
if (strcasecmp($key, 'created') === 0) {
$this->created = $value;
continue;
}
if (strcasecmp($key, 'modified') === 0) {
$this->modified = $value;
continue;
}
if (strcasecmp($key, 'active') === 0) {
$this->active = $value;
continue;
}
if (strcasecmp($key, 'phone') === 0) {
$this->phone = $value;
continue;
}
if (strcasecmp($key, 'emailOther') === 0) {
$this->emailOther = $value;
continue;
}
if (strcasecmp($key, 'token') === 0) {
$this->token = $value;
continue;
}
if (strcasecmp($key, 'tokenValidity') === 0) {
$this->tokenValidity = $value;
continue;
}
if (strcasecmp($key, 'passwordExpiry') === 0) {
$this->passwordExpiry = $value;
continue;
}
$result = false;
}
} else {
$result = false;
}
return $result;
}
/**
* @return string|null
*/
public function getUserName(): ?string
{
return $this->userName;
}
/**
* @param string|null $userName
*/
public function setUserName(?string $userName): void
{
$this->userName = $userName;
}
/**
* @return string|null
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* @param string|null $password
*/
public function setPassword(?string $password): void
{
$this->password = $password;
}
/**
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param string|null $name
*/
public function setName(?string $name): void
{
$this->name = $name;
}
/**
* @return string|null
*/
public function getMaildir(): ?string
{
return $this->maildir;
}
/**
* @param string|null $maildir
*/
public function setMaildir(?string $maildir): void
{
$this->maildir = $maildir;
}
/**
* @return int|null
*/
public function getQuota(): ?int
{
return $this->quota;
}
/**
* @param int|null $quota
*/
public function setQuota(?int $quota): void
{
$this->quota = $quota;
}
/**
* @return string|null
*/
public function getLocalPart(): ?string
{
return $this->localPart;
}
/**
* @param string|null $localPart
*/
public function setLocalPart(?string $localPart): void
{
$this->localPart = $localPart;
}
/**
* @return string|null
*/
public function getDomain(): ?string
{
return $this->domain;
}
/**
* @param string|null $domain
*/
public function setDomain(?string $domain): void
{
$this->domain = $domain;
}
/**
* @return string|null
*/
public function getCreated(): ?string
{
return $this->created;
}
/**
* @param string|null $created
*/
public function setCreated(?string $created): void
{
$this->created = $created;
}
/**
* @return string|null
*/
public function getModified(): ?string
{
return $this->modified;
}
/**
* @param string|null $modified
*/
public function setModified(?string $modified): void
{
$this->modified = $modified;
}
/**
* @return string|null
*/
public function getActive(): ?string
{
return $this->active;
}
/**
* @param string|null $active
*/
public function setActive(?string $active): void
{
$this->active = $active;
}
/**
* @return string|null
*/
public function getPhone(): ?string
{
return $this->phone;
}
/**
* @param string|null $phone
*/
public function setPhone(?string $phone): void
{
$this->phone = $phone;
}
/**
* @return string|null
*/
public function getEmailOther(): ?string
{
return $this->emailOther;
}
/**
* @param string|null $emailOther
*/
public function setEmailOther(?string $emailOther): void
{
$this->emailOther = $emailOther;
}
/**
* @return string|null
*/
public function getToken(): ?string
{
return $this->token;
}
/**
* @param string|null $token
*/
public function setToken(?string $token): void
{
$this->token = $token;
}
/**
* @return string|null
*/
public function getTokenValidity(): ?string
{
return $this->tokenValidity;
}
/**
* @param string|null $tokenValidity
*/
public function setTokenValidity(?string $tokenValidity): void
{
$this->tokenValidity = $tokenValidity;
}
/**
* @return string|null
*/
public function getPasswordExpiry(): ?string
{
return $this->passwordExpiry;
}
/**
* @param string|null $passwordExpiry
*/
public function setPasswordExpiry(?string $passwordExpiry): void
{
$this->passwordExpiry = $passwordExpiry;
}
}

View file

@ -2,7 +2,221 @@
namespace Opf\Models\SCIM\Custom\Domains; namespace Opf\Models\SCIM\Custom\Domains;
// TODO: This is currently a dummy class to demonstrate how to add custom SCIM resources to the codebase use Opf\Models\SCIM\Standard\CommonEntity;
class Domain use Opf\Models\SCIM\Standard\Meta;
use Opf\Util\Util;
class Domain extends CommonEntity
{ {
/** @var string|null $domainName */
private ?string $domainName;
/** @var string|null $description */
private ?string $description = null;
/** @var int $maxAliases */
private int $maxAliases;
/** @var int $maxMailboxes */
private int $maxMailboxes;
/** @var int $maxQuota */
private int $maxQuota;
/** @var int $usedQuota */
private int $usedQuota;
/** @var bool $active */
private bool $active;
/**
* @return string|null
*/
public function getDomainName(): ?string
{
return $this->domainName;
}
/**
* @param string|null $domainName
*/
public function setDomainName(?string $domainName): void
{
$this->domainName = $domainName;
}
/**
* @return string|null
*/
public function getDescription(): ?string
{
return $this->description;
}
/**
* @param string|null $description
*/
public function setDescription(?string $description): void
{
$this->description = $description;
}
/**
* @return int
*/
public function getMaxAliases(): int
{
return $this->maxAliases;
}
/**
* @param int $maxAliases
*/
public function setMaxAliases(int $maxAliases): void
{
$this->maxAliases = $maxAliases;
}
/**
* @return int
*/
public function getMaxMailboxes(): int
{
return $this->maxMailboxes;
}
/**
* @param int $maxMailboxes
*/
public function setMaxMailboxes(int $maxMailboxes): void
{
$this->maxMailboxes = $maxMailboxes;
}
/**
* @return int
*/
public function getMaxQuota(): int
{
return $this->maxQuota;
}
/**
* @param int $maxQuota
*/
public function setMaxQuota(int $maxQuota): void
{
$this->maxQuota = $maxQuota;
}
/**
* @return int
*/
public function getUsedQuota(): int
{
return $this->usedQuota;
}
/**
* @param int $usedQuota
*/
public function setUsedQuota(int $usedQuota): void
{
$this->usedQuota = $usedQuota;
}
/**
* @return bool
*/
public function getActive(): bool
{
return $this->active;
}
/**
* @param bool $active
*/
public function setActive(bool $active): void
{
$this->active = $active;
}
/**
* Create a Domain object from JSON SCIM data
*
* @param array $data The JSON SCIM data
*/
public function fromSCIM(array $data)
{
if (isset($data['id'])) {
$this->setId($data['id']);
}
$this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null);
$this->setDomainName(isset($data['domainName']) ? $data['domainName'] : null);
$this->setDescription(isset($data['description']) ? $data['description'] : null);
// For the int attributes that are set below, we set 0 as the default value
// in case that nothing is supplied and/or set in the JSON
// TODO: Is that an okayish solution with this default value?
$this->setMaxAliases(isset($data['maxAliases']) ? $data['maxAliases'] : 0);
$this->setMaxMailboxes(isset($data['maxMailboxes']) ? $data['maxMailboxes'] : 0);
$this->setMaxQuota(isset($data['maxQuota']) ? $data['maxQuota'] : 0);
$this->setUsedQuota(isset($data['usedQuota']) ? $data['usedQuota'] : 0);
if (isset($data['meta']) && !empty($data['meta'])) {
$meta = new Meta();
$meta->setResourceType("Domain");
$meta->setCreated(isset($data['meta']['created']) ? $data['meta']['created'] : null);
$meta->setLastModified(isset($data['meta']['modified']) ? $data['meta']['modified'] : null);
$meta->setVersion(isset($data['meta']['version']) ? $data['meta']['version'] : null);
$this->setMeta($meta);
}
// In case that "active" is not set in the JSON, we set it to true by default
// TODO: Is that an okayish solution with this default value?
$this->setActive(isset($data['active']) ? boolval($data['active']) : true);
$this->setSchemas(isset($data['schemas']) ? $data['schemas'] : []);
}
/**
* Convert a Domain object to its JSON or array representation
*
* @param bool $encode A flag indicating if the object should be encoded as JSON
* @param string $baseLocation A path indicating the base location of the SCIM server
*
* @return array|string|false If $encode is true, return either a JSON string or false on failure, else an array
*/
public function toSCIM(bool $encode = true, string $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'id' => $this->getId(),
'externalId' => $this->getExternalId(),
'schemas' => [Util::DOMAIN_SCHEMA],
'meta' => null !== $this->getMeta() ? [
'resourceType' => null !== $this->getMeta()->getResourceType()
? $this->getMeta()->getResourceType() : null,
'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null,
'updated' => null !== $this->getMeta()->getLastModified() ? $this->getMeta()->getLastModified() : null,
'location' => $baseLocation . '/Domains/' . $this->getId(),
'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null
] : null,
'domainName' => null !== $this->getDomainName() ? $this->getDomainName() : null,
'description' => null !== $this->getDescription() ? $this->getDescription() : null,
'maxAliases' => $this->getMaxAliases(),
'maxMailboxes' => $this->getMaxMailboxes(),
'maxQuota' => $this->getMaxQuota(),
'usedQuota' => $this->getUsedQuota(),
'active' => $this->getActive()
];
if ($encode) {
$data = json_encode($data);
}
return $data;
}
} }

View file

@ -0,0 +1,89 @@
<?php
namespace Opf\Models\SCIM\Standard\Filters;
class AttributeExpression extends FilterExpression
{
/** @var string $attributePath */
private $attributePath;
/** @var \Opf\Models\SCIM\Standard\Filters\AttributeOperator $compareOperator */
private $compareOperator;
/** @var bool|string|int|null $comparisonValue */
private $comparisonValue;
public function __construct($attributePath, $compareOperator, $comparisonValue)
{
if (isset($attributePath) && !empty($attributePath) && is_string($attributePath)) {
$this->attributePath = $attributePath;
} else {
throw new FilterException(
"Attribute path passed to Attribute Expression was either empty, null or not a string"
);
}
switch ($compareOperator) {
case "eq":
$this->compareOperator = AttributeOperator::OP_EQ;
break;
case "ne":
$this->compareOperator = AttributeOperator::OP_NE;
break;
case "co":
$this->compareOperator = AttributeOperator::OP_CO;
break;
case "sw":
$this->compareOperator = AttributeOperator::OP_SW;
break;
case "ew":
$this->compareOperator = AttributeOperator::OP_EW;
break;
case "gt":
$this->compareOperator = AttributeOperator::OP_GT;
break;
case "lt":
$this->compareOperator = AttributeOperator::OP_LT;
break;
case "ge":
$this->compareOperator = AttributeOperator::OP_GE;
break;
case "le":
$this->compareOperator = AttributeOperator::OP_LE;
break;
case "pr":
$this->compareOperator = AttributeOperator::OP_PR;
break;
default:
throw new FilterException("Invalid AttributeOperation passed to AttributeExpression");
break;
}
$this->comparisonValue = $comparisonValue;
}
public function getAttributePath()
{
return $this->attributePath;
}
public function getCompareOperator()
{
return $this->compareOperator;
}
public function getComparisonValue()
{
return $this->comparisonValue;
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Opf\Models\SCIM\Standard\Filters;
class AttributeOperator
{
public const OP_EQ = 1;
public const OP_NE = 2;
public const OP_CO = 3;
public const OP_SW = 4;
public const OP_EW = 5;
public const OP_PR = 6;
public const OP_GT = 7;
public const OP_GE = 8;
public const OP_LT = 9;
public const OP_LE = 10;
}

View file

@ -0,0 +1,9 @@
<?php
namespace Opf\Models\SCIM\Standard\Filters;
use Exception;
class FilterException extends Exception
{
}

View file

@ -0,0 +1,13 @@
<?php
namespace Opf\Models\SCIM\Standard\Filters;
/**
* Base class that represents different types of filter expresssion
* (e.g., attribute expressions, logical expressions, etc.)
*
* See https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
*/
class FilterExpression
{
}

View file

@ -5,6 +5,7 @@ namespace Opf\Models\SCIM\Standard\Groups;
use Opf\Util\Util; use Opf\Util\Util;
use Opf\Models\SCIM\Standard\CommonEntity; use Opf\Models\SCIM\Standard\CommonEntity;
use Opf\Models\SCIM\Standard\Meta; use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
class CoreGroup extends CommonEntity class CoreGroup extends CommonEntity
{ {
@ -47,14 +48,27 @@ class CoreGroup extends CommonEntity
$this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null); $this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null);
$meta = new Meta(); $meta = new Meta();
if (isset($data['meta']) && isset($data['meta']['created'])) { // This is currently commented out, since the code complains about wrongly
// formatted timestamps sometimes when fromSCIM is called
// TODO: Need to possibly refactor string2datetime and/or dateTime2string in order to fix this
/*if (isset($data['meta']) && isset($data['meta']['created'])) {
$meta->setCreated(Util::string2dateTime($data['meta']['created'])); $meta->setCreated(Util::string2dateTime($data['meta']['created']));
} else { } else {
$meta->setCreated(Util::dateTime2string(new \DateTime('NOW'))); $meta->setCreated(Util::dateTime2string(new \DateTime('NOW')));
} }*/
$this->setMeta($meta); $this->setMeta($meta);
$this->setMembers(isset($data['members']) ? $data['members'] : true); if (isset($data['members'])) {
$members = [];
foreach ($data['members'] as $member) {
$scimMember = new MultiValuedAttribute();
$scimMember->setValue($member);
$members[] = $scimMember;
}
$this->setMembers($members);
} else {
$this->setMembers(null);
}
$this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null); $this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null);
} }
@ -65,17 +79,18 @@ class CoreGroup extends CommonEntity
'schemas' => [Util::GROUP_SCHEMA], 'schemas' => [Util::GROUP_SCHEMA],
'id' => $this->getId(), 'id' => $this->getId(),
'externalId' => $this->getExternalId(), 'externalId' => $this->getExternalId(),
'meta' => [ 'meta' => null !== $this->getMeta() ? [
'resourceType' => $this->getMeta()->getResourceType(), 'resourceType' => null !== $this->getMeta()->getResourceType()
'created' => $this->getMeta()->getCreated(), ? $this->getMeta()->getResourceType() : null,
'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null,
'location' => $baseLocation . '/Groups/' . $this->getId(), 'location' => $baseLocation . '/Groups/' . $this->getId(),
'version' => $this->getMeta()->getVersion() 'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null
], ] : null,
'displayName' => $this->getDisplayName(), 'displayName' => $this->getDisplayName(),
'members' => $this->getMembers() 'members' => $this->getMembers()
]; ];
if (null !== $this->getMeta()->getLastModified()) { if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) {
$data['meta']['updated'] = $this->getMeta()->getLastModified(); $data['meta']['updated'] = $this->getMeta()->getLastModified();
} }

View file

@ -299,19 +299,38 @@ class CoreUser extends CommonEntity
$this->setUserName(isset($data['userName']) ? $data['userName'] : null); $this->setUserName(isset($data['userName']) ? $data['userName'] : null);
$name = new Name(); $name = new Name();
$name->setFamilyName($data['name']['familyName']); if (isset($data['name']) && !empty($data['name'])) {
$name->setFormatted($data['name']['formatted']); if (isset($data['name']['familyName']) && !empty($data['name']['familyName'])) {
$name->setGivenName($data['name']['givenName']); $name->setFamilyName($data['name']['familyName']);
$name->setHonorificPrefix($data['name']['honorificPrefix']); }
$name->setHonorificSuffix($data['name']['honorificSuffix']);
if (isset($data['name']['formatted']) && !empty($data['name']['formatted'])) {
$name->setFormatted($data['name']['formatted']);
}
if (isset($data['name']['givenName']) && !empty($data['name']['givenName'])) {
$name->setGivenName($data['name']['givenName']);
}
if (isset($data['name']['honorificPrefix']) && !empty($data['name']['honorificPrefix'])) {
$name->setHonorificPrefix($data['name']['honorificPrefix']);
}
if (isset($data['name']['honorificSuffix']) && !empty($data['name']['honorificSuffix'])) {
$name->setHonorificSuffix($data['name']['honorificSuffix']);
}
}
$this->setName($name); $this->setName($name);
$meta = new Meta(); $meta = new Meta();
if (isset($data['meta']) && isset($data['meta']['created'])) { // This is currently commented out, since the code complains about wrongly
// formatted timestamps sometimes when fromSCIM is called
// TODO: Need to possibly refactor string2datetime and/or dateTime2string in order to fix this
/*if (isset($data['meta']) && isset($data['meta']['created'])) {
$meta->setCreated(Util::string2dateTime($data['meta']['created'])); $meta->setCreated(Util::string2dateTime($data['meta']['created']));
} else { } else {
$meta->setCreated(Util::dateTime2string(new \DateTime('NOW'))); $meta->setCreated(Util::dateTime2string(new \DateTime('NOW')));
} }*/
$this->setMeta($meta); $this->setMeta($meta);
$this->setActive(isset($data['active']) ? $data['active'] : true); $this->setActive(isset($data['active']) ? $data['active'] : true);
@ -377,7 +396,7 @@ class CoreUser extends CommonEntity
'preferredLanguage' => $this->getPreferredLanguage(), 'preferredLanguage' => $this->getPreferredLanguage(),
'locale' => $this->getLocale(), 'locale' => $this->getLocale(),
'timezone' => $this->getTimezone(), 'timezone' => $this->getTimezone(),
'active' => $this->getActive(), 'active' => boolval($this->getActive()),
'password' => $this->getPassword(), 'password' => $this->getPassword(),
'emails' => $this->getEmails(), 'emails' => $this->getEmails(),
'phoneNumbers' => $this->getPhoneNumbers(), 'phoneNumbers' => $this->getPhoneNumbers(),

View file

@ -2,136 +2,106 @@
namespace Opf\Repositories\Groups; namespace Opf\Repositories\Groups;
use Monolog\Logger;
use Opf\Models\SCIM\Standard\Groups\CoreGroup; use Opf\Models\SCIM\Standard\Groups\CoreGroup;
use Opf\Models\SCIM\Standard\Meta; use Opf\Models\SCIM\Standard\Meta;
use Opf\Repositories\Repository; use Opf\Repositories\Repository;
use Opf\Util\Filters\FilterUtil;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
class MockGroupsRepository extends Repository class MockGroupsRepository extends Repository
{ {
private $logger;
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
$this->dataAccess = $this->container->get('GroupsDataAccess'); $this->dataAccess = $this->container->get('GroupsDataAccess');
$this->adapter = $this->container->get('GroupsAdapter'); $this->adapter = $this->container->get('GroupsAdapter');
$this->logger = $this->container->get(Logger::class);
} }
public function getAll(): array public function getAll(
{ $filter = '',
$startIndex = 0,
$count = 0,
$attributes = [],
$excludedAttributes = []
): array {
// Read all mock groups from the database // Read all mock groups from the database
$mockGroups = $this->dataAccess::all(); $mockGroups = $this->dataAccess->getAll();
$scimGroups = []; $scimGroups = [];
// Transform each mock group to a SCIM group via the injected adapter
foreach ($mockGroups as $mockGroup) { foreach ($mockGroups as $mockGroup) {
$this->adapter->setGroup($mockGroup); $scimGroup = $this->adapter->getCoreGroup($mockGroup);
$scimGroup = new CoreGroup();
$scimGroup->setId($this->adapter->getId());
$scimGroup->setDisplayName($this->adapter->getDisplayName());
$scimGroup->setMembers($this->adapter->getMembers());
$scimGroupMeta = new Meta();
$scimGroupMeta->setCreated($this->adapter->getCreatedAt());
$scimGroup->setMeta($scimGroupMeta);
$scimGroups[] = $scimGroup; $scimGroups[] = $scimGroup;
} }
if (isset($filter) && !empty($filter)) {
$scimGroupsToFilter = [];
foreach ($scimGroups as $scimGroup) {
$scimGroupsToFilter[] = $scimGroup->toSCIM(false);
}
$filteredScimData = FilterUtil::performFiltering($filter, $scimGroupsToFilter);
$scimGroups = [];
foreach ($filteredScimData as $filteredScimGroup) {
$scimGroup = new CoreGroup();
$scimGroup->fromSCIM($filteredScimGroup);
$scimGroups[] = $scimGroup;
}
return $scimGroups;
}
return $scimGroups; return $scimGroups;
} }
public function getOneById(string $id): ?CoreGroup public function getOneById(
{ string $id,
if (isset($id) && !empty($id)) { $filter = '',
$mockGroup = $this->dataAccess::find($id); $startIndex = 0,
$count = 0,
if (isset($mockGroup) && !empty($mockGroup)) { $attributes = [],
$this->adapter->setGroup($mockGroup); $excludedAttributes = []
): ?CoreGroup {
$scimGroup = new CoreGroup(); $mockGroup = $this->dataAccess->getOneById($id);
$scimGroup->setId($this->adapter->getId()); return $this->adapter->getCoreGroup($mockGroup);
$scimGroup->setDisplayName($this->adapter->getDisplayName());
$scimGroup->setMembers($this->adapter->getMembers());
$scimGroupMeta = new Meta();
$scimGroupMeta->setCreated($this->adapter->getCreatedAt());
$scimGroup->setMeta($scimGroupMeta);
return $scimGroup;
} else {
return null;
}
} else {
return null;
}
} }
public function create($object): ?CoreGroup public function create($object): ?CoreGroup
{ {
if (isset($object) && !empty($object)) { $scimGroupToCreate = new CoreGroup();
$scimGroup = new CoreGroup(); $scimGroupToCreate->fromSCIM($object);
$scimGroup->fromSCIM($object);
$this->adapter->setGroup($this->dataAccess); $mockGroupToCreate = $this->adapter->getMockGroup($scimGroupToCreate);
$this->adapter->setId($scimGroup->getId()); $mockGroupCreated = $this->dataAccess->create($mockGroupToCreate);
$this->adapter->setDisplayName($scimGroup->getDisplayName());
$this->adapter->setMembers($scimGroup->getMembers());
$this->adapter->setCreatedAt($scimGroup->getMeta()->getCreated());
$this->dataAccess = $this->adapter->getGroup(); if (isset($mockGroupCreated)) {
if ($this->dataAccess->save()) { return $this->adapter->getCoreGroup($mockGroupCreated);
return $scimGroup;
} else {
return null;
}
} }
return null;
} }
public function update(string $id, $object): ?CoreGroup public function update(string $id, $object): ?CoreGroup
{ {
if (isset($id) && !empty($id)) { $scimGroupToUpdate = new CoreGroup();
$mockGroup = $this->dataAccess::find($id); $scimGroupToUpdate->fromSCIM($object);
if (isset($mockGroup) && !empty($mockGroup)) {
$scimGroup = new CoreGroup();
$scimGroup->fromSCIM($object);
$this->adapter->setGroup($mockGroup); $mockGroupToUpdate = $this->adapter->getMockGroup($scimGroupToUpdate);
$scimGroup->setId($this->adapter->getId()); $mockGroupUpdated = $this->dataAccess->update($id, $mockGroupToUpdate);
$this->adapter->setDisplayName($scimGroup->getDisplayName()); if (isset($mockGroupUpdated)) {
$this->adapter->setMembers($scimGroup->getMembers()); return $this->adapter->getCoreGroup($mockGroupUpdated);
$this->adapter->setCreatedAt($scimGroup->getMeta()->getCreated());
$mockGroup = $this->adapter->getGroup();
if ($mockGroup->save()) {
return $scimGroup;
} else {
return null;
}
} else {
return null;
}
} else {
return null;
} }
return null;
} }
public function delete(string $id): bool public function delete(string $id): bool
{ {
if (isset($id) && !empty($id)) { return $this->dataAccess->delete($id);
$mockGroup = $this->dataAccess::find($id);
if (!isset($mockGroup) || empty($mockGroup)) {
return false;
}
$mockGroup->delete();
return true;
} else {
return false;
}
} }
} }

View file

@ -15,9 +15,68 @@ abstract class Repository
$this->container = $container; $this->container = $container;
} }
abstract public function getAll(): array; /**
abstract public function getOneById(string $id): ?object; * @param string $filter Optional parameter which contains a filter expression
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
*
* @param int $startIndex Optional parameter for specifying the start index, used for pagination
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4
*
* @param int $count Optional parameter for specifying the number of results, used for pagination
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4
*
* @param array $attributes Optional parameter for including only specific attributes in the response
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5
* (if $exludedAttributes is not empty, this should be empty)
*
* @param array $excludedAttributes Optional parameter for excluding specific attributes from the response
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5
* (if $attributes is not empty, this should be empty)
*
* @return array An array of SCIM resources
*/
abstract public function getAll(
$filter = '',
$startIndex = 0,
$count = 0,
$attributes = [],
$excludedAttributes = []
): array;
/**
* @param string $id Required parameter which contains of a given entity that should be retrieved
*
* @param string $filter Optional parameter which contains a filter expression
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
*
* @param int $startIndex Optional parameter for specifying the start index, used for pagination
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4
*
* @param int $count Optional parameter for specifying the number of results, used for pagination
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4
*
* @param array $attributes Optional parameter for including only specific attributes in the response
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5
* (if $exludedAttributes is not empty, this should be empty)
*
* @param array $excludedAttributes Optional parameter for excluding specific attributes from the response
* as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5
* (if $attributes is not empty, this should be empty)
*
* @return object|null A SCIM resource or null if no resource found
*/
abstract public function getOneById(
string $id,
$filter = '',
$startIndex = 0,
$count = 0,
$attributes = [],
$excludedAttributes = []
): ?object;
abstract public function create($object): ?object; abstract public function create($object): ?object;
abstract public function update(string $id, $object): ?object; abstract public function update(string $id, $object): ?object;
abstract public function delete(string $id): bool; abstract public function delete(string $id): bool;
} }

View file

@ -2,190 +2,120 @@
namespace Opf\Repositories\Users; namespace Opf\Repositories\Users;
use Monolog\Logger;
use Opf\Models\SCIM\Standard\Users\CoreUser; use Opf\Models\SCIM\Standard\Users\CoreUser;
use Opf\Models\SCIM\Standard\Meta; use Opf\Models\SCIM\Standard\Meta;
use Opf\Repositories\Repository; use Opf\Repositories\Repository;
use Opf\Util\Filters\FilterUtil;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
class MockUsersRepository extends Repository class MockUsersRepository extends Repository
{ {
private $logger;
public function __construct(ContainerInterface $container) public function __construct(ContainerInterface $container)
{ {
parent::__construct($container); parent::__construct($container);
$this->dataAccess = $this->container->get('UsersDataAccess'); $this->dataAccess = $this->container->get('UsersDataAccess');
$this->adapter = $this->container->get('UsersAdapter'); $this->adapter = $this->container->get('UsersAdapter');
$this->logger = $this->container->get(Logger::class);
} }
public function getAll(): array
{ public function getAll(
$filter = '',
$startIndex = 0,
$count = 0,
$attributes = [],
$excludedAttributes = []
): array {
// Read all mock users from the database // Read all mock users from the database
$mockUsers = $this->dataAccess::all(); $mockUsers = $this->dataAccess->getAll();
$scimUsers = []; $scimUsers = [];
// Transform each mock user to a SCIM user via the injected adapter
foreach ($mockUsers as $mockUser) { foreach ($mockUsers as $mockUser) {
// TODO: Possibly refactor the transformation logic between SCIM users and other users $scimUser = $this->adapter->getCoreUser($mockUser);
// in a separate method or class, since it seems to be rather repetitive
$this->adapter->setUser($mockUser);
$scimUser = new CoreUser();
$scimUser->setId($this->adapter->getId());
$scimUser->setUserName($this->adapter->getUserName());
$scimUserMeta = new Meta();
$scimUserMeta->setCreated($this->adapter->getCreatedAt());
$scimUser->setMeta($scimUserMeta);
$scimUser->setActive($this->adapter->getActive());
$scimUser->setExternalId($this->adapter->getExternalId());
$scimUser->setProfileUrl($this->adapter->getProfileUrl());
$scimUsers[] = $scimUser; $scimUsers[] = $scimUser;
} }
if (isset($filter) && !empty($filter)) {
$scimUsersToFilter = [];
foreach ($scimUsers as $scimUser) {
$scimUsersToFilter[] = $scimUser->toSCIM(false);
}
$filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter);
$scimUsers = [];
foreach ($filteredScimData as $filteredScimUser) {
$scimUser = new CoreUser();
$scimUser->fromSCIM($filteredScimUser);
$scimUsers[] = $scimUser;
}
return $scimUsers;
}
return $scimUsers; return $scimUsers;
} }
public function getOneByUserName(string $userName): ?CoreUser public function getOneById(
{ string $id,
if (isset($userName) && !empty($userName)) { $filter = '',
// Try to find the first user from the database with the supplied username $startIndex = 0,
$mockUser = $this->dataAccess::where('userName', $userName)->first(); $count = 0,
$attributes = [],
$excludedAttributes = []
): ?CoreUser {
$mockUser = $this->dataAccess->getOneById($id);
$scimUser = $this->adapter->getCoreUser($mockUser);
// If such a user exists, map it to a SCIM user and return the SCIM user if (isset($filter) && !empty($filter)) {
if (isset($mockUser) && !empty($mockUser)) { // Pass the single user as an array of an array, representing the user
$this->adapter->setUser($mockUser); $scimUsersToFilter = array($scimUser->toSCIM(false));
$filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter);
if (!empty($filteredScimData)) {
$scimUser = new CoreUser(); $scimUser = new CoreUser();
$scimUser->setId($this->adapter->getId()); $scimUser->fromSCIM($filteredScimData[0]);
$scimUser->setUserName($this->adapter->getUserName());
$scimUserMeta = new Meta();
$scimUserMeta->setCreated($this->adapter->getCreatedAt());
$scimUser->setMeta($scimUserMeta);
$scimUser->setActive($this->adapter->getActive());
$scimUser->setExternalId($this->adapter->getExternalId());
$scimUser->setProfileUrl($this->adapter->getProfileUrl());
return $scimUser; return $scimUser;
} else {
return null;
} }
} }
}
public function getOneById(string $id): ?CoreUser return $scimUser;
{
if (isset($id) && !empty($id)) {
// Try to find a user from the database with the supplied ID
$mockUser = $this->dataAccess::find($id);
// If there's such a user, transform it to a SCIM user and return the SCIM user
if (isset($mockUser) && !empty($mockUser)) {
$this->adapter->setUser($mockUser);
$scimUser = new CoreUser();
$scimUser->setId($this->adapter->getId());
$scimUser->setUserName($this->adapter->getUserName());
$scimUserMeta = new Meta();
$scimUserMeta->setCreated($this->adapter->getCreatedAt());
$scimUser->setMeta($scimUserMeta);
$scimUser->setActive($this->adapter->getActive());
$scimUser->setExternalId($this->adapter->getExternalId());
$scimUser->setProfileUrl($this->adapter->getProfileUrl());
return $scimUser;
} else {
return null;
}
} else {
return null;
}
} }
public function create($object): ?CoreUser public function create($object): ?CoreUser
{ {
if (isset($object) && !empty($object)) { $scimUserToCreate = new CoreUser();
// Transform the incoming JSON user object to a SCIM object $scimUserToCreate->fromSCIM($object);
// Then transform the SCIM object to a mock user that can be stored in the database
$scimUser = new CoreUser();
$scimUser->fromSCIM($object);
// $this->dataAccess represents an instance of the MockUser ORM model $mockUserToCreate = $this->adapter->getMockUser($scimUserToCreate);
// that we use for user storage to and retrieval from SQLite
$this->adapter->setUser($this->dataAccess);
$this->adapter->setId($scimUser->getId()); $mockUserCreated = $this->dataAccess->create($mockUserToCreate);
$this->adapter->setUserName($scimUser->getUserName());
$this->adapter->setCreatedAt($scimUser->getMeta()->getCreated());
$this->adapter->setActive($scimUser->getActive());
$this->adapter->setExternalId($scimUser->getExternalId());
$this->adapter->setProfileUrl($scimUser->getProfileUrl());
// Obtain the transformed mock user from the adapter and try to save it to the database if (isset($mockUserCreated)) {
$this->dataAccess = $this->adapter->getUser(); return $this->adapter->getCoreUser($mockUserCreated);
if ($this->dataAccess->save()) {
return $scimUser;
} else {
return null;
}
} }
return null;
} }
public function update(string $id, $object): ?CoreUser public function update(string $id, $object): ?CoreUser
{ {
if (isset($id) && !empty($id)) { $scimUserToUpdate = new CoreUser();
// Try to find the user with the supplied ID $scimUserToUpdate->fromSCIM($object);
$mockUser = $this->dataAccess::find($id);
if (isset($mockUser) && !empty($mockUser)) {
// Transform the received JSON user object to a SCIM object
$scimUser = new CoreUser();
$scimUser->fromSCIM($object);
// Set the adapter's internal user object to the found user from the database $mockUserToUpdate = $this->adapter->getMockUser($scimUserToUpdate);
$this->adapter->setUser($mockUser);
// Set the SCIM user's ID to be the same as the ID of the found user from the database $mockUserUpdated = $this->dataAccess->update($id, $mockUserToUpdate);
// Otherwise, we might lose the ID if a new one is supplied in the request
$scimUser->setId($this->adapter->getId());
// Transform the SCIM object to a mock user via the adapter and replace if (isset($mockUserUpdated)) {
// any properties of the mock user with the new properties incoming via the SCIM object return $this->adapter->getCoreUser($mockUserUpdated);
$this->adapter->setUserName($scimUser->getUserName());
$this->adapter->setCreatedAt($scimUser->getMeta()->getCreated());
$this->adapter->setActive($scimUser->getActive());
$this->adapter->setExternalId($scimUser->getExternalId());
$this->adapter->setProfileUrl($scimUser->getProfileUrl());
// Obtain the updated mock user via the adapter and try to save it to the database
$mockUser = $this->adapter->getUser();
if ($mockUser->save()) {
return $scimUser;
} else {
return null;
}
} else {
return null;
}
} else {
return null;
} }
return null;
} }
public function delete(string $id): bool public function delete(string $id): bool
{ {
if (isset($id) && !empty($id)) { return $this->dataAccess->delete($id);
// Try to find the user to be deleted
$mockUser = $this->dataAccess::find($id);
if (!isset($mockUser) || empty($mockUser)) {
return false;
}
$mockUser->delete();
return true;
} else {
return false;
}
} }
} }

View file

@ -1,78 +0,0 @@
<?php
namespace Opf\Repositories\Users;
use Opf\Models\SCIM\Custom\Users\ProvisioningUser;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
use Opf\Repositories\Repository;
use Opf\Util\Util;
use Psr\Container\ContainerInterface;
use Monolog\Logger;
class PfaUsersRepository extends Repository
{
private $logger;
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->dataAccess = $this->container->get('UsersDataAccess');
$this->adapter = $this->container->get('UsersAdapter');
$this->logger = $this->container->get(Logger::class);
}
public function getAll(): array
{
$pfaUsers = $this->dataAccess->getAll();
$scimUsers = [];
foreach ($pfaUsers as $pfaUser) {
$scimUser = $this->adapter->getProvisioningUser($pfaUser);
$scimUsers[] = $scimUser;
}
return $scimUsers;
}
public function getOneById(string $id): ?ProvisioningUser
{
$pfaUser = $this->dataAccess->getOneById($id);
return $this->adapter->getProvisioningUser($pfaUser);
}
public function create($object): ?ProvisioningUser
{
$scimUserToCreate = new ProvisioningUser();
$scimUserToCreate->fromSCIM($object);
$pfaUserToCreate = $this->adapter->getPfaUser($scimUserToCreate);
$pfaUserCreated = $this->dataAccess->create($pfaUserToCreate);
if (isset($pfaUserCreated)) {
return $this->adapter->getProvisioningUser($pfaUserCreated);
}
return null;
}
public function update(string $id, $object): ?ProvisioningUser
{
$scimUserToUpdate = new ProvisioningUser();
$scimUserToUpdate->fromSCIM($object);
$pfaUserToUpdate = $this->adapter->getPfaUser($scimUserToUpdate);
$pfaUserUpdated = $this->dataAccess->update($id, $pfaUserToUpdate);
if (isset($pfaUserUpdated)) {
return $this->adapter->getProvisioningUser($pfaUserUpdated);
}
return null;
}
public function delete(string $id): bool
{
return $this->dataAccess->delete($id);
}
}

156
src/ScimServer.php Normal file
View file

@ -0,0 +1,156 @@
<?php
namespace Opf;
use DI\ContainerBuilder;
use Exception;
use Opf\Handlers\HttpErrorHandler;
use Opf\Util\Util;
use Slim\App;
use Slim\Factory\AppFactory;
class ScimServer
{
/**
* @var string $scimServerPhpRoot The root of the project using
* OPF as a dependency. This is needed for autoloading purposes,
* such that all classes of the project using OPF can be made
* visible to OPF.
*/
private string $scimServerPhpRoot;
/**
* A container builder which is used to configure and create a
* DI container, used by the Slim application
*/
private ContainerBuilder $containerBuilder;
/**
* The Slim application to configure and run as a server, exposing
* the SCIM API
*/
private App $app;
/**
* @var array $dependencies An array holding dependency definitions,
* passed to the DI ContainerBuilder for configuring the DI container
*/
private array $dependencies;
/**
* @var array $middleware Any custom middleware that needs to be added
* to the Slim application (e.g., custom auth middleware)
*/
private array $middleware;
/**
* ScimServer class constructor
*
* @param string $scimServerPhpRoot The root of the project using
* the OPF library. Needed for autoloading. See more in description
* of the dedicated class property of the same name
*/
public function __construct(string $scimServerPhpRoot)
{
$this->scimServerPhpRoot = $scimServerPhpRoot;
/**
* Once we have the root directory of the project that's using
* OPF, we include its autoload file, so that we don't run into
* autoloading issues.
*/
require $this->scimServerPhpRoot . '/vendor/autoload.php';
}
public function setConfig(string $configFilePath)
{
if (!isset($configFilePath) || empty($configFilePath)) {
throw new Exception("Config file path must be supplied");
}
Util::setConfigFile($configFilePath);
}
public function setDependencies(array $dependencies = array())
{
$baseDependencies = require __DIR__ . '/Dependencies/dependencies.php';
$this->dependencies = array_merge($baseDependencies, $dependencies);
}
public function setMiddleware(array $middleware = array())
{
$this->middleware = $middleware;
}
public function run()
{
session_start();
// Instantiate the PHP-DI ContainerBuilder
$containerBuilder = new ContainerBuilder();
$config = Util::getConfigFile();
if ($config['isInProduction']) {
$containerBuilder->enableCompilation(__DIR__ . '/../var/cache');
}
// Set up a few Slim-related settings
$settings = [
'settings' => [
'determineRouteBeforeAppMiddleware' => false,
'displayErrorDetails' => true, // set to false in production
'addContentLengthHeader' => false, // Allow the web server to send the content-length header
]
];
$containerBuilder->addDefinitions($settings);
// Set all necessary dependencies which are provided in this class'
// $dependencies attribute
$containerBuilder->addDefinitions($this->dependencies);
// Build PHP-DI Container instance
$container = $containerBuilder->build();
// Instantiate the app
AppFactory::setContainer($container);
$this->app = AppFactory::create();
// Set our app's base path if it's configured
if (isset($config['basePath']) && !empty($config['basePath'])) {
$this->app->setBasePath($config['basePath']);
}
// Register routes
$routes = require __DIR__ . '/routes.php';
$routes($this->app);
// Iterate through the custom middleware (if any) and set it
if (isset($this->middleware) && !empty($this->middleware)) {
foreach ($this->middleware as $middleware) {
$this->app->addMiddleware($this->app->getContainer()->get($middleware));
}
}
// Add Routing Middleware
$this->app->addRoutingMiddleware();
$this->app->addBodyParsingMiddleware();
$callableResolver = $this->app->getCallableResolver();
$responseFactory = $this->app->getResponseFactory();
// Instantiate our custom Http error handler that we need further down below
$errorHandler = new HttpErrorHandler($callableResolver, $responseFactory);
// Add error middleware
$errorMiddleware = $this->app->addErrorMiddleware(
$config['isInProduction'] ? false : true,
true,
true
);
$errorMiddleware->setDefaultErrorHandler($errorHandler);
// Run app
$this->app->run();
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Opf\Util\Authentication;
interface AuthenticatorInterface
{
/**
* A method for performing authentication
*
* @param string $credentials The authentication credentials
* @param array $authorizationInfo Array with infor to check if user is authorized to perform a given operation
*
* @return bool Return true if authentication succeeded, otherwise false
*/
public function authenticate(string $credentials, array $authorizationInfo): bool;
}

View file

@ -0,0 +1,34 @@
<?php
namespace Opf\Util\Authentication;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Opf\Util\Util;
use Psr\Container\ContainerInterface;
class SimpleBearerAuthenticator implements AuthenticatorInterface
{
/** @var \Monolog\Logger */
private $logger;
public function __construct(ContainerInterface $container)
{
$this->logger = $container->get(\Monolog\Logger::class);
}
public function authenticate(string $credentials, array $authorizationInfo): bool
{
$jwtPayload = [];
$jwtSecret = Util::getConfigFile()['jwt']['secret'];
try {
$jwtPayload = (array) JWT::decode($credentials, new Key($jwtSecret, 'HS256'));
} catch (Exception $e) {
// If we land here, something was wrong with the JWT and auth has thus failed
$this->logger->error($e->getMessage());
return false;
}
return true;
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace Opf\Util\Filters;
use Opf\Models\SCIM\Standard\Filters\AttributeExpression;
use Opf\Models\SCIM\Standard\Filters\FilterException;
use Opf\Models\SCIM\Standard\Filters\FilterExpression;
/**
* A parser for SCIM filter expressions
*
* Note: currently this parser is very simplistic and directly tries to parse a string, representing a filter expession.
* For now only attribute expression are supported: see https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
* In later iterations, it could be changed, such that it uses a lexer for lexical analysis first and then performs
* syntactic analysis, based on the lexer's output.
*/
class FilterParser
{
public static function parseFilterExpression(string $filterExpression): FilterExpression
{
if (!isset($filterExpression) || empty($filterExpression) || !is_string($filterExpression)) {
throw new FilterException(
"Invalid filter expression passed for parsing: expression was either null, empty or not a string"
);
}
$splitFilterExpression = explode(" ", $filterExpression);
if (count($splitFilterExpression) < 2 || count($splitFilterExpression) > 3) {
throw new FilterException("Incorrectly formatted AttributeExpression");
}
$attributeExpression = new AttributeExpression(
$splitFilterExpression[0],
$splitFilterExpression[1],
$splitFilterExpression[2]
);
return $attributeExpression;
}
}

View file

@ -0,0 +1,398 @@
<?php
namespace Opf\Util\Filters;
use Exception;
use Opf\Models\SCIM\Standard\Filters\AttributeExpression;
use Opf\Models\SCIM\Standard\Filters\AttributeOperator;
use Opf\Models\SCIM\Standard\Filters\FilterException;
class FilterUtil
{
/**
* @param string $filterExpression The SCIM filter expression that is to be parsed and applied
* @param array $scimData An array of SCIM objects (each represented as an array) that is to be filtered
*
* @return array Array of filtered SCIM objects (again each represented as an array)
*/
public static function performFiltering(string $filterExpression, array $scimData): array
{
try {
$parsedFilterExpression = FilterParser::parseFilterExpression($filterExpression);
} catch (Exception $e) {
throw $e;
}
$filteredScimData = [];
// Call appropriate filtering method
// For now, we only have a method for filtering, based on AttributeExpression
switch (true) {
case $parsedFilterExpression instanceof AttributeExpression:
$filteredScimData = self::filterWithAttributeExpression($parsedFilterExpression, $scimData);
break;
default:
throw new FilterException("Unknown filter expression type");
break;
}
return $filteredScimData;
}
private static function filterWithAttributeExpression(
AttributeExpression $attributeExpression,
array $scimData
): array {
// In case of null or an empty array, we return an empty array, since we have nothing to filter
if (!isset($scimData) || empty($scimData)) {
return [];
}
$filteredScimData = [];
$attributePath = $attributeExpression->getAttributePath();
$compareOperator = $attributeExpression->getCompareOperator();
$comparisonValue = $attributeExpression->getComparisonValue();
// If we have a nested attribute (i.e., an attribute with subattribute(s)),
// then we split the attribute path by "." and store all the single parts
// in an array
// On the other hand (i.e., no nested attribute), we store the attribute path
// in an array which only contains the attribute path as a single element
if (strpos($attributePath, ".") !== false) {
$attributePath = explode(".", $attributePath);
} else {
$attributePath = array($attributePath);
}
// Decide which filter function to call, based on the comparison operator
$filterFunctionToUse = null;
switch ($compareOperator) {
case AttributeOperator::OP_EQ:
$filterFunctionToUse = "stringEqualsFilter";
break;
case AttributeOperator::OP_NE:
$filterFunctionToUse = "stringNotEqualsFilter";
break;
case AttributeOperator::OP_CO:
$filterFunctionToUse = "stringContainsFilter";
break;
case AttributeOperator::OP_SW:
$filterFunctionToUse = "stringStartsWithFilter";
break;
case AttributeOperator::OP_EW:
$filterFunctionToUse = "stringEndsWithFilter";
break;
case AttributeOperator::OP_PR:
if (isset($comparisonValue) && !empty($comparisonValue)) {
throw new FilterException("\"pr\" filter operator must be used without a comparison value");
break;
} else {
$filterFunctionToUse = "hasValueFilter";
break;
}
case AttributeOperator::OP_GT:
$filterFunctionToUse = "greaterThanFilter";
break;
case AttributeOperator::OP_GE:
$filterFunctionToUse = "greaterThanOrEqualToFilter";
break;
case AttributeOperator::OP_LT:
$filterFunctionToUse = "lessThanFilter";
break;
case AttributeOperator::OP_LE:
$filterFunctionToUse = "lessThanOrEqualToFilter";
break;
default:
throw new FilterException("Unknown comparison operator found");
break;
}
// Put the function to call in a variable that we can use for calls below
$filterFunctionToUse = array(FilterUtil::class, $filterFunctionToUse);
foreach ($scimData as $scimObject) {
// Obtain the attribute of the SCIM objects that we want to filter for
$attribute = $scimObject;
foreach ($attributePath as $attributePathComponent) {
if (array_key_exists($attributePathComponent, $attribute)) {
$attribute = $attribute[$attributePathComponent];
} else {
throw new FilterException(
"Attribute " . $attributePathComponent .
" to filter by is undefined for the given SCIM resources"
);
}
}
$filterResult = false;
// If the filter function to call is "hasValueFilter", we need to pass it only the attribute
if (strcmp($filterFunctionToUse[1], "hasValueFilter") === 0) {
$filterResult = $filterFunctionToUse($attribute);
} else {
$filterResult = $filterFunctionToUse($attribute, $comparisonValue);
}
if ($filterResult) {
$filteredScimData[] = $scimObject;
}
}
return $filteredScimData;
}
private static function stringEqualsFilter($attribute, $comparisonValue)
{
if (strcmp($attribute, $comparisonValue) === 0) {
return true;
}
return false;
}
private static function stringNotEqualsFilter($attribute, $comparisonValue)
{
if (strcmp($attribute, $comparisonValue) !== 0) {
return true;
}
return false;
}
private static function stringContainsFilter($attribute, $comparisonValue)
{
if (strpos($attribute, $comparisonValue) !== false) {
return true;
}
return false;
}
private static function stringStartsWithFilter($attribute, $comparisonValue)
{
if (substr($attribute, 0, strlen($comparisonValue)) === $comparisonValue) {
return true;
}
return false;
}
private static function stringEndsWithFilter($attribute, $comparisonValue)
{
if (substr($attribute, -strlen($comparisonValue)) === $comparisonValue) {
return true;
}
return false;
}
private static function hasValueFilter($attribute)
{
if (isset($attribute) && !empty($attribute)) {
return true;
}
return false;
}
private static function greaterThanFilter($attribute, $comparisonValue)
{
$comparisonValueType = gettype($comparisonValue);
$attributeType = gettype($attribute);
// First, make sure that the attribute and the comparison value have the same type
if (strcmp($attributeType, $comparisonValueType) !== 0) {
throw new FilterException(
"\"gt\" filter operator requires the attribute and the comparison value to be of the same type"
);
}
switch ($attributeType) {
case "string":
// Try to parse string to date to see if we need to compare timestamps
if (
strtotime($attribute) !== false
&& strtotime($comparisonValue) !== false
) {
if (strtotime($attribute) > strtotime($comparisonValue)) {
return true;
}
} else { // If not date, but just regular string, then perform lexicographic comparison
if (strcasecmp($attribute, $comparisonValue) > 0) {
return true;
}
}
break;
case "integer":
if ($attribute > $comparisonValue) {
return true;
}
break;
// For any other data type, throw an exception
// TODO: Return 400 with "scimType" of "invalidFilter" as per
// https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
default:
throw new FilterException("Unsupported type for \"gt\" operation");
break;
}
return false;
}
private static function greaterThanOrEqualToFilter($attribute, $comparisonValue)
{
$comparisonValueType = gettype($comparisonValue);
$attributeType = gettype($attribute);
// First, make sure that the attribute and the comparison value have the same type
if (strcmp($attributeType, $comparisonValueType) !== 0) {
throw new FilterException(
"\"ge\" filter operator requires the attribute and the comparison value to be of the same type"
);
}
switch ($attributeType) {
case "string":
// Try to parse string to date to see if we need to compare timestamps
if (
strtotime($attribute) !== false
&& strtotime($comparisonValue) !== false
) {
if (strtotime($attribute) >= strtotime($comparisonValue)) {
return true;
}
} else { // If not date, but just regular string, then perform lexicographic comparison
if (strcasecmp($attribute, $comparisonValue) >= 0) {
return true;
}
}
break;
case "integer":
if ($attribute >= $comparisonValue) {
return true;
}
break;
// For any other data type, throw an exception
// TODO: Return 400 with "scimType" of "invalidFilter" as per
// https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
default:
throw new FilterException("Unsupported type for \"ge\" operation");
break;
}
return false;
}
private static function lessThanFilter($attribute, $comparisonValue)
{
$comparisonValueType = gettype($comparisonValue);
$attributeType = gettype($attribute);
// First, make sure that the attribute and the comparison value have the same type
if (strcmp($attributeType, $comparisonValueType) !== 0) {
throw new FilterException(
"\"lt\" filter operator requires the attribute and the comparison value to be of the same type"
);
}
switch ($attributeType) {
case "string":
// Try to parse string to date to see if we need to compare timestamps
if (
strtotime($attribute) !== false
&& strtotime($comparisonValue) !== false
) {
if (strtotime($attribute) < strtotime($comparisonValue)) {
return true;
}
} else { // If not date, but just regular string, then perform lexicographic comparison
if (strcasecmp($attribute, $comparisonValue) < 0) {
return true;
}
}
break;
case "integer":
if ($attribute < $comparisonValue) {
return true;
}
break;
// For any other data type, throw an exception
// TODO: Return 400 with "scimType" of "invalidFilter" as per
// https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
default:
throw new FilterException("Unsupported type for \"lt\" operation");
break;
}
return false;
}
private static function lessThanOrEqualToFilter($attribute, $comparisonValue)
{
$comparisonValueType = gettype($comparisonValue);
$attributeType = gettype($attribute);
// First, make sure that the attribute and the comparison value have the same type
if (strcmp($attributeType, $comparisonValueType) !== 0) {
throw new FilterException(
"\"le\" filter operator requires the attribute and the comparison value to be of the same type"
);
}
switch ($attributeType) {
case "string":
// Try to parse string to date to see if we need to compare timestamps
if (
strtotime($attribute) !== false
&& strtotime($comparisonValue) !== false
) {
if (strtotime($attribute) <= strtotime($comparisonValue)) {
return true;
}
} else { // If not date, but just regular string, then perform lexicographic comparison
if (strcasecmp($attribute, $comparisonValue) <= 0) {
return true;
}
}
break;
case "integer":
if ($attribute <= $comparisonValue) {
return true;
}
break;
// For any other data type, throw an exception
// TODO: Return 400 with "scimType" of "invalidFilter" as per
// https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2
default:
throw new FilterException("Unsupported type for \"le\" operation");
break;
}
return false;
}
}

View file

@ -2,11 +2,20 @@
namespace Opf\Util; namespace Opf\Util;
use Opf\Models\SCIM\Standard\Service\CoreResourceType;
use Opf\Models\SCIM\Standard\Service\CoreSchemaExtension;
use Exception;
use PDO;
abstract class Util abstract class Util
{ {
private static string $defaultConfigFilePath = __DIR__ . '/../../config/config.default.php';
private static string $customConfigFilePath;
public const USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"; public const USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
public const ENTERPRISE_USER_SCHEMA = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"; public const ENTERPRISE_USER_SCHEMA = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
public const PROVISIONING_USER_SCHEMA = "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User"; public const PROVISIONING_USER_SCHEMA = "urn:ietf:params:scim:schemas:extension:audriga:provisioning:2.0:User";
public const DOMAIN_SCHEMA = "urn:ietf:params:scim:schemas:audriga:2.0:Domain";
public const GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"; public const GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
public const RESOURCE_TYPE_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:ResourceType"; public const RESOURCE_TYPE_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:ResourceType";
public const SERVICE_PROVIDER_CONFIGURATION_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"; public const SERVICE_PROVIDER_CONFIGURATION_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig";
@ -104,12 +113,20 @@ abstract class Util
isset($config['db']['driver']) && !empty($config['db']['driver']) isset($config['db']['driver']) && !empty($config['db']['driver'])
&& isset($config['db']['host']) && !empty($config['db']['host']) && isset($config['db']['host']) && !empty($config['db']['host'])
&& isset($config['db']['port']) && !empty($config['db']['port']) && isset($config['db']['port']) && !empty($config['db']['port'])
&& isset($config['db']['database']) && !empty($config['db']['database']) && isset($config['db']['database']) && !empty($config['db']['database']
&& strcmp($config['db']['driver'], 'mysql') === 0)
) { ) {
return $config['db']['driver'] . ':host=' return $config['db']['driver'] . ':host='
. $config['db']['host'] . ';port=' . $config['db']['host'] . ';port='
. $config['db']['port'] . ';dbname=' . $config['db']['port'] . ';dbname='
. $config['db']['database']; . $config['db']['database'];
} elseif (
isset($config['db']['driver']) && !empty($config['db']['driver'])
&& isset($config['db']['databaseFile']) && !empty($config['db']['databaseFile']
&& strcmp($config['db']['driver'], 'sqlite') === 0)
) {
return $config['db']['driver'] . ':host='
. '../../' . $config['db']['databaseFile'];
} }
} }
} }
@ -119,6 +136,55 @@ abstract class Util
return null; return null;
} }
/**
* Utility method for providing a DB connection via PDO
*
* @throws Exception if there was an issue with obtaining the DB connection
* @return PDO A PDO object representing the DB connection
*/
public static function getDbConnection()
{
// Try to obtain a DSN and complain with an Exception if there's no DSN
$dsn = self::buildDbDsn();
if (!isset($dsn)) {
throw new Exception("Can't obtain DSN to connect to DB");
}
$config = self::getConfigFile();
if (isset($config) && !empty($config)) {
if (isset($config['db']) && !empty($config['db'])) {
if (
isset($config['db']['user'])
&& !empty($config['db']['user'])
&& isset($config['db']['password'])
&& !empty($config['db']['password'])
) {
$dbUsername = $config['db']['user'];
$dbPassword = $config['db']['password'];
} else {
// If no DB username and/or password provided, throw an Exception
throw new Exception("No DB username and/or password provided to connect to DB");
}
}
}
// Create the DB connection with PDO
try {
$dbConnection = new PDO($dsn, $dbUsername, $dbPassword);
} catch (Exception $e) {
throw $e;
}
// Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations
if (isset($config['isInProduction'])) {
if ($config['isInProduction'] === false) {
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
}
return $dbConnection;
}
public static function getDomainFromEmail($email) public static function getDomainFromEmail($email)
{ {
$parts = explode("@", $email); $parts = explode("@", $email);
@ -146,18 +212,147 @@ abstract class Util
*/ */
public static function getConfigFile() public static function getConfigFile()
{ {
$defaultConfigFilePath = dirname(__DIR__) . '/../config/config.default.php';
$customConfigFilePath = dirname(__DIR__) . '/../config/config.php';
$config = []; $config = [];
// In case we don't have a custom config, we just rely on the default one // In case we don't have a custom config, we just rely on the default one
if (!file_exists($customConfigFilePath)) { if (!file_exists(self::$customConfigFilePath)) {
$config = require($defaultConfigFilePath); $config = require(self::$defaultConfigFilePath);
} else { } else {
$config = require($customConfigFilePath); $config = require(self::$customConfigFilePath);
} }
return $config; return $config;
} }
public static function setConfigFile(string $configFilePath)
{
self::$customConfigFilePath = $configFilePath;
}
/**
* A utility method for obtaining the supported SCIM resource types
*
* @param string $baseUrl A base URL required for each resource type that is returned
*
* @return array The array containing the resource types
*/
public static function getResourceTypes($baseUrl)
{
// Check which resource types are supported via the config file and in this method further down below
// make sure to only return those that are indeed supported
$config = Util::getConfigFile();
$supportedResourceTypes = $config['supportedResourceTypes'];
$scimResourceTypes = [];
if (in_array('User', $supportedResourceTypes)) {
$userResourceType = new CoreResourceType();
$userResourceType->setId("User");
$userResourceType->setName("User");
$userResourceType->setEndpoint("/Users");
$userResourceType->setDescription("User Account");
$userResourceType->setSchema(Util::USER_SCHEMA);
if (in_array('EnterpriseUser', $supportedResourceTypes)) {
$enterpriseUserSchemaExtension = new CoreSchemaExtension();
$enterpriseUserSchemaExtension->setSchema(Util::ENTERPRISE_USER_SCHEMA);
$enterpriseUserSchemaExtension->setRequired(true);
$userResourceType->setSchemaExtensions(array($enterpriseUserSchemaExtension));
}
if (in_array('ProvisioningUser', $supportedResourceTypes)) {
$provisioningUserSchemaExtension = new CoreSchemaExtension();
$provisioningUserSchemaExtension->setSchema(Util::PROVISIONING_USER_SCHEMA);
$provisioningUserSchemaExtension->setRequired(true);
$userResourceType->setSchemaExtensions(array($provisioningUserSchemaExtension));
}
$scimResourceTypes[] = $userResourceType->toSCIM(false, $baseUrl);
}
if (in_array('Group', $supportedResourceTypes)) {
$groupResourceType = new CoreResourceType();
$groupResourceType->setId("Group");
$groupResourceType->setName("Group");
$groupResourceType->setEndpoint("/Groups");
$groupResourceType->setDescription("Group");
$groupResourceType->setSchema("urn:ietf:params:scim:schemas:core:2.0:Group");
$groupResourceType->setSchemaExtensions([]);
$scimResourceTypes[] = $groupResourceType->toSCIM(false, $baseUrl);
}
if (in_array('Domain', $supportedResourceTypes)) {
$domainResourceType = new CoreResourceType();
$domainResourceType->setId("Domain");
$domainResourceType->setName("Domain");
$domainResourceType->setEndpoint("/Domains");
$domainResourceType->setDescription("Domain");
$domainResourceType->setSchema(self::DOMAIN_SCHEMA);
$domainResourceType->setSchemaExtensions([]);
$scimResourceTypes[] = $domainResourceType->toSCIM(false, $baseUrl);
}
return $scimResourceTypes;
}
/**
* A utility method for obtaining the configured SCIM schemas
*
* @return array|null Return an array of schemas or null if no schemas were found
*/
public static function getSchemas()
{
$config = Util::getConfigFile();
$supportedSchemas = $config['supportedResourceTypes'];
$mandatorySchemas = ['Schema', 'ResourceType'];
$scimSchemas = [];
// We store the schemas that the SCIM server supports in separate JSON files
// That's why we try to read them here and add them to $scimSchemas, which is returned as a result
$pathToSchemasDir = dirname(__DIR__, 2) . '/config/Schema';
$schemaFiles = scandir($pathToSchemasDir, SCANDIR_SORT_NONE);
// If scandir() failed (i.e., it returned false), then return null
if ($schemaFiles === false) {
return null;
}
foreach ($schemaFiles as $schemaFile) {
if (!in_array($schemaFile, array('.', '..'))) {
$scimSchemaJsonDecoded = json_decode(file_get_contents($pathToSchemasDir . '/' . $schemaFile), true);
// Only return schemas that are either mandatory (like the 'Schema' and 'ResourceType' ones)
// or supported by the server
if (in_array($scimSchemaJsonDecoded['name'], array_merge($supportedSchemas, $mandatorySchemas))) {
$scimSchemas[] = $scimSchemaJsonDecoded;
}
}
}
return $scimSchemas;
}
/**
* A utility method for obtaining the SCIM service provider configuration
*
* @return string|null Return the service provider configuration or null if no config was found
*/
public static function getServiceProviderConfig()
{
$pathToServiceProviderConfigurationFile =
dirname(__DIR__, 2) . '/config/ServiceProviderConfig/serviceProviderConfig.json';
$scimServiceProviderConfigurationFile = file_get_contents($pathToServiceProviderConfigurationFile);
// If there was no service provider config JSON file found, then return null
if ($scimServiceProviderConfigurationFile === false) {
return null;
}
return $scimServiceProviderConfigurationFile;
}
} }

View file

@ -1,15 +0,0 @@
<?php
use Opf\Util\Util;
use Slim\App;
return static function (App $app) {
$config = Util::getConfigFile();
$dbSettings = $config['db'];
// Boot eloquent
$capsule = new Illuminate\Database\Capsule\Manager();
$capsule->addConnection($dbSettings);
$capsule->setAsGlobal();
$capsule->bootEloquent();
};

View file

@ -1,11 +1,15 @@
<?php <?php
use Opf\Controllers\Domains\CreateDomainAction;
use Opf\Controllers\Domains\DeleteDomainAction;
use Opf\Controllers\Domains\GetDomainAction;
use Opf\Controllers\Domains\ListDomainsAction;
use Opf\Controllers\Domains\UpdateDomainAction;
use Opf\Controllers\Groups\CreateGroupAction; use Opf\Controllers\Groups\CreateGroupAction;
use Opf\Controllers\Groups\DeleteGroupAction; use Opf\Controllers\Groups\DeleteGroupAction;
use Opf\Controllers\Groups\GetGroupAction; use Opf\Controllers\Groups\GetGroupAction;
use Opf\Controllers\Groups\ListGroupsAction; use Opf\Controllers\Groups\ListGroupsAction;
use Opf\Controllers\Groups\UpdateGroupAction; use Opf\Controllers\Groups\UpdateGroupAction;
use Opf\Controllers\JWT\GenerateJWTAction;
use Opf\Controllers\ServiceProviders\ListResourceTypesAction; use Opf\Controllers\ServiceProviders\ListResourceTypesAction;
use Opf\Controllers\ServiceProviders\ListSchemasAction; use Opf\Controllers\ServiceProviders\ListSchemasAction;
use Opf\Controllers\ServiceProviders\ListServiceProviderConfigurationsAction; use Opf\Controllers\ServiceProviders\ListServiceProviderConfigurationsAction;
@ -41,6 +45,14 @@ return function (App $app) {
$app->delete('/Groups/{id}', DeleteGroupAction::class)->setName('groups.delete'); $app->delete('/Groups/{id}', DeleteGroupAction::class)->setName('groups.delete');
} }
if (in_array('Domain', $supportedResourceTypes)) {
$app->get('/Domains', ListDomainsAction::class)->setName('domains.list');
$app->get('/Domains/{id}', GetDomainAction::class)->setName('domains.get');
$app->post('/Domains', CreateDomainAction::class)->setName('domains.create');
$app->put('/Domains/{id}', UpdateDomainAction::class)->setName('domains.update');
$app->delete('/Domains/{id}', DeleteDomainAction::class)->setName('domains.delete');
}
// ServiceProvider routes // ServiceProvider routes
$app->get('/ResourceTypes', ListResourceTypesAction::class)->setName('resourceTypes.list'); $app->get('/ResourceTypes', ListResourceTypesAction::class)->setName('resourceTypes.list');
$app->get('/Schemas', ListSchemasAction::class)->setName('schemas.list'); $app->get('/Schemas', ListSchemasAction::class)->setName('schemas.list');
@ -48,7 +60,4 @@ return function (App $app) {
'/ServiceProviderConfig', '/ServiceProviderConfig',
ListServiceProviderConfigurationsAction::class ListServiceProviderConfigurationsAction::class
)->setName('serviceProviderConfigs.list'); )->setName('serviceProviderConfigs.list');
// JWT
$app->get('/jwt', GenerateJWTAction::class)->setName('jwt.generate');
}; };

View file

@ -1,5 +1,5 @@
{ {
"id": "7880a4e5-d9ce-42f8-8ed0-57f886616527", "id": "938b836a-076e-4938-a04a-3f088c363ff0",
"name": "scim-env", "name": "scim-env",
"values": [ "values": [
{ {
@ -7,9 +7,21 @@
"value": "http://localhost:8888", "value": "http://localhost:8888",
"type": "default", "type": "default",
"enabled": true "enabled": true
},
{
"key": "superadmin_jwt",
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW5AbG9jYWxob3N0Lm9yZyJ9.z5j4P09bk7StVda48g9_0Jt0LhopiNhjmmeguQCrVx8",
"type": "any",
"enabled": true
},
{
"key": "non_superadmin_jwt",
"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdEB0ZXN0Lm9yZyJ9.Lu1JcCSUiTRPGeuLgs6k6TG5DgCpuAIyA8IKg_nli5M",
"type": "default",
"enabled": true
} }
], ],
"_postman_variable_scope": "environment", "_postman_variable_scope": "environment",
"_postman_exported_at": "2022-04-14T14:54:36.929Z", "_postman_exported_at": "2022-10-07T10:05:35.659Z",
"_postman_exported_using": "Postman/9.15.2" "_postman_exported_using": "Postman/9.31.0"
} }

View file

@ -1,597 +0,0 @@
{
"info": {
"_postman_id": "2b79327d-70c4-425f-942f-6037f38d67e5",
"name": "PFA SCIM PHP Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "JWT",
"item": [
{
"name": "Get JWT",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const response = pm.response.json();",
"pm.environment.set(\"jwt_token\", response.Bearer)"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/jwt",
"host": [
"{{url}}"
],
"path": [
"jwt"
]
}
},
"response": []
}
]
},
{
"name": "ResourceTypes",
"item": [
{
"name": "Read all ResourceTypes",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains exactly one entry\", () => {",
" pm.expect(pm.response.json().Resources.length).to.eql(1);",
"});",
"",
"pm.test(\"Response body contains ResourceType with id \\\"User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"User\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/ResourceTypes",
"host": [
"{{url}}"
],
"path": [
"ResourceTypes"
]
}
},
"response": []
}
]
},
{
"name": "Schemas",
"item": [
{
"name": "Read all Schemas",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains exactly four entries\", () => {",
" pm.expect(pm.response.json().Resources.length).to.eql(4);",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {",
" pm.expect(pm.response.json().Resources[1].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[2].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:User\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {",
" pm.expect(pm.response.json().Resources[3].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Schema\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/Schemas",
"host": [
"{{url}}"
],
"path": [
"Schemas"
]
}
},
"response": []
}
]
},
{
"name": "ServiceProviderConfigs",
"item": [
{
"name": "Read all ServiceProviderConfigs",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains a ServiceProviderConfig with a correct schema\", () => {",
" pm.expect(pm.response.json().schemas).to.include(\"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/ServiceProviderConfig",
"host": [
"{{url}}"
],
"path": [
"ServiceProviderConfig"
]
}
},
"response": []
}
]
},
{
"name": "Users",
"item": [
{
"name": "Create a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 201\", () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser@test.org\\\"\", () => {",
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser@test.org\");",
"});",
"",
"pm.test(\"Response body contains a valid non-null user ID (the ID of the user which was created)\", () => {",
" pm.expect(pm.response.json().id).to.not.be.null;",
"});",
"",
"pm.test(\"Response body contains user with sizeQuota equal to 12345\", () => {",
" pm.expect(pm.response.json()[\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(12345);",
"});",
"",
"pm.collectionVariables.set(\"testUserId\", pm.response.json().id);"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"userName\": \"createdtestuser@test.org\",\n \"id\": \"createdtestuser@test.org\",\n \"active\": true,\n \"password\": \"somepass123\",\n \"displayName\": \"createdtestuser\",\n \"emails\": [{\"value\":\"createdtestuser@test.org\"}],\n \"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\": {\n \"sizeQuota\": 12345\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Users",
"host": [
"{{url}}"
],
"path": [
"Users"
]
}
},
"response": []
},
{
"name": "Read a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
" pm.expect(pm.response.json().id).to.eql(\"createdtestuser@test.org\");",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser@test.org\\\"\", () => {",
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser@test.org\");",
"});",
"",
"pm.test(\"Response body contains user with sizeQuota equal to 12345\", () => {",
" pm.expect(pm.response.json()[\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(12345);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/Users/{{testUserId}}",
"host": [
"{{url}}"
],
"path": [
"Users",
"{{testUserId}}"
]
}
},
"response": []
},
{
"name": "Read a non-existent user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 404\", () => {",
" pm.response.to.have.status(404);",
"});",
"",
"pm.test(\"Response body is empty\", () => {",
" pm.expect(pm.response.body).to.be.undefined;",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/Users/some-non-existent-user@test.org",
"host": [
"{{url}}"
],
"path": [
"Users",
"some-non-existent-user@test.org"
]
}
},
"response": []
},
{
"name": "Read all users",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser@test.org\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].userName).to.eql(\"createdtestuser@test.org\");",
"});",
"",
"pm.test(\"Response body contains user with sizeQuota equal to 12345\", () => {",
" pm.expect(pm.response.json().Resources[0][\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(12345);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/Users",
"host": [
"{{url}}"
],
"path": [
"Users"
]
}
},
"response": []
},
{
"name": "Update a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
"});",
"",
"pm.test(\"Response body contains user with displayName \\\"updatedtestuser\\\"\", () => {",
" pm.expect(pm.response.json().displayName).to.eql(\"updatedtestuser\");",
"});",
"",
"pm.test(\"Response body contains user with sizeQuota equal to 123456789\", () => {",
" pm.expect(pm.response.json()[\"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\"].sizeQuota).to.eql(123456789);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
},
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"id\": \"createdtestuser@test.org\",\n \"displayName\": \"updatedtestuser\",\n \"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User\": {\n \"sizeQuota\": 123456789\n }\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Users/{{testUserId}}",
"host": [
"{{url}}"
],
"path": [
"Users",
"{{testUserId}}"
]
}
},
"response": []
},
{
"name": "Delete a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 204\", () => {",
" pm.response.to.have.status(204);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
},
{
"key": "Authorization",
"value": "Bearer: {{jwt_token}}",
"type": "default"
}
],
"url": {
"raw": "{{url}}/Users/{{testUserId}}",
"host": [
"{{url}}"
],
"path": [
"Users",
"{{testUserId}}"
]
}
},
"response": []
}
]
}
],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{jwt_token}}",
"type": "string"
}
]
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "testUserId",
"value": ""
},
{
"key": "testGroupId",
"value": ""
}
]
}

View file

@ -1,10 +1,44 @@
{ {
"info": { "info": {
"_postman_id": "73043646-f766-4adc-96ee-05316cc59bdd", "_postman_id": "c90f5107-b2fb-46dc-9a32-b07f5ff68440",
"name": "SCIM PHP Collection", "name": "SCIM PHP Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
}, },
"item": [ "item": [
{
"name": "JWT",
"item": [
{
"name": "Get JWT",
"event": [
{
"listen": "test",
"script": {
"exec": [
"const response = pm.response.json();",
"pm.environment.set(\"jwt_token\", response.Bearer)"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/jwt",
"host": [
"{{url}}"
],
"path": [
"jwt"
]
}
},
"response": []
}
]
},
{ {
"name": "Users", "name": "Users",
"item": [ "item": [
@ -138,7 +172,8 @@
"});", "});",
"", "",
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {", "pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].userName).to.eql(\"createdtestuser\");", " var resources = pm.response.json().Resources.map(x => x.userName);",
" pm.expect(resources).to.contain(\"createdtestuser\");",
"});" "});"
], ],
"type": "text/javascript" "type": "text/javascript"
@ -160,6 +195,154 @@
}, },
"response": [] "response": []
}, },
{
"name": "Filter users by userName",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].userName).to.eql(\"createdtestuser\");",
"});",
"",
"pm.test(\"Response body contains a valid non-null user ID\", () => {",
" pm.expect(pm.response.json().id).to.not.be.null;",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Users?filter=userName%20eq%20createdtestuser",
"host": [
"{{url}}"
],
"path": [
"Users"
],
"query": [
{
"key": "filter",
"value": "userName%20eq%20createdtestuser"
}
]
}
},
"response": []
},
{
"name": "Filter users with invalid filter",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 500\", () => {",
" pm.response.to.have.status(500);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains error '\\\"pr\\\" filter operator must be used without a comparison value'\", () => {",
" pm.expect(pm.response.json().error.description).to.eql(\"\\\"pr\\\" filter operator must be used without a comparison value\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Users?filter=userName%20pr%20createdtestuser",
"host": [
"{{url}}"
],
"path": [
"Users"
],
"query": [
{
"key": "filter",
"value": "userName%20pr%20createdtestuser"
}
]
}
},
"response": []
},
{
"name": "Filter users with unmatching filter",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains no users\", () => {",
" pm.expect(pm.response.json().Resources).to.be.empty;",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Users?filter=userName%20sw%20somenonexistentstring",
"host": [
"{{url}}"
],
"path": [
"Users"
],
"query": [
{
"key": "filter",
"value": "userName%20sw%20somenonexistentstring"
}
]
}
},
"response": []
},
{ {
"name": "Update a single user", "name": "Update a single user",
"event": [ "event": [
@ -403,7 +586,8 @@
"});", "});",
"", "",
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {", "pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].displayName).to.eql(\"createdtestgroup\");", " var resources = pm.response.json().Resources.map(x => x.displayName);",
" pm.expect(resources).to.contain(\"createdtestgroup\");",
"});" "});"
], ],
"type": "text/javascript" "type": "text/javascript"
@ -425,6 +609,154 @@
}, },
"response": [] "response": []
}, },
{
"name": "Filter groups by displayName",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].displayName).to.eql(\"createdtestgroup\");",
"});",
"",
"pm.test(\"Response body contains a valid non-null group ID\", () => {",
" pm.expect(pm.response.json().id).to.not.be.null;",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Groups?filter=displayName%20eq%20createdtestgroup",
"host": [
"{{url}}"
],
"path": [
"Groups"
],
"query": [
{
"key": "filter",
"value": "displayName%20eq%20createdtestgroup"
}
]
}
},
"response": []
},
{
"name": "Filter groups with invalid filter",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 500\", () => {",
" pm.response.to.have.status(500);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains error '\\\"pr\\\" filter operator must be used without a comparison value'\", () => {",
" pm.expect(pm.response.json().error.description).to.eql(\"\\\"pr\\\" filter operator must be used without a comparison value\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Groups?filter=displayName%20pr%20createdtestgroup",
"host": [
"{{url}}"
],
"path": [
"Groups"
],
"query": [
{
"key": "filter",
"value": "displayName%20pr%20createdtestgroup"
}
]
}
},
"response": []
},
{
"name": "Filter groups with unmatching filter",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains no groups\", () => {",
" pm.expect(pm.response.json().Resources).to.be.empty;",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Groups?filter=displayName%20sw%20somenonexistentstring",
"host": [
"{{url}}"
],
"path": [
"Groups"
],
"query": [
{
"key": "filter",
"value": "displayName%20sw%20somenonexistentstring"
}
]
}
},
"response": []
},
{ {
"name": "Update a single group", "name": "Update a single group",
"event": [ "event": [
@ -579,6 +911,8 @@
"listen": "test", "listen": "test",
"script": { "script": {
"exec": [ "exec": [
"jsonData = pm.response.json();",
"",
"pm.test(\"Response status code is 200\", () => {", "pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);", " pm.response.to.have.status(200);",
"});", "});",
@ -588,31 +922,30 @@
"});", "});",
"", "",
"pm.test(\"Response body is not empty\", () => {", "pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;", " pm.expect(jsonData.Resources).to.not.be.empty;",
"});", "});",
"", "",
"pm.test(\"Response body contains exactly five entries\", () => {", "pm.test(\"Response body contains exactly four entries\", () => {",
" pm.expect(pm.response.json().Resources.length).to.eql(5);", " pm.expect(jsonData.Resources.length).to.eql(4);",
"});", "});",
"", "",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Group\\\"\", () => {", "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Group\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Group\");", " isContained = jsonData.Resources.some((resource) => resource.id === \"urn:ietf:params:scim:schemas:core:2.0:Group\");",
" pm.expect(isContained).to.be.true;",
"});", "});",
"", "",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {", "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {",
" pm.expect(pm.response.json().Resources[1].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");", " isContained = jsonData.Resources.some((resource) => resource.id === \"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");",
" pm.expect(isContained).to.be.true;",
"});", "});",
"", "",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {", "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {",
" pm.expect(pm.response.json().Resources[2].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Schema\");", " isContained = jsonData.Resources.some((resource) => resource.id === \"urn:ietf:params:scim:schemas:core:2.0:Schema\");",
" pm.expect(isContained).to.be.true;",
"});", "});",
"", "",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {", "pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[3].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:User\");", " isContained = jsonData.Resources.some((resource) => resource.id === \"urn:ietf:params:scim:schemas:core:2.0:User\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[4].id).to.eql(\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\");",
"});" "});"
], ],
"type": "text/javascript" "type": "text/javascript"
@ -684,6 +1017,16 @@
] ]
} }
], ],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{jwt_token}}",
"type": "string"
}
]
},
"event": [ "event": [
{ {
"listen": "prerequest", "listen": "prerequest",

View file

@ -0,0 +1,17 @@
[
{
"displayName": "testGroup",
"members": []
},
{
"displayName": "testGroup2",
"members": []
},
{
"displayName": "testGroup3",
"members": [
"12345678-9012-3456-7890-12345678",
"87654321-2109-6543-0987-87654321"
]
}
]

View file

@ -0,0 +1,19 @@
[
{
"userName": "testuser",
"profileUrl": "http://example.com/testuser"
},
{
"userName": "testuser2",
"externalId": "testuser2external",
"name": {
"givenName": "given",
"familyName": "family"
}
},
{
"userName": "testuser3",
"externalId": "testuser3external",
"profileUrl": "http://example.com/testuser3"
}
]

View file

@ -0,0 +1,25 @@
<?php
return [
'isInProduction' => false, // Set to true when deploying in production
'basePath' => null, // If you want to specify a base path for the Slim app, add it here (e.g., '/test/scim')
'supportedResourceTypes' => ['User', 'Group'], // Specify all the supported SCIM ResourceTypes by their names here
// SQLite DB settings
'db' => [
'driver' => 'sqlite', // Type of DB
'databaseFile' => 'db/scim-mock.sqlite' // DB name
],
// Monolog settings
'logger' => [
'name' => 'scim-opf',
'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../../logs/app.log',
'level' => \Monolog\Logger::DEBUG,
],
// Bearer token settings
'jwt' => [
'secret' => 'secret'
]
];

View file

@ -0,0 +1,57 @@
<?php
namespace Opf\Test\Unit;
use Opf\Models\SCIM\Standard\Filters\AttributeExpression;
use Opf\Models\SCIM\Standard\Filters\AttributeOperator;
use Opf\Models\SCIM\Standard\Filters\FilterException;
use Opf\Models\SCIM\Standard\Filters\FilterExpression;
use Opf\Util\Filters\FilterParser;
use PHPUnit\Framework\TestCase;
final class FilterParserTest extends TestCase
{
public function testParseAttributeFilterExpression()
{
// Test an "eq" filter expression
$filterString = "userName eq sometestusername";
$attributeFilterExpression = FilterParser::parseFilterExpression($filterString);
$this->assertInstanceOf(FilterExpression::class, $attributeFilterExpression);
$this->assertInstanceOf(AttributeExpression::class, $attributeFilterExpression);
$this->assertEquals("userName", $attributeFilterExpression->getAttributePath());
$this->assertEquals(AttributeOperator::OP_EQ, $attributeFilterExpression->getCompareOperator());
$this->assertEquals("sometestusername", $attributeFilterExpression->getComparisonValue());
// Test a "pr" filter expression
$filterString = "meta.created pr";
$attributeFilterExpression = FilterParser::parseFilterExpression($filterString);
$this->assertInstanceOf(FilterExpression::class, $attributeFilterExpression);
$this->assertInstanceOf(AttributeExpression::class, $attributeFilterExpression);
$this->assertEquals("meta.created", $attributeFilterExpression->getAttributePath());
$this->assertEquals(AttributeOperator::OP_PR, $attributeFilterExpression->getCompareOperator());
$this->assertNull($attributeFilterExpression->getComparisonValue());
}
public function testParseTooShortFilterExpression()
{
$this->expectException(FilterException::class);
$this->expectExceptionMessage("Incorrectly formatted AttributeExpression");
$filterString = "somestring";
$parsedFilterExpression = FilterParser::parseFilterExpression($filterString);
}
public function testParseTooLongFilterExpression()
{
$this->expectException(FilterException::class);
$this->expectExceptionMessage("Incorrectly formatted AttributeExpression");
$filterString = "userName eq some value";
$parsedFilterExpression = FilterParser::parseFilterExpression($filterString);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Opf\Test\Unit;
use Opf\Models\SCIM\Standard\Filters\FilterException;
use Opf\Util\Filters\FilterParser;
use Opf\Util\Filters\FilterUtil;
use PHPUnit\Framework\TestCase;
final class FilterUtilTest extends TestCase
{
/** @var array */
protected $scimGroups = [];
/** @var array */
protected $scimUsers = [];
public function setUp(): void
{
$this->scimGroups = json_decode(file_get_contents(__DIR__ . '/../resources/filterTestGroups.json'), true);
$this->scimUsers = json_decode(file_get_contents(__DIR__ . '/../resources/filterTestUsers.json'), true);
}
public function tearDown(): void
{
$this->scimGroups = [];
$this->scimUsers = [];
}
public function testGroupFiltering()
{
// "ne" filter test
$filterString = "displayName ne testGroup";
$filteredScimGroups = FilterUtil::performFiltering($filterString, $this->scimGroups);
$this->assertEquals(array_splice($this->scimGroups, 1, 2), $filteredScimGroups);
}
public function testUserFiltering()
{
// "sw" filter test
$filterString = "userName sw testuser";
$filteredScimUsers = FilterUtil::performFiltering($filterString, $this->scimUsers);
$this->assertEquals($this->scimUsers, $filteredScimUsers);
}
public function testInvalidFiltering()
{
$this->expectException(FilterException::class);
$this->expectExceptionMessage("Incorrectly formatted AttributeExpression");
$filterString = "externalId eq some value";
$filteredScimUsers = FilterUtil::performFiltering($filterString, $this->scimUsers);
}
}

View file

@ -3,85 +3,50 @@
namespace Opf\Test\Unit; namespace Opf\Test\Unit;
use Illuminate\Database\Capsule\Manager; use Illuminate\Database\Capsule\Manager;
use Opf\Adapters\Groups\MockGroupAdapter;
use Opf\DataAccess\Groups\MockGroupDataAccess; use Opf\DataAccess\Groups\MockGroupDataAccess;
use Opf\Models\SCIM\Standard\Groups\CoreGroup;
use Opf\Util\Util;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use SQLite3; use SQLite3;
final class MockGroupsDataAccessTest extends TestCase final class MockGroupsDataAccessTest extends TestCase
{ {
/** @var SQLite3 */ /** @var Opf\Models\SCIM\Standard\Groups\CoreGroup */
protected $database = null; protected $coreGroup = null;
/** @var array */ /** @var Opf\DataAccess\Groups\MockGroupDataAccess */
protected $dbSettings = null;
/** @var Illuminate\Database\Capsule\Manager */
protected $capsule = null;
/** @var Opf\Models\CoreGroup */
protected $mockGroupDataAccess = null; protected $mockGroupDataAccess = null;
/** @var Opf\Opf\Adapters\Groups\MockGroupAdapter */
protected $mockGroupAdapter = null;
public function setUp(): void public function setUp(): void
{ {
$this->database = new SQLite3(__DIR__ . '/../resources/test-scim-opf.sqlite'); Util::setConfigFile(__DIR__ . '/../resources/mock-test-config.php');
$this->coreGroup = new CoreGroup();
$groupDbSql = "CREATE TABLE IF NOT EXISTS groups ( $this->mockGroupAdapter = new MockGroupAdapter();
id varchar(160) NOT NULL UNIQUE,
displayName varchar(160) NOT NULL DEFAULT '',
members TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NULL
)";
$this->database->exec($groupDbSql);
$createGroupSql = "INSERT INTO groups (
id,
displayName,
members
) VALUES (
'12345678-9012-3456-7890-12345679',
'testGroup',
'12345678-9012-3456-7890-12345678'
)";
$this->database->exec($createGroupSql);
$this->dbSettings = [
'driver' => 'sqlite',
'database' => __DIR__ . '/../resources/test-scim-opf.sqlite',
'prefix' => ''
];
$this->capsule = new Manager();
$this->capsule->addConnection($this->dbSettings);
$this->capsule->setAsGlobal();
$this->capsule->bootEloquent();
$this->mockGroupDataAccess = new MockGroupDataAccess(); $this->mockGroupDataAccess = new MockGroupDataAccess();
} }
public function tearDown(): void public function tearDown(): void
{ {
$this->coreGroup = null;
$this->mockGroupAdapter = null;
$this->mockGroupDataAccess = null; $this->mockGroupDataAccess = null;
$this->capsule = null;
$this->dbSettings = null;
$this->database->exec("DROP TABLE groups");
$this->database = null;
unlink(__DIR__ . '/../resources/test-scim-opf.sqlite');
}
public function testReadAllGroups()
{
$this->assertNotEmpty($this->mockGroupDataAccess->all());
} }
public function testCreateGroup() public function testCreateGroup()
{ {
$testGroupJson = json_decode(file_get_contents(__DIR__ . '/../resources/testGroup.json'), true); $testGroupJson = json_decode(file_get_contents(__DIR__ . '/../resources/testGroup.json'), true);
$this->mockGroupDataAccess->fromSCIM($testGroupJson); $this->coreGroup->fromSCIM($testGroupJson);
$groupCreateRes = $this->mockGroupDataAccess->save(); $mockGroup = $this->mockGroupAdapter->getMockGroup($this->coreGroup);
$this->assertTrue($groupCreateRes); $groupCreateRes = $this->mockGroupDataAccess->create($mockGroup);
$this->assertNotNull($groupCreateRes);
}
public function testReadAllGroups()
{
$this->assertNotEmpty($this->mockGroupDataAccess->getAll());
} }
} }

View file

@ -4,88 +4,49 @@ namespace Opf\Test\Unit;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Illuminate\Database\Capsule\Manager; use Illuminate\Database\Capsule\Manager;
use Opf\Adapters\Users\MockUserAdapter;
use Opf\DataAccess\Users\MockUserDataAccess; use Opf\DataAccess\Users\MockUserDataAccess;
use Opf\Models\SCIM\Standard\Users\CoreUser;
use Opf\Util\Util;
use SQLite3; use SQLite3;
final class MockUsersDataAccessTest extends TestCase final class MockUsersDataAccessTest extends TestCase
{ {
/** @var SQLite3 */ /** @var Opf\Models\SCIM\Standard\Users\CoreUser */
protected $database = null; protected $coreUser = null;
/** @var array */ /** @var Opf\DataAccess\Users\MockUserDataAccess */
protected $dbSettings = null;
/** @var Illuminate\Database\Capsule\Manager */
protected $capsule = null;
/** @var Opf\Models\MockUser */
protected $mockUserDataAccess = null; protected $mockUserDataAccess = null;
/** @var Opf\Adapters\Users\MockUserAdapter */
protected $mockUserAdapter = null;
public function setUp(): void public function setUp(): void
{ {
$this->database = new SQLite3(__DIR__ . '/../resources/test-scim-opf.sqlite'); Util::setConfigFile(__DIR__ . '/../resources/mock-test-config.php');
$this->coreUser = new CoreUser();
$userDbSql = "CREATE TABLE IF NOT EXISTS users (
id varchar(160) NOT NULL UNIQUE,
userName varchar(160) NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
externalId varchar(160) NULL,
profileUrl varchar(160) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NULL
)";
$this->database->exec($userDbSql);
$createUserSql = "INSERT INTO users (
id,
userName,
externalId,
profileUrl
) VALUES (
'12345678-9012-3456-7890-12345678',
'testuser',
'testuserexternal',
'https://example.com/testuser'
)";
$this->database->exec($createUserSql);
$this->dbSettings = [
'driver' => 'sqlite',
'database' => __DIR__ . '/../resources/test-scim-opf.sqlite',
'prefix' => ''
];
$this->capsule = new Manager();
$this->capsule->addConnection($this->dbSettings);
$this->capsule->setAsGlobal();
$this->capsule->bootEloquent();
$this->mockUserDataAccess = new MockUserDataAccess(); $this->mockUserDataAccess = new MockUserDataAccess();
$this->mockUserAdapter = new MockUserAdapter();
} }
public function tearDown(): void public function tearDown(): void
{ {
$this->coreUser = null;
$this->mockUserDataAccess = null; $this->mockUserDataAccess = null;
$this->capsule = null; $this->mockUserAdapter = null;
$this->dbSettings = null;
$this->database->exec("DROP TABLE users");
$this->database = null;
unlink(__DIR__ . '/../resources/test-scim-opf.sqlite');
}
public function testReadAllUsers()
{
$this->assertNotEmpty($this->mockUserDataAccess->all());
} }
public function testCreateUser() public function testCreateUser()
{ {
$testUserJson = json_decode(file_get_contents(__DIR__ . '/../resources/testUser.json'), true); $testUserJson = json_decode(file_get_contents(__DIR__ . '/../resources/testUser.json'), true);
$this->mockUserDataAccess->fromSCIM($testUserJson); $this->coreUser->fromSCIM($testUserJson);
$userCreateRes = $this->mockUserDataAccess->save(); $mockUser = $this->mockUserAdapter->getMockUser($this->coreUser);
$this->assertTrue($userCreateRes); $userCreateRes = $this->mockUserDataAccess->create($mockUser);
$this->assertNotNull($userCreateRes);
}
public function testReadAllUsers()
{
$this->assertNotEmpty($this->mockUserDataAccess->getAll());
} }
} }