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
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
endif
.PHONY: unit_test
unit_test:

195
README.md
View file

@ -1,6 +1,33 @@
# 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)
@ -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:
* 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:
* Standard SCIM resources implementations (*Core User*, *Enterprise User* and *Groups*)
* 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 very simple JWT implementation
* When enabled, a JWT token is generated on the `/jwt` endpoint. You **must** therefore protect this endpoint.
* A simple JWT implementation
* 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
## Postfix Admin SCIM API
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
}
}
```
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**.
## Prerequisites
* **scim-server-php** requires PHP 7.4
* Dependencies are managed with [composer](https://getcomposer.org/)
## Installation
### Local installation
* Run `make install` to automatically install dependencies
## Usage
### Configuration
* 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):
### Get it as a [composer](https://getcomposer.org/) dependency
* You can add the following to your `composer.json` file to get it with [composer](https://getcomposer.org/)
```
"repositories": {
"scim": {
"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
```
// Set up system-specific dependencies
$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php'; // include that line if you want to use the integrated mock SCIM server
$dependencies = require dirname(__DIR__) . '/src/Dependencies/pfa-dependencies.php'; // include that line if you want to use the Postfix Admin SCIM API
$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
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",
"description": "An open provisioning framework using the SCIM protocol",
"name": "audriga/scim-server-php",
"description": "An open library for SCIM servers implementation",
"type": "library",
"require": {
"slim/slim": "^4.10",
@ -20,8 +20,8 @@
},
"authors": [
{
"name": "Stanimir Bozhilov",
"email": "stanimir@audriga.com"
"name": "audriga",
"email": "opensource@audriga.com"
}
],
"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 [
'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
// SQLite DB settings
'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' => [
// 'driver' => 'sqlite', // Type of DB
// 'driver' => 'mysql', // Type of DB
// 'host' => 'localhost', // DB host
// 'port' => '3306', // Port on DB host
// 'database' => 'postfix', // DB name
// 'user' => 'postfix', // DB user
// 'password' => 'postfix123' // DB user's password
// 'database' => 'db_name', // DB name
// 'user' => 'db_user', // DB user
// 'password' => 'db_password' // DB user's password
//],
// Monolog settings
@ -30,7 +30,6 @@ return [
// Bearer token settings
'jwt' => [
'secure' => false,
'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
use DI\ContainerBuilder;
use Opf\Handlers\HttpErrorHandler;
use Opf\Util\Util;
use Slim\Factory\AppFactory;
use Opf\ScimServer;
require dirname(__DIR__) . '/vendor/autoload.php';
session_start();
require __DIR__ . '/../vendor/autoload.php';
// Instantiate the PHP-DI ContainerBuilder
$containerBuilder = new ContainerBuilder();
// Obtain the root of the project
$scimServerPhpRoot = dirname(__DIR__);
$config = Util::getConfigFile();
if ($config['isInProduction']) {
$containerBuilder->enableCompilation(__DIR__ . '/../var/cache');
}
// Create a new ScimServer instance and give it the project root
$scimServer = new ScimServer($scimServerPhpRoot);
// 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);
// Take the config file path and pass it to the scimServer instance
$configFilePath = __DIR__ . '/../config/config.php';
$scimServer->setConfig($configFilePath);
// Set up common dependencies
$dependencies = require dirname(__DIR__) . '/src/Dependencies/dependencies.php';
$dependencies($containerBuilder);
// Obtain custom dependencies (if any) and pass them to the scimServer instance
$dependencies = require __DIR__ . '/../src/Dependencies/mock-dependencies.php';
$scimServer->setDependencies($dependencies);
// Set up system-specific dependencies
$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php';
$dependencies($containerBuilder);
// Set the Authentication Middleware configured in the dependencies files above to the scimServer instance
//$scimServerPhpAuthMiddleware = 'AuthMiddleware';
//$scimServer->setMiddleware(array($scimServerPhpAuthMiddleware));
// Build PHP-DI Container instance
$container = $containerBuilder->build();
// 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();
// Start the scimServer
$scimServer->run();

View file

@ -3,76 +3,69 @@
namespace Opf\Adapters\Groups;
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
{
/** @var Opf\Models\MockGroup $group */
private $group;
public function getGroup()
public function getCoreGroup(?MockGroup $mockGroup): ?CoreGroup
{
return $this->group;
}
public function setGroup(MockGroupDataAccess $group)
{
$this->group = $group;
}
public function getId()
{
if (isset($this->group->id) && !empty($this->group->id)) {
return $this->group->id;
if (!isset($mockGroup)) {
return null;
}
$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)) {
$this->group->id = $id;
if (!isset($coreGroup)) {
return null;
}
}
public function getCreatedAt()
{
if (isset($this->group->created_at) && !empty($this->group->created_at)) {
return $this->group->created_at;
}
}
$mockGroup = new MockGroup();
$mockGroup->setId($coreGroup->getId());
public function setCreatedAt($createdAt)
{
if (isset($createdAt) && !empty($createdAt)) {
$this->group->created_at = $createdAt;
if ($coreGroup->getMeta() !== null) {
$mockGroup->setCreatedAt($coreGroup->getMeta()->getCreated());
$mockGroup->setUpdatedAt($coreGroup->getMeta()->getLastModified());
}
}
public function getDisplayName()
{
if (isset($this->group->displayName) && !empty($this->group->displayName)) {
return $this->group->displayName;
}
}
$mockGroup->setDisplayName($coreGroup->getDisplayName());
public function setDisplayName($displayName)
{
if (isset($displayName) && !empty($displayName)) {
$this->group->displayName = $displayName;
}
}
if ($coreGroup->getMembers() !== null && !empty($coreGroup->getMembers())) {
$mockGroupMembers = [];
foreach ($coreGroup->getMembers() as $coreGroupMember) {
$mockGroupMembers[] = $coreGroupMember->getValue();
}
public function getMembers()
{
if (isset($this->group->members) && !empty($this->group->members)) {
return $this->group->members;
$mockGroup->setMembers($mockGroupMembers);
}
}
public function setMembers($members)
{
if (isset($members) && !empty($members)) {
$this->group->members = $members;
}
return $mockGroup;
}
}

View file

@ -3,104 +3,54 @@
namespace Opf\Adapters\Users;
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
{
/** @var Opf\Models\MockUser $user */
private $user;
public function getUser()
public function getCoreUser(?MockUser $mockUser): ?CoreUser
{
return $this->user;
}
public function setUser(MockUserDataAccess $user)
{
$this->user = $user;
}
public function getId()
{
if (isset($this->user->id) && !empty($this->user->id)) {
return $this->user->id;
if (!isset($mockUser)) {
return null;
}
$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)) {
$this->user->id = $id;
if (!isset($coreUser)) {
return null;
}
}
public function getUserName()
{
if (isset($this->user->userName) && !empty($this->user->userName)) {
return $this->user->userName;
}
}
$mockUser = new MockUser();
$mockUser->setId($coreUser->getId());
public function setUserName($userName)
{
if (isset($userName) && !empty($userName)) {
$this->user->userName = $userName;
if ($coreUser->getMeta() !== null) {
$mockUser->setCreatedAt($coreUser->getMeta()->getCreated());
$mockUser->setUpdatedAt($coreUser->getMeta()->getLastModified());
}
}
public function getCreatedAt()
{
if (isset($this->user->created_at) && !empty($this->user->created_at)) {
return $this->user->created_at;
}
}
$mockUser->setUserName($coreUser->getUserName());
$mockUser->setActive(boolval($coreUser->getActive()));
$mockUser->setExternalId($coreUser->getExternalId());
$mockUser->setProfileUrl($coreUser->getProfileUrl());
public function setCreatedAt($createdAt)
{
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;
}
return $mockUser;
}
}

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
{
$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();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$groups = [];
$groups = $this->repository->getAll();
$groups = $this->repository->getAll($filter);
$scimGroups = [];
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();
$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
// 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);
}
$scimResourceTypes = Util::getResourceTypes($baseUrl);
$scimResourceTypeCollection = (new CoreCollection($scimResourceTypes))->toSCIM(false);
$responseBody = json_encode($scimResourceTypeCollection, JSON_UNESCAPED_SLASHES);

View file

@ -15,20 +15,10 @@ final class ListSchemasAction extends Controller
{
$this->logger->info("GET Schemas");
$config = Util::getConfigFile();
$supportedSchemas = $config['supportedResourceTypes'];
$mandatorySchemas = ['Schema', 'ResourceType'];
$scimSchemas = Util::getSchemas();
$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) {
// If there were no schemas found, return 404
if (is_null($scimSchemas)) {
$this->logger->info("No Schemas found");
$response = new Response($status = 404);
$response = $response->withHeader('Content-Type', 'application/scim+json');
@ -36,18 +26,6 @@ final class ListSchemasAction extends Controller
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);
$responseBody = json_encode($scimSchemasCollection, JSON_UNESCAPED_SLASHES);

View file

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

View file

@ -21,25 +21,17 @@ final class ListUsersAction extends Controller
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("GET Users");
$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);
$userName = null;
$users = [];
if (!empty($request->getQueryParams('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();
}
$users = $this->repository->getAll($filter);
$scimUsers = [];
if (!empty($users)) {

View file

@ -2,59 +2,211 @@
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 PDO;
use PDOException;
class MockGroupDataAccess extends Model
class MockGroupDataAccess
{
protected $table = 'groups';
protected $fillable = ['id', 'displayName', 'members', 'created_at'];
public $incrementing = false;
/** @var PDO */
private $dbConnection;
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0:Group"];
private $baseLocation;
/** @var \Monolog\Logger */
private $logger;
public function fromArray($data)
public function __construct()
{
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid());
$this->displayName = $data['displayName'];
$this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']);
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW');
// Instantiate our logger
$this->logger = new Logger(MockGroupDataAccess::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");
}
// 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());
$this->displayName = $data['displayName'];
$this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']);
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW');
if (isset($this->dbConnection)) {
$selectStatement = $this->dbConnection->query("SELECT * from groups");
if ($selectStatement) {
$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 = [
'schemas' => $this->schemas,
'id' => $this->id,
'displayName' => $this->displayName,
'members' => [],
'meta' => [
'created' => Util::dateTime2string($this->created_at),
'location' => $baseLocation . '/Groups/' . $this->id
]
];
if (isset($id) && !empty($id)) {
if (isset($this->dbConnection)) {
try {
$selectOnePreparedStatement = $this->dbConnection->prepare(
"SELECT * FROM groups WHERE id = ?"
);
if (!empty($this->members)) {
$data['members'] = explode(',', $this->members);
$selectRes = $selectOnePreparedStatement->execute([$id]);
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)) {
$data['meta']['updated'] = Util::dateTime2string($this->updated_at);
}
$this->logger->error(
"Argument provided to getOneById in class " . MockGroupDataAccess::class . " is not set or empty"
);
return null;
}
if ($encode) {
$data = json_encode($data);
}
public function create(MockGroup $groupToCreate): ?MockGroup
{
$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;
use Illuminate\Database\Eloquent\Model;
use Exception;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use Opf\Models\Mock\MockUser;
use Opf\Util\Util;
use PDO;
use PDOException;
class MockUserDataAccess extends Model
class MockUserDataAccess
{
protected $table = 'users';
protected $fillable = ['id', 'userName', 'created_at', 'active',
'externalId', 'profileUrl'];
public $incrementing = false;
/** @var PDO */
private $dbConnection;
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"];
private $baseLocation;
/** @var \Monolog\Logger */
private $logger;
public function fromArray($data)
public function __construct()
{
$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['created']) ? Util::string2dateTime($data['created'])
: (isset($this->created_at) ? $this->created_at : new \DateTime('NOW'));
$this->active = isset($data['active']) ? $data['active'] : true;
// Instantiate our logger
$this->logger = new Logger(MockUserDataAccess::class);
$this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG));
$this->externalId = isset($data['externalId']) ? $data['externalId'] : null;
$this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null;
}
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);
// 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");
}
if ($encode) {
$data = json_encode($data);
// 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 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);
use DI\ContainerBuilder;
use Opf\Controllers\Controller;
use Opf\Util\Util;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Tuupola\Middleware\JwtAuthentication;
return function (ContainerBuilder $containerBuilder) {
$containerBuilder->addDefinitions([
// Monolog
Monolog\Logger::class => function (ContainerInterface $c) {
$config = Util::getConfigFile();
$settings = $config['logger'];
$logger = new Monolog\Logger($settings['name']);
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
return $logger;
},
return [
// Monolog
Monolog\Logger::class => function () {
$config = Util::getConfigFile();
$settings = $config['logger'];
$logger = new Monolog\Logger($settings['name']);
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
return $logger;
},
// JWT
'JwtAuthentication' => function (ContainerInterface $c) {
$config = Util::getConfigFile();
$settings = $config['jwt'];
$settings["logger"] = $c->get(Monolog\Logger::class);
$settings["attribute"] = "jwt";
// JWT
'JwtAuthentication' => function (ContainerInterface $c) {
$config = Util::getConfigFile();
$settings = $config['jwt'];
$settings["logger"] = $c->get(Monolog\Logger::class);
$settings["attribute"] = "jwt";
// Don't ask for JWT when trying to obtain one
$basePath = "";
if (isset($config) && !empty($config)) {
if (isset($config["basePath"]) && !empty($config["basePath"])) {
$basePath = $config["basePath"];
}
// Don't ask for JWT when trying to obtain one
$basePath = "";
if (isset($config) && !empty($config)) {
if (isset($config["basePath"]) && !empty($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);
use DI\ContainerBuilder;
use Opf\Adapters\Groups\MockGroupAdapter;
use Opf\Adapters\Users\MockUserAdapter;
use Opf\Controllers\Controller;
use Opf\DataAccess\Groups\MockGroupDataAccess;
use Opf\DataAccess\Users\MockUserDataAccess;
use Opf\Middleware\SimpleAuthMiddleware;
use Opf\Repositories\Groups\MockGroupsRepository;
use Opf\Repositories\Users\MockUsersRepository;
use Opf\Util\Authentication\SimpleBearerAuthenticator;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Tuupola\Middleware\JwtAuthentication;
return function (ContainerBuilder $containerBuilder) {
$containerBuilder->addDefinitions([
// Repositories
'UsersRepository' => function (ContainerInterface $c) {
return new MockUsersRepository($c);
},
return [
// Repositories
'UsersRepository' => function (ContainerInterface $c) {
return new MockUsersRepository($c);
},
'GroupsRepository' => function (ContainerInterface $c) {
return new MockGroupsRepository($c);
},
'GroupsRepository' => function (ContainerInterface $c) {
return new MockGroupsRepository($c);
},
// Data access classes
'UsersDataAccess' => function () {
return new MockUserDataAccess();
},
// Data access classes
'UsersDataAccess' => function () {
return new MockUserDataAccess();
},
'GroupsDataAccess' => function () {
return new MockGroupDataAccess();
},
'GroupsDataAccess' => function () {
return new MockGroupDataAccess();
},
// Adapters
'UsersAdapter' => function () {
return new MockUserAdapter();
},
// Adapters
'UsersAdapter' => function () {
return new MockUserAdapter();
},
'GroupsAdapter' => function () {
return new MockGroupAdapter();
}
]);
};
'GroupsAdapter' => function () {
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);
$response = $this->responseFactory->createResponse($statusCode);
$response = $response->withHeader('Content-Type', 'application/scim+json');
$response->getBody()->write($payload);
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;
// TODO: This is currently a dummy class to demonstrate how to add custom SCIM resources to the codebase
class Domain
use Opf\Models\SCIM\Standard\CommonEntity;
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\Models\SCIM\Standard\CommonEntity;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
class CoreGroup extends CommonEntity
{
@ -47,14 +48,27 @@ class CoreGroup extends CommonEntity
$this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null);
$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']));
} else {
$meta->setCreated(Util::dateTime2string(new \DateTime('NOW')));
}
}*/
$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);
}
@ -65,17 +79,18 @@ class CoreGroup extends CommonEntity
'schemas' => [Util::GROUP_SCHEMA],
'id' => $this->getId(),
'externalId' => $this->getExternalId(),
'meta' => [
'resourceType' => $this->getMeta()->getResourceType(),
'created' => $this->getMeta()->getCreated(),
'meta' => null !== $this->getMeta() ? [
'resourceType' => null !== $this->getMeta()->getResourceType()
? $this->getMeta()->getResourceType() : null,
'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null,
'location' => $baseLocation . '/Groups/' . $this->getId(),
'version' => $this->getMeta()->getVersion()
],
'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null
] : null,
'displayName' => $this->getDisplayName(),
'members' => $this->getMembers()
];
if (null !== $this->getMeta()->getLastModified()) {
if (null !== $this->getMeta() && null !== $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);
$name = new Name();
$name->setFamilyName($data['name']['familyName']);
$name->setFormatted($data['name']['formatted']);
$name->setGivenName($data['name']['givenName']);
$name->setHonorificPrefix($data['name']['honorificPrefix']);
$name->setHonorificSuffix($data['name']['honorificSuffix']);
if (isset($data['name']) && !empty($data['name'])) {
if (isset($data['name']['familyName']) && !empty($data['name']['familyName'])) {
$name->setFamilyName($data['name']['familyName']);
}
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);
$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']));
} else {
$meta->setCreated(Util::dateTime2string(new \DateTime('NOW')));
}
}*/
$this->setMeta($meta);
$this->setActive(isset($data['active']) ? $data['active'] : true);
@ -377,7 +396,7 @@ class CoreUser extends CommonEntity
'preferredLanguage' => $this->getPreferredLanguage(),
'locale' => $this->getLocale(),
'timezone' => $this->getTimezone(),
'active' => $this->getActive(),
'active' => boolval($this->getActive()),
'password' => $this->getPassword(),
'emails' => $this->getEmails(),
'phoneNumbers' => $this->getPhoneNumbers(),

View file

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

View file

@ -15,9 +15,68 @@ abstract class Repository
$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 update(string $id, $object): ?object;
abstract public function delete(string $id): bool;
}

View file

@ -2,190 +2,120 @@
namespace Opf\Repositories\Users;
use Monolog\Logger;
use Opf\Models\SCIM\Standard\Users\CoreUser;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Repositories\Repository;
use Opf\Util\Filters\FilterUtil;
use Psr\Container\ContainerInterface;
class MockUsersRepository 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
{
public function getAll(
$filter = '',
$startIndex = 0,
$count = 0,
$attributes = [],
$excludedAttributes = []
): array {
// Read all mock users from the database
$mockUsers = $this->dataAccess::all();
$mockUsers = $this->dataAccess->getAll();
$scimUsers = [];
// Transform each mock user to a SCIM user via the injected adapter
foreach ($mockUsers as $mockUser) {
// TODO: Possibly refactor the transformation logic between SCIM users and other users
// 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());
$scimUser = $this->adapter->getCoreUser($mockUser);
$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;
}
public function getOneByUserName(string $userName): ?CoreUser
{
if (isset($userName) && !empty($userName)) {
// Try to find the first user from the database with the supplied username
$mockUser = $this->dataAccess::where('userName', $userName)->first();
public function getOneById(
string $id,
$filter = '',
$startIndex = 0,
$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($mockUser) && !empty($mockUser)) {
$this->adapter->setUser($mockUser);
if (isset($filter) && !empty($filter)) {
// Pass the single user as an array of an array, representing the user
$scimUsersToFilter = array($scimUser->toSCIM(false));
$filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter);
if (!empty($filteredScimData)) {
$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());
$scimUser->fromSCIM($filteredScimData[0]);
return $scimUser;
} else {
return null;
}
}
}
public function getOneById(string $id): ?CoreUser
{
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;
}
return $scimUser;
}
public function create($object): ?CoreUser
{
if (isset($object) && !empty($object)) {
// Transform the incoming JSON user object to a SCIM object
// Then transform the SCIM object to a mock user that can be stored in the database
$scimUser = new CoreUser();
$scimUser->fromSCIM($object);
$scimUserToCreate = new CoreUser();
$scimUserToCreate->fromSCIM($object);
// $this->dataAccess represents an instance of the MockUser ORM model
// that we use for user storage to and retrieval from SQLite
$this->adapter->setUser($this->dataAccess);
$mockUserToCreate = $this->adapter->getMockUser($scimUserToCreate);
$this->adapter->setId($scimUser->getId());
$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());
$mockUserCreated = $this->dataAccess->create($mockUserToCreate);
// Obtain the transformed mock user from the adapter and try to save it to the database
$this->dataAccess = $this->adapter->getUser();
if ($this->dataAccess->save()) {
return $scimUser;
} else {
return null;
}
if (isset($mockUserCreated)) {
return $this->adapter->getCoreUser($mockUserCreated);
}
return null;
}
public function update(string $id, $object): ?CoreUser
{
if (isset($id) && !empty($id)) {
// Try to find the user with the supplied ID
$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);
$scimUserToUpdate = new CoreUser();
$scimUserToUpdate->fromSCIM($object);
// Set the adapter's internal user object to the found user from the database
$this->adapter->setUser($mockUser);
$mockUserToUpdate = $this->adapter->getMockUser($scimUserToUpdate);
// Set the SCIM user's ID to be the same as the ID of the found user from the database
// Otherwise, we might lose the ID if a new one is supplied in the request
$scimUser->setId($this->adapter->getId());
$mockUserUpdated = $this->dataAccess->update($id, $mockUserToUpdate);
// Transform the SCIM object to a mock user via the adapter and replace
// any properties of the mock user with the new properties incoming via the SCIM object
$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;
if (isset($mockUserUpdated)) {
return $this->adapter->getCoreUser($mockUserUpdated);
}
return null;
}
public function delete(string $id): bool
{
if (isset($id) && !empty($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;
}
return $this->dataAccess->delete($id);
}
}

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;
use Opf\Models\SCIM\Standard\Service\CoreResourceType;
use Opf\Models\SCIM\Standard\Service\CoreSchemaExtension;
use Exception;
use PDO;
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 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 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";
@ -104,12 +113,20 @@ abstract class Util
isset($config['db']['driver']) && !empty($config['db']['driver'])
&& isset($config['db']['host']) && !empty($config['db']['host'])
&& 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='
. $config['db']['host'] . ';port='
. $config['db']['port'] . ';dbname='
. $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;
}
/**
* 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)
{
$parts = explode("@", $email);
@ -146,18 +212,147 @@ abstract class Util
*/
public static function getConfigFile()
{
$defaultConfigFilePath = dirname(__DIR__) . '/../config/config.default.php';
$customConfigFilePath = dirname(__DIR__) . '/../config/config.php';
$config = [];
// In case we don't have a custom config, we just rely on the default one
if (!file_exists($customConfigFilePath)) {
$config = require($defaultConfigFilePath);
if (!file_exists(self::$customConfigFilePath)) {
$config = require(self::$defaultConfigFilePath);
} else {
$config = require($customConfigFilePath);
$config = require(self::$customConfigFilePath);
}
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
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\DeleteGroupAction;
use Opf\Controllers\Groups\GetGroupAction;
use Opf\Controllers\Groups\ListGroupsAction;
use Opf\Controllers\Groups\UpdateGroupAction;
use Opf\Controllers\JWT\GenerateJWTAction;
use Opf\Controllers\ServiceProviders\ListResourceTypesAction;
use Opf\Controllers\ServiceProviders\ListSchemasAction;
use Opf\Controllers\ServiceProviders\ListServiceProviderConfigurationsAction;
@ -41,6 +45,14 @@ return function (App $app) {
$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
$app->get('/ResourceTypes', ListResourceTypesAction::class)->setName('resourceTypes.list');
$app->get('/Schemas', ListSchemasAction::class)->setName('schemas.list');
@ -48,7 +60,4 @@ return function (App $app) {
'/ServiceProviderConfig',
ListServiceProviderConfigurationsAction::class
)->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",
"values": [
{
@ -7,9 +7,21 @@
"value": "http://localhost:8888",
"type": "default",
"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_exported_at": "2022-04-14T14:54:36.929Z",
"_postman_exported_using": "Postman/9.15.2"
}
"_postman_exported_at": "2022-10-07T10:05:35.659Z",
"_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": {
"_postman_id": "73043646-f766-4adc-96ee-05316cc59bdd",
"_postman_id": "c90f5107-b2fb-46dc-9a32-b07f5ff68440",
"name": "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": "Users",
"item": [
@ -138,7 +172,8 @@
"});",
"",
"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"
@ -160,6 +195,154 @@
},
"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",
"event": [
@ -403,7 +586,8 @@
"});",
"",
"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"
@ -425,6 +609,154 @@
},
"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",
"event": [
@ -579,6 +911,8 @@
"listen": "test",
"script": {
"exec": [
"jsonData = pm.response.json();",
"",
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
@ -588,31 +922,30 @@
"});",
"",
"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.expect(pm.response.json().Resources.length).to.eql(5);",
"pm.test(\"Response body contains exactly four entries\", () => {",
" 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.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.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.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.expect(pm.response.json().Resources[3].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: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\");",
" isContained = jsonData.Resources.some((resource) => resource.id === \"urn:ietf:params:scim:schemas:core:2.0:User\");",
"});"
],
"type": "text/javascript"
@ -684,6 +1017,16 @@
]
}
],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{jwt_token}}",
"type": "string"
}
]
},
"event": [
{
"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;
use Illuminate\Database\Capsule\Manager;
use Opf\Adapters\Groups\MockGroupAdapter;
use Opf\DataAccess\Groups\MockGroupDataAccess;
use Opf\Models\SCIM\Standard\Groups\CoreGroup;
use Opf\Util\Util;
use PHPUnit\Framework\TestCase;
use SQLite3;
final class MockGroupsDataAccessTest extends TestCase
{
/** @var SQLite3 */
protected $database = null;
/** @var Opf\Models\SCIM\Standard\Groups\CoreGroup */
protected $coreGroup = null;
/** @var array */
protected $dbSettings = null;
/** @var Illuminate\Database\Capsule\Manager */
protected $capsule = null;
/** @var Opf\Models\CoreGroup */
/** @var Opf\DataAccess\Groups\MockGroupDataAccess */
protected $mockGroupDataAccess = null;
/** @var Opf\Opf\Adapters\Groups\MockGroupAdapter */
protected $mockGroupAdapter = null;
public function setUp(): void
{
$this->database = new SQLite3(__DIR__ . '/../resources/test-scim-opf.sqlite');
$groupDbSql = "CREATE TABLE IF NOT EXISTS groups (
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();
Util::setConfigFile(__DIR__ . '/../resources/mock-test-config.php');
$this->coreGroup = new CoreGroup();
$this->mockGroupAdapter = new MockGroupAdapter();
$this->mockGroupDataAccess = new MockGroupDataAccess();
}
public function tearDown(): void
{
$this->coreGroup = null;
$this->mockGroupAdapter = 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()
{
$testGroupJson = json_decode(file_get_contents(__DIR__ . '/../resources/testGroup.json'), true);
$this->mockGroupDataAccess->fromSCIM($testGroupJson);
$groupCreateRes = $this->mockGroupDataAccess->save();
$this->assertTrue($groupCreateRes);
$this->coreGroup->fromSCIM($testGroupJson);
$mockGroup = $this->mockGroupAdapter->getMockGroup($this->coreGroup);
$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 Illuminate\Database\Capsule\Manager;
use Opf\Adapters\Users\MockUserAdapter;
use Opf\DataAccess\Users\MockUserDataAccess;
use Opf\Models\SCIM\Standard\Users\CoreUser;
use Opf\Util\Util;
use SQLite3;
final class MockUsersDataAccessTest extends TestCase
{
/** @var SQLite3 */
protected $database = null;
/** @var Opf\Models\SCIM\Standard\Users\CoreUser */
protected $coreUser = null;
/** @var array */
protected $dbSettings = null;
/** @var Illuminate\Database\Capsule\Manager */
protected $capsule = null;
/** @var Opf\Models\MockUser */
/** @var Opf\DataAccess\Users\MockUserDataAccess */
protected $mockUserDataAccess = null;
/** @var Opf\Adapters\Users\MockUserAdapter */
protected $mockUserAdapter = null;
public function setUp(): void
{
$this->database = new SQLite3(__DIR__ . '/../resources/test-scim-opf.sqlite');
$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();
Util::setConfigFile(__DIR__ . '/../resources/mock-test-config.php');
$this->coreUser = new CoreUser();
$this->mockUserDataAccess = new MockUserDataAccess();
$this->mockUserAdapter = new MockUserAdapter();
}
public function tearDown(): void
{
$this->coreUser = null;
$this->mockUserDataAccess = null;
$this->capsule = 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());
$this->mockUserAdapter = null;
}
public function testCreateUser()
{
$testUserJson = json_decode(file_get_contents(__DIR__ . '/../resources/testUser.json'), true);
$this->mockUserDataAccess->fromSCIM($testUserJson);
$userCreateRes = $this->mockUserDataAccess->save();
$this->assertTrue($userCreateRes);
$this->coreUser->fromSCIM($testUserJson);
$mockUser = $this->mockUserAdapter->getMockUser($this->coreUser);
$userCreateRes = $this->mockUserDataAccess->create($mockUser);
$this->assertNotNull($userCreateRes);
}
public function testReadAllUsers()
{
$this->assertNotEmpty($this->mockUserDataAccess->getAll());
}
}