├── .gitignore ├── .htaccess ├── LICENSE ├── Makefile ├── README.md ├── bin └── generate_jwt.php ├── composer.json ├── composer.lock ├── config ├── Schema │ ├── coreGroupSchema.json │ ├── coreUserSchema.json │ ├── domainSchema.json │ ├── enterpriseUserSchema.json │ ├── provisioningUserSchema.json │ ├── resourceTypeSchema.json │ └── schemaSchema.json ├── ServiceProviderConfig │ └── serviceProviderConfig.json └── config.default.php ├── db └── database.php ├── phpcs.xml ├── public ├── .htaccess └── index.php ├── src ├── Adapters │ ├── AbstractAdapter.php │ ├── Groups │ │ └── MockGroupAdapter.php │ └── Users │ │ └── MockUserAdapter.php ├── Controllers │ ├── Controller.php │ ├── Domains │ │ ├── CreateDomainAction.php │ │ ├── DeleteDomainAction.php │ │ ├── GetDomainAction.php │ │ ├── ListDomainsAction.php │ │ └── UpdateDomainAction.php │ ├── Groups │ │ ├── CreateGroupAction.php │ │ ├── DeleteGroupAction.php │ │ ├── GetGroupAction.php │ │ ├── ListGroupsAction.php │ │ └── UpdateGroupAction.php │ ├── ServiceProviders │ │ ├── ListResourceTypesAction.php │ │ ├── ListSchemasAction.php │ │ └── ListServiceProviderConfigurationsAction.php │ └── Users │ │ ├── CreateUserAction.php │ │ ├── DeleteUserAction.php │ │ ├── GetUserAction.php │ │ ├── ListUsersAction.php │ │ └── UpdateUserAction.php ├── DataAccess │ ├── Groups │ │ └── MockGroupDataAccess.php │ └── Users │ │ └── MockUserDataAccess.php ├── Dependencies │ ├── dependencies.php │ └── mock-dependencies.php ├── Handlers │ └── HttpErrorHandler.php ├── Middleware │ └── SimpleAuthMiddleware.php ├── Models │ ├── Mock │ │ ├── MockCommonEntity.php │ │ ├── MockGroup.php │ │ └── MockUser.php │ └── SCIM │ │ ├── Custom │ │ ├── Domains │ │ │ └── Domain.php │ │ └── Users │ │ │ └── ProvisioningUser.php │ │ └── Standard │ │ ├── CommonEntity.php │ │ ├── CoreCollection.php │ │ ├── Filters │ │ ├── AttributeExpression.php │ │ ├── AttributeOperator.php │ │ ├── FilterException.php │ │ └── FilterExpression.php │ │ ├── Groups │ │ └── CoreGroup.php │ │ ├── Meta.php │ │ ├── MultiValuedAttribute.php │ │ ├── Service │ │ ├── Attribute.php │ │ ├── AuthenticationScheme.php │ │ ├── Bulk.php │ │ ├── CoreResourceType.php │ │ ├── CoreSchema.php │ │ ├── CoreSchemaExtension.php │ │ ├── CoreServiceProviderConfiguration.php │ │ ├── Filter.php │ │ └── SupportableConfigProperty.php │ │ └── Users │ │ ├── Address.php │ │ ├── CoreUser.php │ │ ├── EnterpriseUser.php │ │ ├── Manager.php │ │ └── Name.php ├── Repositories │ ├── Groups │ │ └── MockGroupsRepository.php │ ├── Repository.php │ └── Users │ │ └── MockUsersRepository.php ├── ScimServer.php ├── ScimServerPhp │ └── Firebase │ │ └── JWT │ │ ├── BeforeValidException.php │ │ ├── CachedKeySet.php │ │ ├── ExpiredException.php │ │ ├── JWK.php │ │ ├── JWT.php │ │ ├── Key.php │ │ └── SignatureInvalidException.php ├── Util │ ├── Authentication │ │ ├── AuthenticatorInterface.php │ │ └── SimpleBearerAuthenticator.php │ ├── Filters │ │ ├── FilterParser.php │ │ └── FilterUtil.php │ └── Util.php └── routes.php └── test ├── integration ├── .gitignore ├── ansible.cfg ├── hosts.sample ├── tests.yml └── users.yml ├── phpunit.xml ├── postman ├── scim-env.postman_environment.json └── scim-opf.postman_collection.json ├── resources ├── filterTestGroups.json ├── filterTestUsers.json ├── mock-test-config.php ├── testGroup.json └── testUser.json └── unit ├── FilterParserTest.php ├── FilterUtilTest.php ├── MockGroupsDataAccessTest.php └── MockUsersDataAccessTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /logs/ 3 | *.sqlite 4 | *.cache 5 | /config/config.php 6 | /.idea/ 7 | -------------------------------------------------------------------------------- /.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteRule ^$ public/ [L] 3 | RewriteRule (.*) public/$1 [L] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 audriga.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Default port to start server on 2 | PORT := 8888 3 | 4 | .PHONY: clean 5 | clean: 6 | rm -rf ./vendor 7 | 8 | .PHONY: install 9 | install: 10 | composer install --prefer-dist 11 | php db/database.php 12 | 13 | .PHONY: start-server 14 | start-server: 15 | composer install --prefer-dist 16 | php db/database.php 17 | php -S localhost:$(PORT) -t public/ public/index.php 18 | 19 | # linting based on https://github.com/dbfx/github-phplint 20 | .PHONY: lint 21 | lint: 22 | # Make sure we don't lint test classes 23 | composer install --prefer-dist --no-dev 24 | 25 | # Lint for installed PHP version 26 | sh -c "! (find . -type f -name \"*.php\" -not -path \"./build/*\" -not -path \"./vendor/*\" $1 -exec php -l -n {} \; | grep -v \"No syntax errors detected\")" 27 | 28 | # Make devtools available again 29 | composer install --prefer-dist 30 | 31 | # Lint with CodeSniffer 32 | vendor/bin/phpcs --standard=phpcs.xml src/ --ignore=src/Vendor 33 | 34 | .PHONY: api_test 35 | api_test: 36 | newman run test/postman/scim-opf.postman_collection.json -e test/postman/scim-env.postman_environment.json 37 | 38 | .PHONY: unit_test 39 | unit_test: 40 | composer install --prefer-dist 41 | vendor/bin/phpunit -c test/phpunit.xml --testdox 42 | 43 | .PHONY: fulltest 44 | fulltest: lint api_test unit_test 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scim-server-php 2 | 3 | This is the Open Provisioning Framework project by audriga which makes use of the [SCIM](http://www.simplecloud.info/) protocol. 4 | 5 | --- 6 | 7 | # Table of Contents 8 | 1. [Info](#info) 9 | 1. [Related projects](#related-projects) 10 | 1. [Capabilities](#capabilities) 11 | 1. [Prerequisites](#prerequisites) 12 | 1. [Usage](#usage) 13 | 1. [Get it as a composer dependency](#get-it-as-a-composer-dependency) 14 | 1. [Try out the embedded mock server](#try-out-the-embedded-mock-server) 15 | 1. [Enable JWT authentication](#enable-jwt-authentication) 16 | 1. [Use scim-server-php for your own project](#use-scim-server-php-for-your-own-project) 17 | 1. [SCIM resources](#scim-resources) 18 | 1. [SCIM server](#scim-server) 19 | 1. [Authentication/Authorization](#authenticationauthorization) 20 | 1. [Define your authentication/authorization logic](#define-your-authenticationauthorization-logic) 21 | 1. [Define your authentication/authorization middleware](#define-your-authenticationauthorization-middleware) 22 | 1. [Add your authentication/authorization middleware to the SCIM server](#add-your-authenticationauthorization-middleware-to-the-scim-server) 23 | 1. [Full example](#full-example) 24 | 1. [Acknowledgements](#acknowledgements) 25 | 26 | --- 27 | 28 | ## Info 29 | 30 | **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. 31 | 32 | 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) 33 | 34 | This is a **work in progress** project. It already works pretty well but some features will be added in the future and some bugs may still be arround 😉 35 | 36 | The **scim-server-php** project currently includes the following: 37 | 38 | * A SCIM 2.0 server core library 39 | * An integrated Mock SCIM server based on a SQLite database. 40 | 41 | ## Related projects 42 | 43 | * 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 44 | * 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 45 | 46 | ## Capabilities 47 | 48 | This library provides: 49 | 50 | * Standard SCIM resources implementations (*Core User*, *Enterprise User* and *Groups*) 51 | * Custom SCIM resource *Provisioning User* implementation 52 | * Custom SCIM resource *Domain* implementation 53 | * Standard CRUD operations on above SCIM resources 54 | * A HTTP server handling requests and responses on defined endpoints, based on the [Slim](https://www.slimframework.com/) framework 55 | * A simple JWT implementation 56 | * When enabled, this JWT token needs to be provided in all requests using the Bearer schema (`Authorization: Bearer `) 57 | * You can generate a token with the script located at `bin/generate_jwt.php` 58 | * The secret you use *must* be also defined in your `config/config.php` file 59 | * An easily reusable code architecture for implementing SCIM servers 60 | 61 | 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**. 62 | 63 | ## Prerequisites 64 | * **scim-server-php** requires PHP 7.4 65 | * Dependencies are managed with [composer](https://getcomposer.org/) 66 | 67 | ## Usage 68 | 69 | ### Get it as a [composer](https://getcomposer.org/) dependency 70 | 71 | * You can add the following to your `composer.json` file to get it with [composer](https://getcomposer.org/) 72 | 73 | ``` 74 | "repositories": { 75 | "scim": { 76 | "type": "vcs", 77 | "url": "git@bitbucket.org:audriga/scim-server-php.git" 78 | } 79 | }, 80 | "require": { 81 | "audriga/scim-server-php": "dev-master" 82 | }, 83 | ``` 84 | 85 | * We plan to publish to [packagist](https://packagist.org/) in the future 86 | 87 | ### Try out the embedded mock server 88 | 89 | * To help you use and understand this library, a mock server is provided 90 | * Clone this repository 91 | * Run `make install` to automatically install dependencies and setup a mock database 92 | * Run `make start-server` to start a local mock SCIM server accessible on `localhost:8888` 93 | * Send your first SCIM requests! For example, try out `curl http://localhost:8888/Users` 94 | * It supports all basic CRUD operations on SCIM Core Users and Groups 95 | 96 | #### Enable JWT authentication 97 | 98 | * A very simple JWT authentication is provided 99 | * Enable it for the embedded mock server by uncommenting the 2 following lines in `public/index.php` and restart it 100 | 101 | ``` 102 | $scimServerPhpAuthMiddleware = 'AuthMiddleware'; 103 | $scimServer->setMiddleware(array($scimServerPhpAuthMiddleware)); 104 | ``` 105 | 106 | * You will now need to send a valid JWT token with all your requests to the mock server 107 | * 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` 108 | * To generate a token, use the script located at `bin/generate_jwt.php` 109 | * 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. 110 | 111 | ### Use scim-server-php for your own project 112 | 113 | #### SCIM resources 114 | 115 | * You can directly reuse the SCIM resources implementation from the `src/Models/SCIM/` folder in any PHP project 116 | * Here are the provided resources implementations 117 | * `src/Models/SCIM/Standard/Users/CoreUser.php` implements the Core User resource from the SCIM standard 118 | * `src/Models/SCIM/Standard/Users/EnterpriseUser.php` implements the Enterprise User extension from the SCIM standard 119 | * `src/Models/SCIM/Standard/Groups/CoreGroup.php` implements the Core Group resource from the SCIM standard 120 | * `src/Models/SCIM/Custom/Domains/Domain.php` implements the custom Domain resource 121 | * `src/Models/SCIM/Custom/Users/ProvisioningUser.php` implements the custom Provisioning User extension of the Core User 122 | 123 | #### SCIM server 124 | 125 | * You can use **scim-server-php** to easily create a full-fledged SCIM server for your own data source 126 | * **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 127 | * You can use the embedded mock server implementation as an example ;) 128 | * Concretelly, you will need to implement the following for each resource type of your data source 129 | * `Model` classes representing your resources 130 | * See e.g. `src/Models/Mock/MockUsers` 131 | * `DataAccess` classes defining how to access your data source 132 | * See e.g. `src/DataAccess/Users/MockUserDataAccess.php` 133 | * `Adapter` classes, extending `AbstractAdapter` and defining how to convert your resources to/from SCIM resources 134 | * See e.g. `src/Adapters/Users/MockUserAdapter.php` 135 | * `Repository` classes, extending `Opf\Repositories\Repository` and defining the operations available on your resources 136 | * See e.g. `src/Repositories/Users/MockUsersRepository.php` 137 | * 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`) 138 | 139 | * **scim-server-php** uses [Dependency Injection Container](https://php-di.org/) internally 140 | * Create a `dependencies` file reusing the pattern of `src/Dependencies/mock-dependencies.php` 141 | * The "Auth middleware" and "Authenticators" sections are explained in the [Authentication/Authorization](#authenticationauthorization) section bellow 142 | * Your `Repository` classes will get the corresponding `DataAccess` and `Adapter` classes through the **scim-server-php** container 143 | 144 | * Instantiate a `ScimServer` and feed it with your `dependencies` file as shown in `public/index.php` 145 | * The "Authentication Middleware" section is explained in the [Authentication/Authorization](#authenticationauthorization) section bellow 146 | 147 | ### Authentication/Authorization 148 | 149 | #### Define your authentication/authorization logic 150 | 151 | * Authentication is mostly delegated to the system using **scim-server-php** 152 | * A basic JWT based authentication implementation is provided as an example in `src/Util/Authentication/SimpleBearerAuthenticator` 153 | * Define your own `Authenticator` class(es) by implementing the `AuthenticatorInterface` available in `Util/Authentication` 154 | * A script generating a JWT token containing a single `user` claim is provided in `bin/generate_jwt.php` 155 | * Authorization is delegated to the system using **scim-server-php** 156 | 157 | #### Define your authentication/authorization middleware 158 | 159 | * 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 160 | * Authentication and authorization should therefore be implemented as "Middleware(s)" 161 | * This means implementing the `MiddlewareInterface` 162 | * The authentication middleware should then delegate the actual authentication process to your `Authenticator` 163 | * The authorization implementation is up to you 164 | * You can either integrate it in the `Authenticator` (and so, in the authentication middleware) 165 | * Or you can implement an independent authentication middleware 166 | * You can use `src/Middleware/SimpleAuthMiddleware` as an example 167 | 168 | #### Add your authentication/authorization middleware to the SCIM server 169 | 170 | * Add your middleware to your dependencies file 171 | * You can use `src/Dependencies/mock-dependencies.php` as an example 172 | * Note that the mock `SimpleAuthMiddleware` also uses the **scim-server-php** container to gets the authenticator to use 173 | * Hence `src/Dependencies/mock-dependencies.php` defines a `'BearerAuthenticator'` which is then used in `SimpleAuthMiddleware` 174 | 175 | ### Full example 176 | 177 | * We advise to use https://github.com/audriga/postfixadmin-scim-api as a full **scim-server-php** implementation example 178 | 179 | ## Acknowledgements 180 | 181 | 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. 182 | -------------------------------------------------------------------------------- /bin/generate_jwt.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | -s= 21 | generate_jwt.php --username= --secret= 22 | generate_jwt.php (-h | --help)\n" 23 | ); 24 | } 25 | 26 | /** 27 | * Generate a JWT for a given user 28 | * 29 | * @param string $username The username of the user we generate a JWT for 30 | * @param string $secret The JWT secret signing key 31 | * @return string The JWT of the user 32 | */ 33 | function generateJwt(string $username, string $secret): string 34 | { 35 | $jwtPayload = array( 36 | "user" => $username 37 | ); 38 | 39 | return JWT::encode($jwtPayload, $secret, "HS256"); 40 | } 41 | 42 | 43 | // Specify the CLI options, passed to getopt() 44 | $shortOptions = "hu:s:"; 45 | $longOptions = ["help", "username:", "secret:"]; 46 | 47 | // Obtain the CLI args, passed to the script via getopt() 48 | $cliOptions = getopt($shortOptions, $longOptions); 49 | 50 | // If there was some issue with the CLI args, we show the help message 51 | if ($cliOptions === false) { 52 | showUsage(); 53 | exit(1); 54 | } 55 | 56 | // We check if a username was provided 57 | if ( 58 | (isset($cliOptions["u"]) || isset($cliOptions["username"])) 59 | && (isset($cliOptions["s"]) || isset($cliOptions["secret"])) 60 | ) { 61 | $username = isset($cliOptions["u"]) ? $cliOptions["u"] : $cliOptions["username"]; 62 | $secret = isset($cliOptions["s"]) ? $cliOptions["s"] : $cliOptions["secret"]; 63 | } else { 64 | // If no username or secret was provided, we let the user know 65 | fwrite(STDERR, "A username and a secret JWT key must be provided\n"); 66 | showUsage(); 67 | exit(1); 68 | } 69 | 70 | $jwt = generateJwt($username, $secret); 71 | fwrite(STDOUT, "$jwt\n"); 72 | exit(0); 73 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "audriga/scim-server-php", 3 | "description": "An open library for SCIM servers implementation", 4 | "type": "library", 5 | "require": { 6 | "slim/slim": "^4.10", 7 | "illuminate/database": "^8.83", 8 | "php": "^7.4", 9 | "slim/php-view": "^3.1", 10 | "monolog/monolog": "^2.4", 11 | "ramsey/uuid": "^4.2", 12 | "slim/psr7": "^1.5", 13 | "php-di/php-di": "^6.3", 14 | "firebase/php-jwt": "^6.3", 15 | "coenjacobs/mozart": "^0.7.1" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Opf\\": "src/" 20 | } 21 | }, 22 | "scripts": { 23 | "post-install-cmd": [ 24 | "\"vendor/bin/mozart\" compose", 25 | "composer dump-autoload" 26 | ], 27 | "post-update-cmd": [ 28 | "\"vendor/bin/mozart\" compose", 29 | "composer dump-autoload" 30 | ] 31 | }, 32 | "authors": [ 33 | { 34 | "name": "audriga", 35 | "email": "opensource@audriga.com" 36 | } 37 | ], 38 | "require-dev": { 39 | "phpunit/phpunit": "^9.5", 40 | "squizlabs/php_codesniffer": "^3.6" 41 | }, 42 | "extra": { 43 | "mozart": { 44 | "dep_namespace": "Opf\\ScimServerPhp\\", 45 | "dep_directory": "/src/ScimServerPhp/", 46 | "classmap_directory": "/classes/scimserverphp/", 47 | "classmap_prefix": "ScimServerPhp_", 48 | "packages": [ 49 | "firebase/php-jwt" 50 | ], 51 | "delete_vendor_directories": true 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/Schema/coreGroupSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "urn:ietf:params:scim:schemas:core:2.0:Group", 3 | "name": "Group", 4 | "description": "Group", 5 | "attributes": [ 6 | { 7 | "name": "displayName", 8 | "type": "string", 9 | "multiValued": false, 10 | "description": "A human-readable name for the Group. REQUIRED.", 11 | "required": false, 12 | "caseExact": false, 13 | "mutability": "readWrite", 14 | "returned": "default", 15 | "uniqueness": "none" 16 | }, 17 | { 18 | "name": "members", 19 | "type": "complex", 20 | "multiValued": true, 21 | "description": "A list of members of the Group.", 22 | "required": false, 23 | "subAttributes": [ 24 | { 25 | "name": "value", 26 | "type": "string", 27 | "multiValued": false, 28 | "description": "Identifier of the member of this Group.", 29 | "required": false, 30 | "caseExact": false, 31 | "mutability": "immutable", 32 | "returned": "default", 33 | "uniqueness": "none" 34 | }, 35 | { 36 | "name": "$ref", 37 | "type": "reference", 38 | "referenceTypes": [ 39 | "User", 40 | "Group" 41 | ], 42 | "multiValued": false, 43 | "description": "The URI corresponding to a SCIM resource that is a member of this Group.", 44 | "required": false, 45 | "caseExact": false, 46 | "mutability": "immutable", 47 | "returned": "default", 48 | "uniqueness": "none" 49 | }, 50 | { 51 | "name": "type", 52 | "type": "string", 53 | "multiValued": false, 54 | "description": "A label indicating the type of resource, e.g., 'User' or 'Group'.", 55 | "required": false, 56 | "caseExact": false, 57 | "canonicalValues": [ 58 | "User", 59 | "Group" 60 | ], 61 | "mutability": "immutable", 62 | "returned": "default", 63 | "uniqueness": "none" 64 | } 65 | ], 66 | "mutability": "readWrite", 67 | "returned": "default" 68 | } 69 | ], 70 | "meta": { 71 | "resourceType": "Schema", 72 | "location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group" 73 | } 74 | } -------------------------------------------------------------------------------- /config/Schema/domainSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "urn:ietf:params:scim:schema:audriga:core:2.0:Domain", 3 | "name": "Domain", 4 | "description": "Domain", 5 | "attributes": [ 6 | { 7 | "name": "domainName", 8 | "type": "string", 9 | "multiValued": false, 10 | "description": "The name of the domain. REQUIRED.", 11 | "required": true, 12 | "caseExact": false, 13 | "mutability": "readWrite", 14 | "returned": "default", 15 | "uniqueness": "server" 16 | }, 17 | { 18 | "name": "description", 19 | "type": "string", 20 | "multiValued": false, 21 | "description": "A description of the domain. OPTIONAL.", 22 | "required": false, 23 | "caseExact": false, 24 | "mutability": "readWrite", 25 | "returned": "default", 26 | "uniqueness": "none" 27 | }, 28 | { 29 | "name": "maxAliases", 30 | "type": "int", 31 | "multiValued": false, 32 | "description": "The maximum number of aliases of the domain. OPTIONAL.", 33 | "required": false, 34 | "caseExact": false, 35 | "mutability": "readWrite", 36 | "returned": "default", 37 | "uniqueness": "none" 38 | }, 39 | { 40 | "name": "maxMailboxes", 41 | "type": "int", 42 | "multiValued": false, 43 | "description": "The maximum number of mailboxes the domain can have. OPTIONAL.", 44 | "required": false, 45 | "caseExact": false, 46 | "mutability": "readWrite", 47 | "returned": "default", 48 | "uniqueness": "none" 49 | }, 50 | { 51 | "name": "maxQuota", 52 | "type": "int", 53 | "multiValued": false, 54 | "description": "The maximum quota, allowed for mailboxes of the domain (in MB). OPTIONAL.", 55 | "required": false, 56 | "caseExact": false, 57 | "mutability": "readWrite", 58 | "returned": "default", 59 | "uniqueness": "none" 60 | }, 61 | { 62 | "name": "usedQuota", 63 | "type": "int", 64 | "multiValued": false, 65 | "description": "The currently used quota by the mailboxes of the domain (in MB). OPTIONAL.", 66 | "required": false, 67 | "caseExact": false, 68 | "mutability": "readOnly", 69 | "returned": "default", 70 | "uniqueness": "none" 71 | }, 72 | { 73 | "name": "active", 74 | "type": "bool", 75 | "multiValued": false, 76 | "description": "A flag indicating whether the domain is currently active. REQUIRED.", 77 | "required": true, 78 | "caseExact": false, 79 | "mutability": "readWrite", 80 | "returned": "default", 81 | "uniqueness": "none" 82 | } 83 | ], 84 | "meta": { 85 | "resourceType": "Schema", 86 | "location": "/v2/Schemas/urn:ietf:params:scim:schema:audriga:core:2.0:Domain" 87 | } 88 | } -------------------------------------------------------------------------------- /config/Schema/enterpriseUserSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User", 3 | "name": "EnterpriseUser", 4 | "description": "Enterprise User", 5 | "attributes": [ 6 | { 7 | "name": "employeeNumber", 8 | "type": "string", 9 | "multiValued": false, 10 | "description": "Numeric or alphanumeric identifier assigned to a person, typically based on order of hire or association with an organization.", 11 | "required": false, 12 | "caseExact": false, 13 | "mutability": "readWrite", 14 | "returned": "default", 15 | "uniqueness": "none" 16 | }, 17 | { 18 | "name": "costCenter", 19 | "type": "string", 20 | "multiValued": false, 21 | "description": "Identifies the name of a cost center.", 22 | "required": false, 23 | "caseExact": false, 24 | "mutability": "readWrite", 25 | "returned": "default", 26 | "uniqueness": "none" 27 | }, 28 | { 29 | "name": "organization", 30 | "type": "string", 31 | "multiValued": false, 32 | "description": "Identifies the name of an organization.", 33 | "required": false, 34 | "caseExact": false, 35 | "mutability": "readWrite", 36 | "returned": "default", 37 | "uniqueness": "none" 38 | }, 39 | { 40 | "name": "division", 41 | "type": "string", 42 | "multiValued": false, 43 | "description": "Identifies the name of a division.", 44 | "required": false, 45 | "caseExact": false, 46 | "mutability": "readWrite", 47 | "returned": "default", 48 | "uniqueness": "none" 49 | }, 50 | { 51 | "name": "department", 52 | "type": "string", 53 | "multiValued": false, 54 | "description": "Identifies the name of a department.", 55 | "required": false, 56 | "caseExact": false, 57 | "mutability": "readWrite", 58 | "returned": "default", 59 | "uniqueness": "none" 60 | }, 61 | { 62 | "name": "manager", 63 | "type": "complex", 64 | "multiValued": false, 65 | "description": "The User's manager. A complex type that optionally allows service providers to represent organizational hierarchy by referencing the 'id' attribute of another User.", 66 | "required": false, 67 | "subAttributes": [ 68 | { 69 | "name": "value", 70 | "type": "string", 71 | "multiValued": false, 72 | "description": "The id of the SCIM resource representing the User's manager. REQUIRED.", 73 | "required": false, 74 | "caseExact": false, 75 | "mutability": "readWrite", 76 | "returned": "default", 77 | "uniqueness": "none" 78 | }, 79 | { 80 | "name": "$ref", 81 | "type": "reference", 82 | "referenceTypes": [ 83 | "User" 84 | ], 85 | "multiValued": false, 86 | "description": "The URI of the SCIM resource representing the User's manager. REQUIRED.", 87 | "required": false, 88 | "caseExact": false, 89 | "mutability": "readWrite", 90 | "returned": "default", 91 | "uniqueness": "none" 92 | }, 93 | { 94 | "name": "displayName", 95 | "type": "string", 96 | "multiValued": false, 97 | "description": "The displayName of the User's manager. OPTIONAL and READ-ONLY.", 98 | "required": false, 99 | "caseExact": false, 100 | "mutability": "readOnly", 101 | "returned": "default", 102 | "uniqueness": "none" 103 | } 104 | ], 105 | "mutability": "readWrite", 106 | "returned": "default" 107 | } 108 | ], 109 | "meta": { 110 | "resourceType": "Schema", 111 | "location": "/v2/Schemas/urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" 112 | } 113 | } -------------------------------------------------------------------------------- /config/Schema/provisioningUserSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User", 3 | "name": "ProvisioningUser", 4 | "description": "Provisioning User", 5 | "attributes": [ 6 | { 7 | "name": "sizeQuota", 8 | "type": "int", 9 | "multiValued": false, 10 | "description": "Size quota assigned to a user, in bytes", 11 | "required": false, 12 | "caseExact": false, 13 | "mutability": "readWrite", 14 | "returned": "default", 15 | "uniqueness": "none" 16 | } 17 | ], 18 | "meta": { 19 | "resourceType": "Schema", 20 | "location": "/v2/Schemas/urn:audriga:params:scim:schemas:extension:provisioning:2.0:User" 21 | } 22 | } -------------------------------------------------------------------------------- /config/Schema/resourceTypeSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType", 3 | "name": "ResourceType", 4 | "description": "Specifies the schema that describes a SCIM resource type", 5 | "attributes": [ 6 | { 7 | "name": "id", 8 | "type": "string", 9 | "multiValued": false, 10 | "description": "The resource type's server unique id. May be the same as the 'name' attribute.", 11 | "required": false, 12 | "caseExact": false, 13 | "mutability": "readOnly", 14 | "returned": "default", 15 | "uniqueness": "none" 16 | }, 17 | { 18 | "name": "name", 19 | "type": "string", 20 | "multiValued": false, 21 | "description": "The resource type name. When applicable, service providers MUST specify the name, e.g., 'User'.", 22 | "required": true, 23 | "caseExact": false, 24 | "mutability": "readOnly", 25 | "returned": "default", 26 | "uniqueness": "none" 27 | }, 28 | { 29 | "name": "description", 30 | "type": "string", 31 | "multiValued": false, 32 | "description": "The resource type's human-readable description. When applicable, service providers MUST specify the description.", 33 | "required": false, 34 | "caseExact": false, 35 | "mutability": "readOnly", 36 | "returned": "default", 37 | "uniqueness": "none" 38 | }, 39 | { 40 | "name": "endpoint", 41 | "type": "reference", 42 | "referenceTypes": [ 43 | "uri" 44 | ], 45 | "multiValued": false, 46 | "description": "The resource type's HTTP-addressable endpoint relative to the Base URL, e.g., '/Users'.", 47 | "required": true, 48 | "caseExact": false, 49 | "mutability": "readOnly", 50 | "returned": "default", 51 | "uniqueness": "none" 52 | }, 53 | { 54 | "name": "schema", 55 | "type": "reference", 56 | "referenceTypes": [ 57 | "uri" 58 | ], 59 | "multiValued": false, 60 | "description": "The resource type's primary/base schema URI.", 61 | "required": true, 62 | "caseExact": true, 63 | "mutability": "readOnly", 64 | "returned": "default", 65 | "uniqueness": "none" 66 | }, 67 | { 68 | "name": "schemaExtensions", 69 | "type": "complex", 70 | "multiValued": false, 71 | "description": "A list of URIs of the resource type's schema extensions.", 72 | "required": true, 73 | "mutability": "readOnly", 74 | "returned": "default", 75 | "subAttributes": [ 76 | { 77 | "name": "schema", 78 | "type": "reference", 79 | "referenceTypes": [ 80 | "uri" 81 | ], 82 | "multiValued": false, 83 | "description": "The URI of a schema extension.", 84 | "required": true, 85 | "caseExact": true, 86 | "mutability": "readOnly", 87 | "returned": "default", 88 | "uniqueness": "none" 89 | }, 90 | { 91 | "name": "required", 92 | "type": "boolean", 93 | "multiValued": false, 94 | "description": "A Boolean value that specifies whether or not the schema extension is required for the resource type. If true, a resource of this type MUST include this schema extension and also include any attributes declared as required in this schema extension. If false, a resource of this type MAY omit this schema extension.", 95 | "required": true, 96 | "mutability": "readOnly", 97 | "returned": "default" 98 | } 99 | ] 100 | } 101 | ] 102 | } -------------------------------------------------------------------------------- /config/ServiceProviderConfig/serviceProviderConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemas": [ 3 | "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig" 4 | ], 5 | "documentationUri": null, 6 | "patch": { 7 | "supported": false 8 | }, 9 | "bulk": { 10 | "supported": false, 11 | "maxOperations": 0, 12 | "maxPayloadSize": 0 13 | }, 14 | "filter": { 15 | "supported": true, 16 | "maxResults": 200 17 | }, 18 | "changePassword": { 19 | "supported": false 20 | }, 21 | "sort": { 22 | "supported": false 23 | }, 24 | "etag": { 25 | "supported": false 26 | }, 27 | "authenticationSchemes": [ 28 | { 29 | "name": "HTTP Basic", 30 | "description": "Authentication scheme using the HTTP Basic Standard", 31 | "specUri": "http://www.rfc-editor.org/info/rfc2617", 32 | "documentationUri": null, 33 | "type": "httpbasic" 34 | }, 35 | { 36 | "name": "OAuth Bearer Token", 37 | "description": "Authentication scheme using the OAuth Bearer Token Standard", 38 | "specUri": "http://www.rfc-editor.org/info/rfc6750", 39 | "documentationUri": "http://example.com/help/oauth.html", 40 | "type": "oauthbearertoken", 41 | "primary": true 42 | } 43 | ], 44 | "meta": { 45 | "location": "https://example.com/v2/ServiceProviderConfig", 46 | "resourceType": "ServiceProviderConfig", 47 | "created": "2010-01-23T04:56:22Z", 48 | "lastModified": "2011-05-13T04:42:34Z", 49 | "version": "W\/\"3694e05e9dff594\"" 50 | } 51 | } -------------------------------------------------------------------------------- /config/config.default.php: -------------------------------------------------------------------------------- 1 | false, // Set to true when deploying in production 5 | 'basePath' => '', // If you want to specify a base path for the Slim app, add it here (e.g., '/test/scim') 6 | 'supportedResourceTypes' => ['User', 'Group'], // Specify all the supported SCIM ResourceTypes by their names here 7 | 8 | // SQLite DB settings 9 | 'db' => [ 10 | 'driver' => 'sqlite', // Type of DB 11 | 'databaseFile' => 'db/scim-mock.sqlite' // DB name 12 | ], 13 | 14 | // MySQL DB settings 15 | //'db' => [ 16 | // 'driver' => 'mysql', // Type of DB 17 | // 'host' => 'localhost', // DB host 18 | // 'port' => '3306', // Port on DB host 19 | // 'database' => 'db_name', // DB name 20 | // 'user' => 'db_user', // DB user 21 | // 'password' => 'db_password' // DB user's password 22 | //], 23 | 24 | // Monolog settings 25 | 'logger' => [ 26 | 'name' => 'scim-opf', 27 | 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log', 28 | 'level' => \Monolog\Logger::DEBUG, 29 | ], 30 | 31 | // Bearer token settings 32 | 'jwt' => [ 33 | 'secret' => 'secret' 34 | ] 35 | ]; 36 | -------------------------------------------------------------------------------- /db/database.php: -------------------------------------------------------------------------------- 1 | exec("DROP TABLE users"); 7 | // $database->exec("DROP TABLE groups"); 8 | 9 | $user_db_sql = "CREATE TABLE IF NOT EXISTS users ( 10 | id varchar(160) NOT NULL UNIQUE, 11 | userName varchar(160) NOT NULL, 12 | active BOOLEAN NOT NULL DEFAULT 1, 13 | externalId varchar(160) NULL, 14 | profileUrl varchar(160) NULL, 15 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 16 | updated_at DATETIME NULL 17 | )"; 18 | 19 | $database->exec($user_db_sql); 20 | 21 | $group_db_sql = "CREATE TABLE IF NOT EXISTS groups ( 22 | id varchar(160) NOT NULL UNIQUE, 23 | displayName varchar(160) NOT NULL DEFAULT '', 24 | members TEXT NOT NULL DEFAULT '', 25 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 | updated_at DATETIME NULL 27 | )"; 28 | 29 | $database->exec($group_db_sql); 30 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | The coding standard for audriga. 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule ^ index.php [QSA,L] 5 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | setConfig($configFilePath); 16 | 17 | // Obtain custom dependencies (if any) and pass them to the scimServer instance 18 | $dependencies = require __DIR__ . '/../src/Dependencies/mock-dependencies.php'; 19 | $scimServer->setDependencies($dependencies); 20 | 21 | // Set the Authentication Middleware configured in the dependencies files above to the scimServer instance 22 | //$scimServerPhpAuthMiddleware = 'AuthMiddleware'; 23 | //$scimServer->setMiddleware(array($scimServerPhpAuthMiddleware)); 24 | 25 | // Start the scimServer 26 | $scimServer->run(); 27 | -------------------------------------------------------------------------------- /src/Adapters/AbstractAdapter.php: -------------------------------------------------------------------------------- 1 | setId($mockGroup->getId()); 21 | 22 | $coreGroupMeta = new Meta(); 23 | $coreGroupMeta->setResourceType("Group"); 24 | $coreGroupMeta->setCreated($mockGroup->getCreatedAt()); 25 | $coreGroupMeta->setLastModified($mockGroup->getUpdatedAt()); 26 | $coreGroup->setMeta($coreGroupMeta); 27 | 28 | $coreGroup->setDisplayName($mockGroup->getDisplayName()); 29 | 30 | if ($mockGroup->getMembers() !== null && !empty($mockGroup->getMembers())) { 31 | $coreGroupMembers = []; 32 | foreach ($mockGroup->getMembers() as $mockGroupMember) { 33 | $coreGroupMember = new MultiValuedAttribute(); 34 | $coreGroupMember->setValue($mockGroupMember->getValue()); 35 | $coreGroupMember->setDisplay($mockGroupMember->getDisplay()); 36 | $coreGroupMember->setRef($mockGroupMember->getRef()); 37 | $coreGroupMembers[] = $coreGroupMember; 38 | } 39 | 40 | $coreGroup->setMembers($coreGroupMembers); 41 | } 42 | 43 | return $coreGroup; 44 | } 45 | 46 | public function getMockGroup(?CoreGroup $coreGroup): ?MockGroup 47 | { 48 | if (!isset($coreGroup)) { 49 | return null; 50 | } 51 | 52 | $mockGroup = new MockGroup(); 53 | $mockGroup->setId($coreGroup->getId()); 54 | 55 | if ($coreGroup->getMeta() !== null) { 56 | $mockGroup->setCreatedAt($coreGroup->getMeta()->getCreated()); 57 | $mockGroup->setUpdatedAt($coreGroup->getMeta()->getLastModified()); 58 | } 59 | 60 | $mockGroup->setDisplayName($coreGroup->getDisplayName()); 61 | 62 | if ($coreGroup->getMembers() !== null && !empty($coreGroup->getMembers())) { 63 | $mockGroupMembers = []; 64 | foreach ($coreGroup->getMembers() as $coreGroupMember) { 65 | $mockGroupMember = new MultiValuedAttribute(); 66 | $mockGroupMember->setValue($coreGroupMember->getValue()); 67 | $mockGroupMember->setDisplay($coreGroupMember->getDisplay()); 68 | $mockGroupMember->setRef($coreGroupMember->getRef()); 69 | $mockGroupMembers[] = $mockGroupMember; 70 | } 71 | 72 | $mockGroup->setMembers($mockGroupMembers); 73 | } 74 | return $mockGroup; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Adapters/Users/MockUserAdapter.php: -------------------------------------------------------------------------------- 1 | setId($mockUser->getId()); 20 | $coreUser->setExternalId($mockUser->getExternalId()); 21 | 22 | $coreUserMeta = new Meta(); 23 | $coreUserMeta->setResourceType("User"); 24 | $coreUserMeta->setCreated($mockUser->getCreatedAt()); 25 | $coreUserMeta->setLastModified($mockUser->getUpdatedAt()); 26 | $coreUser->setMeta($coreUserMeta); 27 | 28 | $coreUser->setUserName($mockUser->getUserName()); 29 | $coreUser->setActive(boolval($mockUser->getActive())); 30 | $coreUser->setProfileUrl($mockUser->getProfileUrl()); 31 | 32 | return $coreUser; 33 | } 34 | 35 | public function getMockUser(?CoreUser $coreUser): ?MockUser 36 | { 37 | if (!isset($coreUser)) { 38 | return null; 39 | } 40 | 41 | $mockUser = new MockUser(); 42 | $mockUser->setId($coreUser->getId()); 43 | 44 | if ($coreUser->getMeta() !== null) { 45 | $mockUser->setCreatedAt($coreUser->getMeta()->getCreated()); 46 | $mockUser->setUpdatedAt($coreUser->getMeta()->getLastModified()); 47 | } 48 | 49 | $mockUser->setUserName($coreUser->getUserName()); 50 | $mockUser->setActive(boolval($coreUser->getActive())); 51 | $mockUser->setExternalId($coreUser->getExternalId()); 52 | $mockUser->setProfileUrl($coreUser->getProfileUrl()); 53 | 54 | return $mockUser; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | container = $container; 21 | $this->logger = $this->container->get(Logger::class); 22 | 23 | $config = Util::getConfigFile(); 24 | if (isset($config['basePath']) && !empty($config['basePath'])) { 25 | $this->basePath = $config['basePath']; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Controllers/Domains/CreateDomainAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('DomainsRepository'); 18 | } 19 | 20 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 21 | { 22 | $this->logger->info("CREATE Domain"); 23 | $this->logger->info($request->getBody()); 24 | 25 | $uri = $request->getUri(); 26 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 27 | 28 | try { 29 | $domain = $this->repository->create($request->getParsedBody()); 30 | 31 | if (isset($domain) && !empty($domain)) { 32 | $this->logger->info("Created domain / domain=" . $domain->getId()); 33 | 34 | $scimDomain = $domain->toSCIM(false, $baseUrl); 35 | 36 | $responseBody = json_encode($scimDomain, JSON_UNESCAPED_SLASHES); 37 | $this->logger->info($responseBody); 38 | $response = new Response($status = 201); 39 | $response->getBody()->write($responseBody); 40 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 41 | return $response; 42 | } else { 43 | $this->logger->error("Error creating domain"); 44 | $errorResponseBody = json_encode( 45 | ["Errors" => ["description" => "Error creating domain", "code" => 400]] 46 | ); 47 | $response = new Response($status = 400); 48 | $response->getBody()->write($errorResponseBody); 49 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 50 | return $response; 51 | } 52 | } catch (Exception $e) { 53 | $this->logger->error("Error creating domain: " . $e->getMessage()); 54 | $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); 55 | $response = new Response($status = 400); 56 | $response->getBody()->write($errorResponseBody); 57 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 58 | return $response; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Controllers/Domains/DeleteDomainAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('DomainsRepository'); 16 | } 17 | 18 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 19 | { 20 | $this->logger->info("DELETE Domain"); 21 | $id = $request->getAttribute('id'); 22 | $this->logger->info("ID: " . $id); 23 | $deleteRes = $this->repository->delete($id); 24 | if (!$deleteRes) { 25 | $this->logger->info("Not found"); 26 | return $response->withStatus(404); 27 | } 28 | $this->logger->info("Domain deleted"); 29 | 30 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 31 | return $response->withStatus(204); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Controllers/Domains/GetDomainAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('DomainsRepository'); 17 | } 18 | 19 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 20 | { 21 | $this->logger->info("GET Domain"); 22 | $uri = $request->getUri(); 23 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 24 | 25 | $id = $request->getAttribute('id'); 26 | $this->logger->info("ID: " . $id); 27 | $domain = $this->repository->getOneById($id); 28 | if (!isset($domain) || empty($domain)) { 29 | $this->logger->info("Not found"); 30 | return $response->withStatus(404); 31 | } 32 | 33 | $scimDomain = $domain->toSCIM(false, $baseUrl); 34 | 35 | $responseBody = json_encode($scimDomain, JSON_UNESCAPED_SLASHES); 36 | $this->logger->info($responseBody); 37 | $response = new Response($status = 200); 38 | $response->getBody()->write($responseBody); 39 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 40 | return $response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controllers/Domains/ListDomainsAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('DomainsRepository'); 18 | } 19 | 20 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 21 | { 22 | $this->logger->info("GET Domains"); 23 | $filter = ''; 24 | if (!empty($request->getQueryParams()['filter'])) { 25 | $this->logger->info("Filter --> " . $request->getQueryParams()['filter']); 26 | $filter = $request->getQueryParams()['filter']; 27 | } 28 | $uri = $request->getUri(); 29 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 30 | 31 | $domains = []; 32 | $domains = $this->repository->getAll($filter); 33 | 34 | $scimDomains = []; 35 | if (!empty($domains)) { 36 | foreach ($domains as $domain) { 37 | $scimDomains[] = $domain->toSCIM(false, $baseUrl); 38 | } 39 | } 40 | $scimDomainCollection = (new CoreCollection($scimDomains))->toSCIM(false); 41 | 42 | $responseBody = json_encode($scimDomainCollection, JSON_UNESCAPED_SLASHES); 43 | $this->logger->info($responseBody); 44 | $response = new Response($status = 200); 45 | $response->getBody()->write($responseBody); 46 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Controllers/Domains/UpdateDomainAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('DomainsRepository'); 18 | } 19 | 20 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 21 | { 22 | $this->logger->info("UPDATE Domain"); 23 | $this->logger->info($request->getBody()); 24 | 25 | $uri = $request->getUri(); 26 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 27 | 28 | $id = $request->getAttribute('id'); 29 | $this->logger->info("ID: " . $id); 30 | 31 | // Try to find a domain with the supplied ID 32 | // and if it doesn't exist, return a 404 33 | $domain = $this->repository->getOneById($id); 34 | if (!isset($domain) || empty($domain)) { 35 | $this->logger->info("Not found"); 36 | return $response->withStatus(404); 37 | } 38 | 39 | try { 40 | $domain = $this->repository->update($id, $request->getParsedBody()); 41 | if (isset($domain) && !empty($domain)) { 42 | $scimDomain = $domain->toSCIM(false, $baseUrl); 43 | 44 | $responseBody = json_encode($scimDomain, JSON_UNESCAPED_SLASHES); 45 | $this->logger->info($responseBody); 46 | $response = new Response($status = 200); 47 | $response->getBody()->write($responseBody); 48 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 49 | return $response; 50 | } else { 51 | $this->logger->error("Error updating domain"); 52 | $errorResponseBody = json_encode( 53 | ["Errors" => ["decription" => "Error updating domain", "code" => 400]] 54 | ); 55 | $response = new Response($status = 400); 56 | $response->getBody()->write($errorResponseBody); 57 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 58 | return $response; 59 | } 60 | } catch (Exception $e) { 61 | $this->logger->error("Error updating domain: " . $e->getMessage()); 62 | $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); 63 | $response = new Response($status = 400); 64 | $response->getBody()->write($errorResponseBody); 65 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 66 | return $response; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Controllers/Groups/CreateGroupAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('GroupsRepository'); 17 | } 18 | 19 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 20 | { 21 | $this->logger->info("CREATE Group"); 22 | $this->logger->info($request->getBody()); 23 | 24 | $uri = $request->getUri(); 25 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 26 | 27 | try { 28 | $group = $this->repository->create($request->getParsedBody()); 29 | 30 | if (isset($group) && !empty($group)) { 31 | $scimGroup = $group->toSCIM(false, $baseUrl); 32 | 33 | $responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES); 34 | $this->logger->info($responseBody); 35 | $response = new Response($status = 201); 36 | $response->getBody()->write($responseBody); 37 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 38 | return $response; 39 | } else { 40 | $this->logger->error("Error creating group"); 41 | $errorResponseBody = json_encode( 42 | [ 43 | "Errors" => [ 44 | "description" => "Error creating group", "code" => 400 45 | ] 46 | ] 47 | ); 48 | $response = new Response($status = 400); 49 | $response->getBody()->write($errorResponseBody); 50 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 51 | return $response; 52 | } 53 | } catch (\Exception $e) { 54 | $this->logger->error("Error creating group: " . $e->getMessage()); 55 | $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); 56 | $response = new Response($status = 400); 57 | $response->getBody()->write($errorResponseBody); 58 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 59 | return $response; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Controllers/Groups/DeleteGroupAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('GroupsRepository'); 16 | } 17 | 18 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 19 | { 20 | $this->logger->info("DELETE Group"); 21 | $id = $request->getAttribute('id'); 22 | $this->logger->info("ID: " . $id); 23 | $deleteRes = $this->repository->delete($id); 24 | if (!$deleteRes) { 25 | $this->logger->info("Not found"); 26 | return $response->withStatus(404); 27 | } 28 | $this->logger->info("Group deleted"); 29 | 30 | return $response->withStatus(200); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Controllers/Groups/GetGroupAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('GroupsRepository'); 17 | } 18 | 19 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 20 | { 21 | $this->logger->info("GET Group"); 22 | $uri = $request->getUri(); 23 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 24 | 25 | $id = $request->getAttribute('id'); 26 | $this->logger->info("ID: " . $id); 27 | $group = $this->repository->getOneById($id); 28 | if (!isset($group) || empty($group)) { 29 | $this->logger->info("Not found"); 30 | return $response->withStatus(404); 31 | } 32 | 33 | $scimGroup = $group->toSCIM(false, $baseUrl); 34 | 35 | $responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES); 36 | $this->logger->info($responseBody); 37 | $response = new Response($status = 200); 38 | $response->getBody()->write($responseBody); 39 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 40 | return $response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controllers/Groups/ListGroupsAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('GroupsRepository'); 18 | } 19 | 20 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 21 | { 22 | $this->logger->info("GET Groups"); 23 | $filter = ''; 24 | if (!empty($request->getQueryParams()['filter'])) { 25 | $this->logger->info("Filter --> " . $request->getQueryParams()['filter']); 26 | $filter = $request->getQueryParams()['filter']; 27 | } 28 | $uri = $request->getUri(); 29 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 30 | 31 | $groups = []; 32 | $groups = $this->repository->getAll($filter); 33 | 34 | $scimGroups = []; 35 | if (!empty($groups)) { 36 | foreach ($groups as $group) { 37 | $scimGroups[] = $group->toSCIM(false, $baseUrl); 38 | } 39 | } 40 | $scimGroupCollection = (new CoreCollection($scimGroups))->toSCIM(false); 41 | 42 | $responseBody = json_encode($scimGroupCollection, JSON_UNESCAPED_SLASHES); 43 | $this->logger->info($responseBody); 44 | $response = new Response($status = 200); 45 | $response->getBody()->write($responseBody); 46 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 47 | return $response; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Controllers/Groups/UpdateGroupAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('GroupsRepository'); 18 | } 19 | 20 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 21 | { 22 | $this->logger->info("UPDATE Group"); 23 | $this->logger->info($request->getBody()); 24 | 25 | $uri = $request->getUri(); 26 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 27 | 28 | $id = $request->getAttribute('id'); 29 | $this->logger->info("ID: " . $id); 30 | 31 | $group = $this->repository->getOneById($id); 32 | if (!isset($group) || empty($group)) { 33 | $this->logger->info("Not found"); 34 | return $response->withStatus(404); 35 | } 36 | 37 | try { 38 | $group = $this->repository->update($id, $request->getParsedBody()); 39 | if (isset($group) && !empty($group)) { 40 | $scimGroup = $group->toSCIM(false, $baseUrl); 41 | 42 | $responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES); 43 | $this->logger->info($responseBody); 44 | $response = new Response($status = 201); 45 | $response->getBody()->write($responseBody); 46 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 47 | return $response; 48 | } else { 49 | $this->logger->error("Error updating group"); 50 | $errorResponseBody = json_encode(["Errors" => ["decription" => "Error updating group", "code" => 400]]); 51 | $response = new Response($status = 400); 52 | $response->getBody()->write($errorResponseBody); 53 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 54 | return $response; 55 | } 56 | } catch (\Exception $e) { 57 | $this->logger->error("Error updating group: " . $e->getMessage()); 58 | $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); 59 | $response = new Response($status = 400); 60 | $response->getBody()->write($errorResponseBody); 61 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 62 | return $response; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Controllers/ServiceProviders/ListResourceTypesAction.php: -------------------------------------------------------------------------------- 1 | logger->info("GET ResourceTypes"); 20 | 21 | $uri = $request->getUri(); 22 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 23 | 24 | $scimResourceTypes = Util::getResourceTypes($baseUrl); 25 | $scimResourceTypeCollection = (new CoreCollection($scimResourceTypes))->toSCIM(false); 26 | 27 | $responseBody = json_encode($scimResourceTypeCollection, JSON_UNESCAPED_SLASHES); 28 | $this->logger->info($responseBody); 29 | $response = new Response($status = 200); 30 | $response->getBody()->write($responseBody); 31 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Controllers/ServiceProviders/ListSchemasAction.php: -------------------------------------------------------------------------------- 1 | logger->info("GET Schemas"); 17 | 18 | $scimSchemas = Util::getSchemas(); 19 | 20 | // If there were no schemas found, return 404 21 | if (is_null($scimSchemas)) { 22 | $this->logger->info("No Schemas found"); 23 | $response = new Response($status = 404); 24 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 25 | 26 | return $response; 27 | } 28 | 29 | $scimSchemasCollection = (new CoreCollection($scimSchemas))->toSCIM(false); 30 | 31 | $responseBody = json_encode($scimSchemasCollection, JSON_UNESCAPED_SLASHES); 32 | $this->logger->info($responseBody); 33 | $response = new Response($status = 200); 34 | $response->getBody()->write($responseBody); 35 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 36 | 37 | return $response; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Controllers/ServiceProviders/ListServiceProviderConfigurationsAction.php: -------------------------------------------------------------------------------- 1 | logger->info("GET ServiceProviderConfigurations"); 16 | 17 | $scimServiceProviderConfiguration = Util::getServiceProviderConfig(); 18 | 19 | if (is_null($scimServiceProviderConfiguration)) { 20 | $this->logger->info("No ServiceProviderConfiguration found"); 21 | $response = new Response($status = 404); 22 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 23 | 24 | return $response; 25 | } 26 | 27 | $responseBody = $scimServiceProviderConfiguration; 28 | $this->logger->info($responseBody); 29 | $response = new Response($status = 200); 30 | $response->getBody()->write($responseBody); 31 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 32 | 33 | return $response; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Controllers/Users/CreateUserAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('UsersRepository'); 17 | } 18 | 19 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 20 | { 21 | $this->logger->info("CREATE User"); 22 | $this->logger->info($request->getBody()); 23 | 24 | $uri = $request->getUri(); 25 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 26 | 27 | try { 28 | $user = $this->repository->create($request->getParsedBody()); 29 | 30 | if (isset($user) && !empty($user)) { 31 | $this->logger->info("Created user / username=" . $user->getUserName() . " / ID=" . $user->getId()); 32 | 33 | $scimUser = $user->toSCIM(false, $baseUrl); 34 | 35 | $responseBody = json_encode($scimUser, JSON_UNESCAPED_SLASHES); 36 | $this->logger->info($responseBody); 37 | $response = new Response($status = 201); 38 | $response->getBody()->write($responseBody); 39 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 40 | return $response; 41 | } else { 42 | $this->logger->error("Error creating user"); 43 | $errorResponseBody = json_encode(["Errors" => ["description" => "Error creating user", "code" => 400]]); 44 | $response = new Response($status = 400); 45 | $response->getBody()->write($errorResponseBody); 46 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 47 | return $response; 48 | } 49 | } catch (\Exception $e) { 50 | $this->logger->error("Error creating user: " . $e->getMessage()); 51 | $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); 52 | $response = new Response($status = 400); 53 | $response->getBody()->write($errorResponseBody); 54 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 55 | return $response; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Controllers/Users/DeleteUserAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('UsersRepository'); 16 | } 17 | 18 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 19 | { 20 | $this->logger->info("DELETE User"); 21 | $id = $request->getAttribute('id'); 22 | $this->logger->info("ID: " . $id); 23 | $deleteRes = $this->repository->delete($id); 24 | if (!$deleteRes) { 25 | $this->logger->info("Not found"); 26 | return $response->withStatus(404); 27 | } 28 | $this->logger->info("User deleted"); 29 | 30 | $response = $response->withHeader("Content-Type", "application/scim+json"); 31 | return $response->withStatus(204); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Controllers/Users/GetUserAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('UsersRepository'); 17 | } 18 | 19 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 20 | { 21 | $this->logger->info("GET User"); 22 | $uri = $request->getUri(); 23 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 24 | 25 | $id = $request->getAttribute('id'); 26 | $this->logger->info("ID: " . $id); 27 | $user = $this->repository->getOneById($id); 28 | if (!isset($user) || empty($user)) { 29 | $this->logger->info("Not found"); 30 | return $response->withStatus(404); 31 | } 32 | 33 | $scimUser = $user->toSCIM(false, $baseUrl); 34 | 35 | $responseBody = json_encode($scimUser, JSON_UNESCAPED_SLASHES); 36 | $this->logger->info($responseBody); 37 | $response = new Response($status = 200); 38 | $response->getBody()->write($responseBody); 39 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 40 | return $response; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controllers/Users/ListUsersAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('UsersRepository'); 19 | } 20 | 21 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 22 | { 23 | $this->logger->info("GET Users"); 24 | $filter = ''; 25 | if (!empty($request->getQueryParams()['filter'])) { 26 | $this->logger->info("Filter --> " . $request->getQueryParams()['filter']); 27 | $filter = $request->getQueryParams()['filter']; 28 | } 29 | $uri = $request->getUri(); 30 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 31 | 32 | $userName = null; 33 | $users = []; 34 | $users = $this->repository->getAll($filter); 35 | 36 | $scimUsers = []; 37 | if (!empty($users)) { 38 | foreach ($users as $user) { 39 | $scimUsers[] = $user->toSCIM(false, $baseUrl); 40 | } 41 | } 42 | $scimUserCollection = (new CoreCollection($scimUsers))->toSCIM(false); 43 | 44 | $responseBody = json_encode($scimUserCollection, JSON_UNESCAPED_SLASHES); 45 | $this->logger->info($responseBody); 46 | $response = new Response($status = 200); 47 | $response->getBody()->write($responseBody); 48 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 49 | return $response; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Controllers/Users/UpdateUserAction.php: -------------------------------------------------------------------------------- 1 | repository = $this->container->get('UsersRepository'); 17 | } 18 | 19 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface 20 | { 21 | $this->logger->info("UPDATE User"); 22 | $this->logger->info($request->getBody()); 23 | 24 | $uri = $request->getUri(); 25 | $baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath); 26 | 27 | $id = $request->getAttribute('id'); 28 | $this->logger->info("ID: " . $id); 29 | 30 | // Try to find a user with the supplied ID 31 | // and if it doesn't exist, return a 404 32 | $user = $this->repository->getOneById($id); 33 | if (!isset($user) || empty($user)) { 34 | $this->logger->info("Not found"); 35 | return $response->withStatus(404); 36 | } 37 | 38 | try { 39 | $user = $this->repository->update($id, $request->getParsedBody()); 40 | if (isset($user) && !empty($user)) { 41 | $scimUser = $user->toSCIM(false, $baseUrl); 42 | 43 | $responseBody = json_encode($scimUser, JSON_UNESCAPED_SLASHES); 44 | $this->logger->info($responseBody); 45 | $response = new Response($status = 200); 46 | $response->getBody()->write($responseBody); 47 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 48 | return $response; 49 | } else { 50 | $this->logger->error("Error updating user"); 51 | $errorResponseBody = json_encode(["Errors" => ["decription" => "Error updating user", "code" => 400]]); 52 | $response = new Response($status = 400); 53 | $response->getBody()->write($errorResponseBody); 54 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 55 | return $response; 56 | } 57 | } catch (\Exception $e) { 58 | $this->logger->error("Error updating user: " . $e->getMessage()); 59 | $errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]); 60 | $response = new Response($status = 400); 61 | $response->getBody()->write($errorResponseBody); 62 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 63 | return $response; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/DataAccess/Groups/MockGroupDataAccess.php: -------------------------------------------------------------------------------- 1 | logger = new Logger(MockGroupDataAccess::class); 25 | $this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG)); 26 | 27 | // Try to obtain a DSN via the Util class and complain with an Exception if there's no DSN 28 | $dsn = Util::buildDbDsn(); 29 | if (!isset($dsn)) { 30 | throw new Exception("Can't obtain DSN to connect to DB"); 31 | } 32 | 33 | // Create the DB connection with PDO (no need to pass username or password for mock DB) 34 | $this->dbConnection = new PDO($dsn, null, null); 35 | 36 | // Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations 37 | $this->dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 38 | } 39 | 40 | public function getAll(): ?array 41 | { 42 | if (isset($this->dbConnection)) { 43 | $selectStatement = $this->dbConnection->query("SELECT * from groups"); 44 | if ($selectStatement) { 45 | $mockGroups = []; 46 | $mockGroupsRaw = $selectStatement->fetchAll(PDO::FETCH_ASSOC); 47 | foreach ($mockGroupsRaw as $group) { 48 | $mockGroup = new MockGroup(); 49 | $mockGroup->mapFromArray($group); 50 | $mockGroups[] = $mockGroup; 51 | } 52 | return $mockGroups; 53 | } 54 | 55 | $this->logger->error("Couldn't read all groups from mock DB. SELECT query to DB failed"); 56 | return null; 57 | } 58 | } 59 | 60 | public function getOneById($id): ?MockGroup 61 | { 62 | if (isset($id) && !empty($id)) { 63 | if (isset($this->dbConnection)) { 64 | try { 65 | $selectOnePreparedStatement = $this->dbConnection->prepare( 66 | "SELECT * FROM groups WHERE id = ?" 67 | ); 68 | 69 | $selectRes = $selectOnePreparedStatement->execute([$id]); 70 | 71 | if ($selectRes) { 72 | $mockGroupsRaw = $selectOnePreparedStatement->fetchAll(PDO::FETCH_ASSOC); 73 | if ($mockGroupsRaw) { 74 | $mockGroup = new MockGroup(); 75 | $mockGroup->mapFromArray($mockGroupsRaw[0]); 76 | return $mockGroup; 77 | } else { 78 | return null; 79 | } 80 | } else { 81 | return null; 82 | } 83 | } catch (PDOException $e) { 84 | $this->logger->error($e->getMessage()); 85 | } 86 | } 87 | } 88 | 89 | $this->logger->error( 90 | "Argument provided to getOneById in class " . MockGroupDataAccess::class . " is not set or empty" 91 | ); 92 | return null; 93 | } 94 | 95 | public function create(MockGroup $groupToCreate): ?MockGroup 96 | { 97 | $dateNow = date('Y-m-d H:i:s'); 98 | 99 | if (isset($this->dbConnection)) { 100 | try { 101 | $insertStatement = $this->dbConnection->prepare( 102 | "INSERT INTO groups 103 | (id, displayName, members, created_at, updated_at) 104 | VALUES (?, ?, ?, ?, ?)" 105 | ); 106 | 107 | $groupToCreate->setId(Util::genUuid()); 108 | 109 | $insertRes = $insertStatement->execute([ 110 | $groupToCreate->getId(), 111 | $groupToCreate->getDisplayName(), 112 | // we serialize the whole members array and store it as is in the database 113 | // this is relatively dirty, but fine enough for a mock group implementation 114 | $groupToCreate->getMembers() !== null && !empty($groupToCreate->getMembers()) 115 | ? serialize($groupToCreate->getMembers()) : "", 116 | $dateNow, 117 | $dateNow 118 | ]); 119 | 120 | if ($insertRes) { 121 | $this->logger->info("Created group " . $groupToCreate->getDisplayName()); 122 | return $groupToCreate; 123 | } else { 124 | return null; 125 | } 126 | } catch (PDOException $e) { 127 | $this->logger->error($e->getMessage()); 128 | } 129 | } else { 130 | $this->logger->error("DB connection not available"); 131 | } 132 | $this->logger->error("Error creating group"); 133 | return null; 134 | } 135 | 136 | public function update(string $id, MockGroup $groupToUpdate): ?MockGroup 137 | { 138 | $dateNow = date('Y-m-d H:i:s'); 139 | 140 | if (isset($this->dbConnection)) { 141 | try { 142 | $query = ""; 143 | $values = array(); 144 | 145 | if ($groupToUpdate->getDisplayName() !== null) { 146 | $query = $query . "displayName = ?, "; 147 | $values[] = $groupToUpdate->getDisplayName(); 148 | } 149 | 150 | if ($groupToUpdate->getMembers() !== null) { 151 | $query = $query . "members = ?, "; 152 | 153 | // We need to transform the string array of user IDs to a single string 154 | $values[] = serialize($groupToUpdate->getMembers()); 155 | } 156 | 157 | if (empty($query)) { 158 | $this->logger->error("No group properties to update"); 159 | return null; 160 | } 161 | 162 | $query = $query . "updated_at = ? "; 163 | $values[] = $dateNow; 164 | $values[] = $id; 165 | 166 | $updateStatement = $this->dbConnection->prepare( 167 | "UPDATE groups SET " . $query . " WHERE id = ?" 168 | ); 169 | 170 | $updateRes = $updateStatement->execute($values); 171 | 172 | if ($updateRes) { 173 | $this->logger->info("Updated group " . $id); 174 | return $this->getOneById($id); 175 | } else { 176 | $this->logger->error("Error updating group " . $id); 177 | return null; 178 | } 179 | } catch (PDOException $e) { 180 | $this->logger->error($e->getMessage()); 181 | } 182 | } else { 183 | $this->logger->error("Error updating group " . $id . " - DB connection unavailable"); 184 | } 185 | $this->logger->error("Error updating group " . $id); 186 | return null; 187 | } 188 | 189 | public function delete($id): bool 190 | { 191 | if (isset($this->dbConnection)) { 192 | try { 193 | $deleteStatement = $this->dbConnection->prepare( 194 | "DELETE FROM groups WHERE id = ?" 195 | ); 196 | $deleteRes = $deleteStatement->execute([$id]); 197 | 198 | // In case the delete was successful, return true 199 | if ($deleteRes) { 200 | $this->logger->info("Deleted group " . $id); 201 | return true; 202 | } else { 203 | return false; 204 | } 205 | } catch (PDOException $e) { 206 | $this->logger->error($e->getMessage()); 207 | } 208 | } else { 209 | $this->logger->error("Error deleting group " . $id . " - DB connection unavailable"); 210 | } 211 | $this->logger->error("Error deleting group " . $id); 212 | return false; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/DataAccess/Users/MockUserDataAccess.php: -------------------------------------------------------------------------------- 1 | logger = new Logger(MockUserDataAccess::class); 25 | $this->logger->pushHandler(new StreamHandler(__DIR__ . '/../../../logs/app.log', Logger::DEBUG)); 26 | 27 | // Try to obtain a DSN via the Util class and complain with an Exception if there's no DSN 28 | $dsn = Util::buildDbDsn(); 29 | if (!isset($dsn)) { 30 | throw new Exception("Can't obtain DSN to connect to DB"); 31 | } 32 | 33 | // Create the DB connection with PDO (no need to pass username or password for mock DB) 34 | $this->dbConnection = new PDO($dsn, null, null); 35 | 36 | // Tell PDO explicitly to throw exceptions on errors, so as to have more info when debugging DB operations 37 | $this->dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 38 | } 39 | 40 | public function getAll(): ?array 41 | { 42 | if (isset($this->dbConnection)) { 43 | $selectStatement = $this->dbConnection->query("SELECT * from users"); 44 | if ($selectStatement) { 45 | $mockUsers = []; 46 | $mockUsersRaw = $selectStatement->fetchAll(PDO::FETCH_ASSOC); 47 | foreach ($mockUsersRaw as $user) { 48 | $mockUser = new MockUser(); 49 | $mockUser->mapFromArray($user); 50 | $mockUsers[] = $mockUser; 51 | } 52 | return $mockUsers; 53 | } 54 | 55 | $this->logger->error("Couldn't read all users from mock DB. SELECT query to DB failed"); 56 | return null; 57 | } 58 | } 59 | 60 | public function getOneById($id): ?MockUser 61 | { 62 | if (isset($id) && !empty($id)) { 63 | if (isset($this->dbConnection)) { 64 | try { 65 | $selectOnePreparedStatement = $this->dbConnection->prepare( 66 | "SELECT * FROM users WHERE id = ?" 67 | ); 68 | 69 | $selectRes = $selectOnePreparedStatement->execute([$id]); 70 | 71 | if ($selectRes) { 72 | $mockUsersRaw = $selectOnePreparedStatement->fetchAll(PDO::FETCH_ASSOC); 73 | if ($mockUsersRaw) { 74 | $mockUser = new MockUser(); 75 | $mockUser->mapFromArray($mockUsersRaw[0]); 76 | return $mockUser; 77 | } else { 78 | return null; 79 | } 80 | } else { 81 | return null; 82 | } 83 | } catch (PDOException $e) { 84 | $this->logger->error($e->getMessage()); 85 | } 86 | } 87 | } 88 | 89 | $this->logger->error( 90 | "Argument provided to getOneById in class " . MockUserDataAccess::class . " is not set or empty" 91 | ); 92 | return null; 93 | } 94 | 95 | public function create(MockUser $userToCreate): ?MockUser 96 | { 97 | $dateNow = date('Y-m-d H:i:s'); 98 | 99 | if (isset($this->dbConnection)) { 100 | try { 101 | $insertStatement = $this->dbConnection->prepare( 102 | "INSERT INTO users 103 | (id, userName, active, externalId, profileUrl, created_at, updated_at) 104 | VALUES (?, ?, ?, ?, ?, ?, ?)" 105 | ); 106 | 107 | $userToCreate->setId(Util::genUuid()); 108 | 109 | $insertRes = $insertStatement->execute([ 110 | $userToCreate->getId(), 111 | $userToCreate->getUserName(), 112 | $userToCreate->getActive(), 113 | $userToCreate->getExternalId(), 114 | $userToCreate->getProfileUrl(), 115 | $dateNow, 116 | $dateNow 117 | ]); 118 | 119 | if ($insertRes) { 120 | $this->logger->info("Created user " . $userToCreate->getUserName()); 121 | return $userToCreate; 122 | } else { 123 | return null; 124 | } 125 | } catch (PDOException $e) { 126 | $this->logger->error($e->getMessage()); 127 | } 128 | } else { 129 | $this->logger->error("DB connection not available"); 130 | } 131 | $this->logger->error("Error creating user"); 132 | return null; 133 | } 134 | 135 | public function update(string $id, MockUser $userToUpdate): ?MockUser 136 | { 137 | $dateNow = date('Y-m-d H:i:s'); 138 | 139 | if (isset($this->dbConnection)) { 140 | try { 141 | $query = ""; 142 | $values = array(); 143 | 144 | if ($userToUpdate->getUserName() !== null) { 145 | $query = $query . "userName = ?, "; 146 | $values[] = $userToUpdate->getUserName(); 147 | } 148 | 149 | if ($userToUpdate->getActive() !== null) { 150 | $query = $query . "active = ?, "; 151 | $values[] = $userToUpdate->getActive(); 152 | } 153 | 154 | if ($userToUpdate->getProfileUrl() !== null) { 155 | $query = $query . "profileUrl = ?, "; 156 | $values[] = $userToUpdate->getProfileUrl(); 157 | } 158 | 159 | if ($userToUpdate->getExternalId() !== null) { 160 | $query = $query . "externalId = ?, "; 161 | $values[] = $userToUpdate->getExternalId(); 162 | } 163 | 164 | if (empty($query)) { 165 | $this->logger->error("No user properties to update"); 166 | return null; 167 | } 168 | 169 | $query = $query . "updated_at = ? "; 170 | $values[] = $dateNow; 171 | $values[] = $id; 172 | 173 | $updateStatement = $this->dbConnection->prepare( 174 | "UPDATE users SET " . $query . " WHERE id = ?" 175 | ); 176 | 177 | $updateRes = $updateStatement->execute($values); 178 | 179 | if ($updateRes) { 180 | $this->logger->info("Updated user " . $id); 181 | return $this->getOneById($id); 182 | } else { 183 | $this->logger->error("Error updating user " . $id); 184 | return null; 185 | } 186 | } catch (PDOException $e) { 187 | $this->logger->error($e->getMessage()); 188 | } 189 | } else { 190 | $this->logger->error("Error updating user " . $id . " - DB connection unavailable"); 191 | } 192 | $this->logger->error("Error updating user " . $id); 193 | return null; 194 | } 195 | 196 | public function delete($id): bool 197 | { 198 | if (isset($this->dbConnection)) { 199 | try { 200 | $deleteStatement = $this->dbConnection->prepare( 201 | "DELETE FROM users WHERE id = ?" 202 | ); 203 | $deleteRes = $deleteStatement->execute([$id]); 204 | 205 | // In case the delete was successful, return true 206 | if ($deleteRes) { 207 | $this->logger->info("Deleted user " . $id); 208 | return true; 209 | } else { 210 | return false; 211 | } 212 | } catch (PDOException $e) { 213 | $this->logger->error($e->getMessage()); 214 | } 215 | } else { 216 | $this->logger->error("Error deleting user " . $id . " - DB connection unavailable"); 217 | } 218 | $this->logger->error("Error deleting user " . $id); 219 | return false; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/Dependencies/dependencies.php: -------------------------------------------------------------------------------- 1 | function () { 13 | $config = Util::getConfigFile(); 14 | $settings = $config['logger']; 15 | $logger = new Monolog\Logger($settings['name']); 16 | $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level'])); 17 | return $logger; 18 | }, 19 | 20 | // Controllers 21 | Controller::class => function (ContainerInterface $c) { 22 | return new Controller($c); 23 | } 24 | ]; 25 | -------------------------------------------------------------------------------- /src/Dependencies/mock-dependencies.php: -------------------------------------------------------------------------------- 1 | function (ContainerInterface $c) { 18 | return new MockUsersRepository($c); 19 | }, 20 | 21 | 'GroupsRepository' => function (ContainerInterface $c) { 22 | return new MockGroupsRepository($c); 23 | }, 24 | 25 | // Data access classes 26 | 'UsersDataAccess' => function () { 27 | return new MockUserDataAccess(); 28 | }, 29 | 30 | 'GroupsDataAccess' => function () { 31 | return new MockGroupDataAccess(); 32 | }, 33 | 34 | // Adapters 35 | 'UsersAdapter' => function () { 36 | return new MockUserAdapter(); 37 | }, 38 | 39 | 'GroupsAdapter' => function () { 40 | return new MockGroupAdapter(); 41 | }, 42 | 43 | // Auth middleware 44 | 'AuthMiddleware' => function (ContainerInterface $c) { 45 | return new SimpleAuthMiddleware($c); 46 | }, 47 | 48 | // Authenticators (used by SimpleAuthMiddleware) 49 | 'BearerAuthenticator' => function (ContainerInterface $c) { 50 | return new SimpleBearerAuthenticator($c); 51 | } 52 | ]; 53 | -------------------------------------------------------------------------------- /src/Handlers/HttpErrorHandler.php: -------------------------------------------------------------------------------- 1 | exception; 30 | $statusCode = 500; 31 | $type = self::SERVER_ERROR; 32 | $description = 'An internal error has occurred while processing your request.'; 33 | 34 | if ($exception instanceof HttpException) { 35 | $statusCode = $exception->getCode(); 36 | $description = $exception->getMessage(); 37 | 38 | if ($exception instanceof HttpNotFoundException) { 39 | $type = self::RESOURCE_NOT_FOUND; 40 | } elseif ($exception instanceof HttpMethodNotAllowedException) { 41 | $type = self::NOT_ALLOWED; 42 | } elseif ($exception instanceof HttpUnauthorizedException) { 43 | $type = self::UNAUTHENTICATED; 44 | } elseif ($exception instanceof HttpForbiddenException) { 45 | $type = self::UNAUTHENTICATED; 46 | } elseif ($exception instanceof HttpBadRequestException) { 47 | $type = self::BAD_REQUEST; 48 | } elseif ($exception instanceof HttpNotImplementedException) { 49 | $type = self::NOT_IMPLEMENTED; 50 | } 51 | } 52 | 53 | if ( 54 | !($exception instanceof HttpException) 55 | && ($exception instanceof Exception || $exception instanceof Throwable) 56 | && $this->displayErrorDetails 57 | ) { 58 | $description = $exception->getMessage(); 59 | } 60 | 61 | $error = [ 62 | 'statusCode' => $statusCode, 63 | 'error' => [ 64 | 'type' => $type, 65 | 'description' => $description, 66 | ], 67 | ]; 68 | 69 | $payload = json_encode($error, JSON_PRETTY_PRINT); 70 | 71 | $response = $this->responseFactory->createResponse($statusCode); 72 | $response = $response->withHeader('Content-Type', 'application/scim+json'); 73 | $response->getBody()->write($payload); 74 | 75 | return $response; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Middleware/SimpleAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | bearerAuthenticator = $container->get('BearerAuthenticator'); 21 | } 22 | 23 | public function process(Request $request, RequestHandler $handler): Response 24 | { 25 | // If no 'Authorization' header supplied, we directly return a 401 26 | if (!$request->hasHeader('Authorization')) { 27 | return new Response(401); 28 | } 29 | 30 | // $request->getHeader() gives back a string array, hence the need for [0] 31 | $authHeader = $request->getHeader('Authorization')[0]; 32 | 33 | // Obtain the auth type and the supplied credentials 34 | $authHeaderSplit = explode(' ', $authHeader); 35 | $authType = $authHeaderSplit[0]; 36 | $authCredentials = $authHeaderSplit[1]; 37 | 38 | // This is a flag that tracks whether auth succeeded or not 39 | $isAuthSuccessful = false; 40 | 41 | $authorizationInfo = []; 42 | 43 | // Call the right authenticator, based on the auth type 44 | if (strcmp($authType, 'Bearer') === 0) { 45 | $isAuthSuccessful = $this->bearerAuthenticator->authenticate($authCredentials, $authorizationInfo); 46 | } 47 | 48 | // If everything went fine, let the request pass through 49 | if ($isAuthSuccessful) { 50 | return $handler->handle($request); 51 | } 52 | 53 | // If something didn't go right so far, then return a 401 54 | return new Response(401); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Models/Mock/MockCommonEntity.php: -------------------------------------------------------------------------------- 1 | id; 19 | } 20 | 21 | public function setId($id) 22 | { 23 | $this->id = $id; 24 | } 25 | 26 | public function getCreatedAt() 27 | { 28 | return $this->createdAt; 29 | } 30 | 31 | public function setCreatedAt($createdAt) 32 | { 33 | $this->createdAt = $createdAt; 34 | } 35 | 36 | public function getUpdatedAt() 37 | { 38 | return $this->updatedAt; 39 | } 40 | 41 | public function setUpdatedAt($updatedAt) 42 | { 43 | $this->updatedAt = $updatedAt; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Models/Mock/MockGroup.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (strcasecmp($key, 'id') === 0) { 19 | $this->id = $value; 20 | continue; 21 | } 22 | 23 | if (strcasecmp($key, 'created_at') === 0) { 24 | $this->createdAt = $value; 25 | continue; 26 | } 27 | 28 | if (strcasecmp($key, 'updated_at') === 0) { 29 | $this->updatedAt = $value; 30 | continue; 31 | } 32 | 33 | if (strcasecmp($key, 'displayName') === 0) { 34 | $this->displayName = $value; 35 | continue; 36 | } 37 | 38 | if (strcasecmp($key, 'members') === 0) { 39 | // the members array is stored as a serialized array in the DB 40 | $this->members = unserialize($value); 41 | continue; 42 | } 43 | $result = false; 44 | } 45 | } else { 46 | $result = false; 47 | } 48 | return $result; 49 | } 50 | 51 | public function getDisplayName() 52 | { 53 | return $this->displayName; 54 | } 55 | 56 | public function setDisplayName($displayName) 57 | { 58 | $this->displayName = $displayName; 59 | } 60 | 61 | public function getMembers() 62 | { 63 | return $this->members; 64 | } 65 | 66 | public function setMembers($members) 67 | { 68 | $this->members = $members; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Models/Mock/MockUser.php: -------------------------------------------------------------------------------- 1 | $value) { 24 | if (strcasecmp($key, 'id') === 0) { 25 | $this->id = $value; 26 | continue; 27 | } 28 | 29 | if (strcasecmp($key, 'created_at') === 0) { 30 | $this->createdAt = $value; 31 | continue; 32 | } 33 | 34 | if (strcasecmp($key, 'updated_at') === 0) { 35 | $this->updatedAt = $value; 36 | continue; 37 | } 38 | 39 | if (strcasecmp($key, 'userName') === 0) { 40 | $this->userName = $value; 41 | continue; 42 | } 43 | 44 | if (strcasecmp($key, 'active') === 0) { 45 | if ($value === "1") { 46 | $this->active = true; 47 | } elseif ($value === "0") { 48 | $this->active = false; 49 | } else { 50 | $this->active = $value; 51 | } 52 | continue; 53 | } 54 | 55 | if (strcasecmp($key, 'externalId') === 0) { 56 | $this->externalId = $value; 57 | continue; 58 | } 59 | 60 | if (strcasecmp($key, 'profileUrl') === 0) { 61 | $this->profileUrl = $value; 62 | continue; 63 | } 64 | $result = false; 65 | } 66 | } else { 67 | $result = false; 68 | } 69 | return $result; 70 | } 71 | 72 | public function getUserName() 73 | { 74 | return $this->userName; 75 | } 76 | 77 | public function setUserName($userName) 78 | { 79 | $this->userName = $userName; 80 | } 81 | 82 | public function getActive() 83 | { 84 | return $this->active; 85 | } 86 | 87 | public function setActive($active) 88 | { 89 | $this->active = $active; 90 | } 91 | 92 | public function getExternalId() 93 | { 94 | return $this->externalId; 95 | } 96 | 97 | public function setExternalId($externalId) 98 | { 99 | $this->externalId = $externalId; 100 | } 101 | 102 | public function getProfileUrl() 103 | { 104 | return $this->profileUrl; 105 | } 106 | 107 | public function setProfileUrl($profileUrl) 108 | { 109 | $this->profileUrl = $profileUrl; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Models/SCIM/Custom/Domains/Domain.php: -------------------------------------------------------------------------------- 1 | domainName; 38 | } 39 | 40 | /** 41 | * @param string|null $domainName 42 | */ 43 | public function setDomainName(?string $domainName): void 44 | { 45 | $this->domainName = $domainName; 46 | } 47 | 48 | /** 49 | * @return string|null 50 | */ 51 | public function getDescription(): ?string 52 | { 53 | return $this->description; 54 | } 55 | 56 | /** 57 | * @param string|null $description 58 | */ 59 | public function setDescription(?string $description): void 60 | { 61 | $this->description = $description; 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | public function getMaxAliases(): int 68 | { 69 | return $this->maxAliases; 70 | } 71 | 72 | /** 73 | * @param int $maxAliases 74 | */ 75 | public function setMaxAliases(int $maxAliases): void 76 | { 77 | $this->maxAliases = $maxAliases; 78 | } 79 | 80 | /** 81 | * @return int 82 | */ 83 | public function getMaxMailboxes(): int 84 | { 85 | return $this->maxMailboxes; 86 | } 87 | 88 | /** 89 | * @param int $maxMailboxes 90 | */ 91 | public function setMaxMailboxes(int $maxMailboxes): void 92 | { 93 | $this->maxMailboxes = $maxMailboxes; 94 | } 95 | 96 | /** 97 | * @return int 98 | */ 99 | public function getMaxQuota(): int 100 | { 101 | return $this->maxQuota; 102 | } 103 | 104 | /** 105 | * @param int $maxQuota 106 | */ 107 | public function setMaxQuota(int $maxQuota): void 108 | { 109 | $this->maxQuota = $maxQuota; 110 | } 111 | 112 | /** 113 | * @return int 114 | */ 115 | public function getUsedQuota(): int 116 | { 117 | return $this->usedQuota; 118 | } 119 | 120 | /** 121 | * @param int $usedQuota 122 | */ 123 | public function setUsedQuota(int $usedQuota): void 124 | { 125 | $this->usedQuota = $usedQuota; 126 | } 127 | 128 | /** 129 | * @return bool 130 | */ 131 | public function getActive(): bool 132 | { 133 | return $this->active; 134 | } 135 | 136 | /** 137 | * @param bool $active 138 | */ 139 | public function setActive(bool $active): void 140 | { 141 | $this->active = $active; 142 | } 143 | 144 | /** 145 | * Create a Domain object from JSON SCIM data 146 | * 147 | * @param array $data The JSON SCIM data 148 | */ 149 | public function fromSCIM(array $data) 150 | { 151 | if (isset($data['id'])) { 152 | $this->setId($data['id']); 153 | } 154 | 155 | $this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null); 156 | 157 | $this->setDomainName(isset($data['domainName']) ? $data['domainName'] : null); 158 | $this->setDescription(isset($data['description']) ? $data['description'] : null); 159 | 160 | // For the int attributes that are set below, we set 0 as the default value 161 | // in case that nothing is supplied and/or set in the JSON 162 | // TODO: Is that an okayish solution with this default value? 163 | $this->setMaxAliases(isset($data['maxAliases']) ? $data['maxAliases'] : 0); 164 | $this->setMaxMailboxes(isset($data['maxMailboxes']) ? $data['maxMailboxes'] : 0); 165 | $this->setMaxQuota(isset($data['maxQuota']) ? $data['maxQuota'] : 0); 166 | $this->setUsedQuota(isset($data['usedQuota']) ? $data['usedQuota'] : 0); 167 | 168 | if (isset($data['meta']) && !empty($data['meta'])) { 169 | $meta = new Meta(); 170 | $meta->setResourceType("Domain"); 171 | $meta->setCreated(isset($data['meta']['created']) ? $data['meta']['created'] : null); 172 | $meta->setLastModified(isset($data['meta']['modified']) ? $data['meta']['modified'] : null); 173 | $meta->setVersion(isset($data['meta']['version']) ? $data['meta']['version'] : null); 174 | 175 | $this->setMeta($meta); 176 | } 177 | 178 | // In case that "active" is not set in the JSON, we set it to true by default 179 | // TODO: Is that an okayish solution with this default value? 180 | $this->setActive(isset($data['active']) ? boolval($data['active']) : true); 181 | 182 | $this->setSchemas(isset($data['schemas']) ? $data['schemas'] : []); 183 | } 184 | 185 | /** 186 | * Convert a Domain object to its JSON or array representation 187 | * 188 | * @param bool $encode A flag indicating if the object should be encoded as JSON 189 | * @param string $baseLocation A path indicating the base location of the SCIM server 190 | * 191 | * @return array|string|false If $encode is true, return either a JSON string or false on failure, else an array 192 | */ 193 | public function toSCIM(bool $encode = true, string $baseLocation = 'http://localhost:8888/v1') 194 | { 195 | $data = [ 196 | 'id' => $this->getId(), 197 | 'externalId' => $this->getExternalId(), 198 | 'schemas' => [Util::DOMAIN_SCHEMA], 199 | 'meta' => null !== $this->getMeta() ? [ 200 | 'resourceType' => null !== $this->getMeta()->getResourceType() 201 | ? $this->getMeta()->getResourceType() : null, 202 | 'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null, 203 | 'updated' => null !== $this->getMeta()->getLastModified() ? $this->getMeta()->getLastModified() : null, 204 | 'location' => $baseLocation . '/Domains/' . $this->getId(), 205 | 'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null 206 | ] : null, 207 | 'domainName' => null !== $this->getDomainName() ? $this->getDomainName() : null, 208 | 'description' => null !== $this->getDescription() ? $this->getDescription() : null, 209 | 'maxAliases' => $this->getMaxAliases(), 210 | 'maxMailboxes' => $this->getMaxMailboxes(), 211 | 'maxQuota' => $this->getMaxQuota(), 212 | 'usedQuota' => $this->getUsedQuota(), 213 | 'active' => $this->getActive() 214 | ]; 215 | 216 | if ($encode) { 217 | $data = json_encode($data); 218 | } 219 | 220 | return $data; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/Models/SCIM/Custom/Users/ProvisioningUser.php: -------------------------------------------------------------------------------- 1 | sizeQuota; 19 | } 20 | 21 | /** 22 | * @param int|null $sizeQuota 23 | */ 24 | public function setSizeQuota(?int $sizeQuota): void 25 | { 26 | $this->sizeQuota = $sizeQuota; 27 | } 28 | 29 | public function fromSCIM($data) 30 | { 31 | parent::fromSCIM($data); 32 | if (isset($data[Util::PROVISIONING_USER_SCHEMA])) { 33 | $provisioningUserData = $data[Util::PROVISIONING_USER_SCHEMA]; 34 | $this->setSizeQuota(isset($provisioningUserData['sizeQuota']) ? $provisioningUserData['sizeQuota'] : null); 35 | } 36 | } 37 | 38 | public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') 39 | { 40 | $data = parent::toSCIM($encode, $baseLocation); 41 | $data['schemas'][] = Util::PROVISIONING_USER_SCHEMA; 42 | $data[Util::PROVISIONING_USER_SCHEMA]['sizeQuota'] = $this->getSizeQuota(); 43 | 44 | if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) { 45 | $data['meta']['updated'] = $this->getMeta()->getLastModified(); 46 | } 47 | 48 | if ($encode) { 49 | $data = json_encode($data); 50 | } 51 | 52 | return $data; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/CommonEntity.php: -------------------------------------------------------------------------------- 1 | $schemas */ 14 | private $schemas; 15 | 16 | /** @var string $id */ 17 | private $id; 18 | 19 | /** @var string $externalId */ 20 | private $externalId; 21 | 22 | /** @var \Opf\Models\SCIM\Meta $meta */ 23 | private $meta; 24 | 25 | public function getSchemas() 26 | { 27 | return $this->schemas; 28 | } 29 | 30 | public function setSchemas($schemas) 31 | { 32 | $this->schemas = $schemas; 33 | } 34 | 35 | public function getId() 36 | { 37 | return $this->id; 38 | } 39 | 40 | public function setId($id) 41 | { 42 | $this->id = $id; 43 | } 44 | 45 | public function getExternalId() 46 | { 47 | return $this->externalId; 48 | } 49 | 50 | public function setExternalId($externalId) 51 | { 52 | $this->externalId = $externalId; 53 | } 54 | 55 | public function getMeta() 56 | { 57 | return $this->meta; 58 | } 59 | 60 | public function setMeta($meta) 61 | { 62 | $this->meta = $meta; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/CoreCollection.php: -------------------------------------------------------------------------------- 1 | scimItems = $scimItems; 14 | } 15 | 16 | public function toSCIM($encode = true) 17 | { 18 | $data = [ 19 | 'totalResults' => count($this->scimItems), 20 | 'startIndex' => 1, 21 | 'schemas' => $this->schemas, 22 | 'Resources' => $this->scimItems 23 | ]; 24 | 25 | if ($encode) { 26 | $data = json_encode($data); 27 | } 28 | 29 | return $data; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Filters/AttributeExpression.php: -------------------------------------------------------------------------------- 1 | attributePath = $attributePath; 20 | } else { 21 | throw new FilterException( 22 | "Attribute path passed to Attribute Expression was either empty, null or not a string" 23 | ); 24 | } 25 | 26 | switch ($compareOperator) { 27 | case "eq": 28 | $this->compareOperator = AttributeOperator::OP_EQ; 29 | break; 30 | 31 | case "ne": 32 | $this->compareOperator = AttributeOperator::OP_NE; 33 | break; 34 | 35 | case "co": 36 | $this->compareOperator = AttributeOperator::OP_CO; 37 | break; 38 | 39 | case "sw": 40 | $this->compareOperator = AttributeOperator::OP_SW; 41 | break; 42 | 43 | case "ew": 44 | $this->compareOperator = AttributeOperator::OP_EW; 45 | break; 46 | 47 | case "gt": 48 | $this->compareOperator = AttributeOperator::OP_GT; 49 | break; 50 | 51 | case "lt": 52 | $this->compareOperator = AttributeOperator::OP_LT; 53 | break; 54 | 55 | case "ge": 56 | $this->compareOperator = AttributeOperator::OP_GE; 57 | break; 58 | 59 | case "le": 60 | $this->compareOperator = AttributeOperator::OP_LE; 61 | break; 62 | 63 | case "pr": 64 | $this->compareOperator = AttributeOperator::OP_PR; 65 | break; 66 | 67 | default: 68 | throw new FilterException("Invalid AttributeOperation passed to AttributeExpression"); 69 | break; 70 | } 71 | 72 | $this->comparisonValue = $comparisonValue; 73 | } 74 | 75 | public function getAttributePath() 76 | { 77 | return $this->attributePath; 78 | } 79 | 80 | public function getCompareOperator() 81 | { 82 | return $this->compareOperator; 83 | } 84 | 85 | public function getComparisonValue() 86 | { 87 | return $this->comparisonValue; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Filters/AttributeOperator.php: -------------------------------------------------------------------------------- 1 | displayName; 21 | } 22 | 23 | public function setDisplayName($displayName) 24 | { 25 | $this->displayName = $displayName; 26 | } 27 | 28 | public function getMembers() 29 | { 30 | return $this->members; 31 | } 32 | 33 | public function setMembers($members) 34 | { 35 | $this->members = $members; 36 | } 37 | 38 | public function fromSCIM($data) 39 | { 40 | if (isset($data['id'])) { 41 | $this->setId($data['id']); 42 | } elseif ($this->getId() !== null) { 43 | $this->setId($this->getId()); 44 | } else { 45 | $this->setId(Util::genUuid()); 46 | } 47 | 48 | $this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null); 49 | 50 | $meta = new Meta(); 51 | // This is currently commented out, since the code complains about wrongly 52 | // formatted timestamps sometimes when fromSCIM is called 53 | // TODO: Need to possibly refactor string2datetime and/or dateTime2string in order to fix this 54 | /*if (isset($data['meta']) && isset($data['meta']['created'])) { 55 | $meta->setCreated(Util::string2dateTime($data['meta']['created'])); 56 | } else { 57 | $meta->setCreated(Util::dateTime2string(new \DateTime('NOW'))); 58 | }*/ 59 | $this->setMeta($meta); 60 | 61 | if (isset($data['members'])) { 62 | $members = []; 63 | foreach ($data['members'] as $member) { 64 | $scimMember = new MultiValuedAttribute(); 65 | $scimMember->setValue($member['value']); 66 | $scimMember->setDisplay($member['display']); 67 | $scimMember->setRef($member['$ref']); 68 | $members[] = $scimMember; 69 | } 70 | $this->setMembers($members); 71 | } else { 72 | $this->setMembers(null); 73 | } 74 | 75 | $this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null); 76 | } 77 | 78 | public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') 79 | { 80 | $data = [ 81 | 'schemas' => [Util::GROUP_SCHEMA], 82 | 'id' => $this->getId(), 83 | 'externalId' => $this->getExternalId(), 84 | 'meta' => null !== $this->getMeta() ? [ 85 | 'resourceType' => null !== $this->getMeta()->getResourceType() 86 | ? $this->getMeta()->getResourceType() : null, 87 | 'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null, 88 | 'location' => $baseLocation . '/Groups/' . $this->getId(), 89 | 'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null 90 | ] : null, 91 | 'displayName' => $this->getDisplayName(), 92 | 'members' => $this->getMembers() 93 | ]; 94 | 95 | if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) { 96 | $data['meta']['updated'] = $this->getMeta()->getLastModified(); 97 | } 98 | 99 | if ($encode) { 100 | $data = json_encode($data); 101 | } 102 | 103 | return $data; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Meta.php: -------------------------------------------------------------------------------- 1 | resourceType; 31 | } 32 | 33 | public function setResourceType($resourceType) 34 | { 35 | $this->resourceType = $resourceType; 36 | } 37 | 38 | public function getCreated() 39 | { 40 | return $this->created; 41 | } 42 | 43 | public function setCreated($created) 44 | { 45 | $this->created = $created; 46 | } 47 | 48 | public function getLastModified() 49 | { 50 | return $this->lastModified; 51 | } 52 | 53 | public function setLastModified($lastModified) 54 | { 55 | $this->lastModified = $lastModified; 56 | } 57 | 58 | public function getVersion() 59 | { 60 | return $this->version; 61 | } 62 | 63 | public function setVersion($version) 64 | { 65 | $this->version = $version; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/MultiValuedAttribute.php: -------------------------------------------------------------------------------- 1 | type; 27 | } 28 | 29 | public function setType($type) 30 | { 31 | $this->type = $type; 32 | } 33 | 34 | public function getPrimary() 35 | { 36 | return $this->primary; 37 | } 38 | 39 | public function setPrimary($primary) 40 | { 41 | $this->primary = $primary; 42 | } 43 | 44 | public function getDisplay() 45 | { 46 | return $this->display; 47 | } 48 | 49 | public function setDisplay($display) 50 | { 51 | $this->display = $display; 52 | } 53 | 54 | public function getValue() 55 | { 56 | return $this->value; 57 | } 58 | 59 | public function setValue($value) 60 | { 61 | $this->value = $value; 62 | } 63 | 64 | public function getRef() 65 | { 66 | return $this->ref; 67 | } 68 | 69 | public function setRef($ref) 70 | { 71 | $this->ref = $ref; 72 | } 73 | 74 | public function jsonSerialize() 75 | { 76 | return (object)[ 77 | "type" => $this->getType(), 78 | "primary" => $this->getPrimary(), 79 | "display" => $this->getDisplay(), 80 | "value" => $this->getValue(), 81 | "\$ref" => $this->getRef() 82 | ]; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/Attribute.php: -------------------------------------------------------------------------------- 1 | $subAttributes */ 21 | private $subAttributes; 22 | 23 | /** @var boolean $multiValued */ 24 | private $multiValued; 25 | 26 | /** @var string $description */ 27 | private $description; 28 | 29 | /** @var boolean $required */ 30 | private $required; 31 | 32 | /** @var array $canonicalValues */ 33 | private $canonicalValues; 34 | 35 | /** @var boolean $caseExact */ 36 | private $caseExact; 37 | 38 | /** @var string $mutability */ 39 | private $mutability; 40 | 41 | /** @var string $returned */ 42 | private $returned; 43 | 44 | /** @var string $uniqueness */ 45 | private $uniqueness; 46 | 47 | /** @var array $referenceTypes */ 48 | private $referenceTypes; 49 | 50 | public function getName() 51 | { 52 | return $this->name; 53 | } 54 | 55 | public function setName($name) 56 | { 57 | $this->name = $name; 58 | } 59 | 60 | public function getType() 61 | { 62 | return $this->type; 63 | } 64 | 65 | public function setType($type) 66 | { 67 | $this->type = $type; 68 | } 69 | 70 | public function getSubAttributes() 71 | { 72 | return $this->subAttributes; 73 | } 74 | 75 | public function setSubAttributes($subAttributes) 76 | { 77 | $this->subAttributes = $subAttributes; 78 | } 79 | 80 | public function getMultiValued() 81 | { 82 | return $this->multiValued; 83 | } 84 | 85 | public function setMultiValued($multiValued) 86 | { 87 | $this->multiValued = $multiValued; 88 | } 89 | 90 | public function getDescription() 91 | { 92 | return $this->description; 93 | } 94 | 95 | public function setDescription($description) 96 | { 97 | $this->description = $description; 98 | } 99 | 100 | public function getRequired() 101 | { 102 | return $this->required; 103 | } 104 | 105 | public function setRequired($required) 106 | { 107 | $this->required = $required; 108 | } 109 | 110 | public function getCanonicalValues() 111 | { 112 | return $this->canonicalValues; 113 | } 114 | 115 | public function setCanonicalValues($canonicalValues) 116 | { 117 | $this->canonicalValues = $canonicalValues; 118 | } 119 | 120 | public function getCaseExact() 121 | { 122 | return $this->caseExact; 123 | } 124 | 125 | public function setCaseExact($caseExact) 126 | { 127 | $this->caseExact = $caseExact; 128 | } 129 | 130 | public function getMutability() 131 | { 132 | return $this->mutability; 133 | } 134 | 135 | public function setMutability($mutability) 136 | { 137 | $this->mutability = $mutability; 138 | } 139 | 140 | public function getReturned() 141 | { 142 | return $this->returned; 143 | } 144 | 145 | public function setReturned($returned) 146 | { 147 | $this->returned = $returned; 148 | } 149 | 150 | public function getUniqueness() 151 | { 152 | return $this->uniqueness; 153 | } 154 | 155 | public function setUniqueness($uniqueness) 156 | { 157 | $this->uniqueness = $uniqueness; 158 | } 159 | 160 | public function getReferenceTypes() 161 | { 162 | return $this->referenceTypes; 163 | } 164 | 165 | public function setReferenceTypes($referenceTypes) 166 | { 167 | $this->referenceTypes = $referenceTypes; 168 | } 169 | 170 | public function jsonSerialize() 171 | { 172 | return (object)[ 173 | "name" => $this->getName(), 174 | "type" => $this->getType(), 175 | "subAttributes" => $this->getSubAttributes(), 176 | "multiValued" => $this->getMultiValued(), 177 | "description" => $this->getDescription(), 178 | "required" => $this->getRequired(), 179 | "canonicalValues" => $this->getCanonicalValues(), 180 | "caseExact" => $this->getCaseExact(), 181 | "mutability" => $this->getMutability(), 182 | "returned" => $this->getReturned(), 183 | "uniqueness" => $this->getUniqueness(), 184 | "referenceTypes" => $this->getReferenceTypes() 185 | ]; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/AuthenticationScheme.php: -------------------------------------------------------------------------------- 1 | type; 25 | } 26 | 27 | public function setType($type) 28 | { 29 | $this->type = $type; 30 | } 31 | 32 | public function getName() 33 | { 34 | return $this->name; 35 | } 36 | 37 | public function setName($name) 38 | { 39 | $this->name = $name; 40 | } 41 | 42 | public function getDescription() 43 | { 44 | return $this->description; 45 | } 46 | 47 | public function setDescription($description) 48 | { 49 | $this->description = $description; 50 | } 51 | 52 | public function getSpecUri() 53 | { 54 | return $this->specUri; 55 | } 56 | 57 | public function setSpecUri($specUri) 58 | { 59 | $this->specUri = $specUri; 60 | } 61 | 62 | public function getDocumentationUri() 63 | { 64 | return $this->documentationUri; 65 | } 66 | 67 | public function setDocumentationUri($documentationUri) 68 | { 69 | $this->documentationUri = $documentationUri; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/Bulk.php: -------------------------------------------------------------------------------- 1 | maxOperations; 16 | } 17 | 18 | public function setMaxOperations($maxOperations) 19 | { 20 | $this->maxOperations = $maxOperations; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/CoreResourceType.php: -------------------------------------------------------------------------------- 1 | */ 12 | private $schemas = [Util::RESOURCE_TYPE_SCHEMA]; 13 | 14 | /** @var string */ 15 | private $id; 16 | 17 | /** @var string */ 18 | private $name; 19 | 20 | /** @var string */ 21 | private $description; 22 | 23 | /** @var string */ 24 | private $endpoint; 25 | 26 | /** @var string */ 27 | private $schema; 28 | 29 | /** @var array */ 30 | private $schemaExtensions; 31 | 32 | public function getId() 33 | { 34 | return $this->id; 35 | } 36 | 37 | public function setId($id) 38 | { 39 | $this->id = $id; 40 | } 41 | 42 | public function getName() 43 | { 44 | return $this->name; 45 | } 46 | 47 | public function setName($name) 48 | { 49 | $this->name = $name; 50 | } 51 | 52 | public function getDescription() 53 | { 54 | return $this->description; 55 | } 56 | 57 | public function setDescription($description) 58 | { 59 | $this->description = $description; 60 | } 61 | 62 | public function getEndpoint() 63 | { 64 | return $this->endpoint; 65 | } 66 | 67 | public function setEndpoint($endpoint) 68 | { 69 | $this->endpoint = $endpoint; 70 | } 71 | 72 | public function getSchema() 73 | { 74 | return $this->schema; 75 | } 76 | 77 | public function setSchema($schema) 78 | { 79 | $this->schema = $schema; 80 | } 81 | 82 | public function getSchemaExtensions() 83 | { 84 | return $this->schemaExtensions; 85 | } 86 | 87 | public function setSchemaExtensions($schemaExtensions) 88 | { 89 | $this->schemaExtensions = $schemaExtensions; 90 | } 91 | 92 | public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') 93 | { 94 | if (isset($this->schemaExtensions) && !empty($this->schemaExtensions)) { 95 | $transformedSchemaExtensions = []; 96 | 97 | foreach ($this->schemaExtensions as $schemaExtension) { 98 | $transformedSchemaExtensions[] = $schemaExtension->toSCIM(); 99 | } 100 | 101 | $this->setSchemaExtensions($transformedSchemaExtensions); 102 | } 103 | 104 | $data = [ 105 | 'schemas' => $this->schemas, 106 | 'id' => $this->id, 107 | 'name' => $this->name, 108 | 'description' => $this->description, 109 | 'endpoint' => $this->endpoint, 110 | 'schema' => $this->schema, 111 | 'schemaExtensions' => $this->schemaExtensions, 112 | 'meta' => [ 113 | 'location' => $baseLocation . '/' . $this->id, 114 | 'resourceType' => 'ResourceType' 115 | ] 116 | ]; 117 | 118 | if ($encode) { 119 | $data = json_encode($data, JSON_UNESCAPED_SLASHES); 120 | } 121 | 122 | return $data; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/CoreSchema.php: -------------------------------------------------------------------------------- 1 | $attributes */ 22 | private $attributes; 23 | 24 | public function getId() 25 | { 26 | return $this->id; 27 | } 28 | 29 | public function setId($id) 30 | { 31 | $this->id = $id; 32 | } 33 | 34 | public function getName() 35 | { 36 | return $this->name; 37 | } 38 | 39 | public function setName($name) 40 | { 41 | $this->name = $name; 42 | } 43 | 44 | public function getDescription() 45 | { 46 | return $this->description; 47 | } 48 | 49 | public function setDescription($description) 50 | { 51 | $this->description = $description; 52 | } 53 | 54 | public function getAttributes() 55 | { 56 | return $this->attributes; 57 | } 58 | 59 | public function setAttributes($attributes) 60 | { 61 | $this->attributes = $attributes; 62 | } 63 | 64 | public function toSCIM($encode = true) 65 | { 66 | $data = [ 67 | "id" => $this->id, 68 | "name" => $this->name, 69 | "description" => $this->description, 70 | "attributes" => $this->attributes 71 | ]; 72 | 73 | if ($encode) { 74 | $data = json_encode($data, JSON_UNESCAPED_SLASHES); 75 | } 76 | 77 | return $data; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/CoreSchemaExtension.php: -------------------------------------------------------------------------------- 1 | schema; 16 | } 17 | 18 | public function setSchema($schema) 19 | { 20 | $this->schema = $schema; 21 | } 22 | 23 | public function getRequired() 24 | { 25 | return $this->required; 26 | } 27 | 28 | public function setRequired($required) 29 | { 30 | $this->required = $required; 31 | } 32 | 33 | public function toSCIM($encode = false) 34 | { 35 | $data = [ 36 | 'schema' => $this->schema, 37 | 'required' => $this->required 38 | ]; 39 | 40 | if ($encode) { 41 | $data = json_encode($data, JSON_UNESCAPED_SLASHES); 42 | } 43 | 44 | return $data; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/CoreServiceProviderConfiguration.php: -------------------------------------------------------------------------------- 1 | $schemas */ 10 | private $schemas = [Util::SERVICE_PROVIDER_CONFIGURATION_SCHEMA]; 11 | 12 | /** @var string $documentationUri */ 13 | private $documentationUri; 14 | 15 | /** @var \Opf\Models\SCIM\SupportableConfigProperty $patch */ 16 | private $patch; 17 | 18 | /** @var \Opf\Models\SCIM\Bulk $bulk */ 19 | private $bulk; 20 | 21 | /** @var \Opf\Models\SCIM\Filter $filter */ 22 | private $filter; 23 | 24 | /** @var \Opf\Models\SCIM\SupportableConfigProperty $changePassword */ 25 | private $changePassword; 26 | 27 | /** @var \Opf\Models\SCIM\SupportableConfigProperty $sort */ 28 | private $sort; 29 | 30 | /** @var \Opf\Models\SCIM\SupportableConfigProperty $etag */ 31 | private $etag; 32 | 33 | /** @var array<\Opf\Models\SCIM\AuthenticationScheme> $authenticationSchemes */ 34 | private $authenticationSchemes; 35 | 36 | public function getSchemas() 37 | { 38 | return $this->schemas; 39 | } 40 | 41 | public function setSchemas($schemas) 42 | { 43 | $this->schemas = $schemas; 44 | } 45 | 46 | public function getDocumentationUri() 47 | { 48 | return $this->documentationUri; 49 | } 50 | 51 | public function setDocumentationUri($documentationUri) 52 | { 53 | $this->documentationUri = $documentationUri; 54 | } 55 | 56 | public function getPatch() 57 | { 58 | return $this->patch; 59 | } 60 | 61 | public function setPatch($patch) 62 | { 63 | $this->patch = $patch; 64 | } 65 | 66 | public function getBulk() 67 | { 68 | return $this->bulk; 69 | } 70 | 71 | public function setBulk($bulk) 72 | { 73 | $this->bulk = $bulk; 74 | } 75 | 76 | public function getFilter() 77 | { 78 | return $this->filter; 79 | } 80 | 81 | public function setFilter($filter) 82 | { 83 | $this->filter = $filter; 84 | } 85 | 86 | public function getChangePassword() 87 | { 88 | return $this->changePassword; 89 | } 90 | 91 | public function setChangePassword($changePassword) 92 | { 93 | $this->changePassword = $changePassword; 94 | } 95 | 96 | public function getSort() 97 | { 98 | return $this->sort; 99 | } 100 | 101 | public function setSort($sort) 102 | { 103 | $this->sort = $sort; 104 | } 105 | 106 | public function getEtag() 107 | { 108 | return $this->etag; 109 | } 110 | 111 | public function setEtag($etag) 112 | { 113 | $this->etag = $etag; 114 | } 115 | 116 | public function getAuthenticationSchemes() 117 | { 118 | return $this->authenticationSchemes; 119 | } 120 | 121 | public function setAuthenticationSchemes($authenticationSchemes) 122 | { 123 | $this->authenticationSchemes = $authenticationSchemes; 124 | } 125 | 126 | public function toSCIM($encode = true) 127 | { 128 | $data = [ 129 | "schemas" => $this->schemas, 130 | "documentationUri" => $this->documentationUri, 131 | "patch" => $this->patch, 132 | "bulk" => $this->bulk, 133 | "filter" => $this->filter, 134 | "changePassword" => $this->changePassword, 135 | "sort" => $this->sort, 136 | "etag" => $this->etag, 137 | "authenticationSchemes" => $this->authenticationSchemes 138 | ]; 139 | 140 | if ($encode) { 141 | $data = json_encode($data, JSON_UNESCAPED_SLASHES); 142 | } 143 | 144 | return $data; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/Filter.php: -------------------------------------------------------------------------------- 1 | maxResults; 13 | } 14 | 15 | public function setMaxResults($maxResults) 16 | { 17 | $this->maxResults = $maxResults; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Service/SupportableConfigProperty.php: -------------------------------------------------------------------------------- 1 | supported; 13 | } 14 | 15 | public function setSupported($supported) 16 | { 17 | $this->supported = $supported; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Users/Address.php: -------------------------------------------------------------------------------- 1 | formatted; 30 | } 31 | 32 | public function setFormatted($formatted) 33 | { 34 | $this->formatted = $formatted; 35 | } 36 | 37 | public function getStreetAddress() 38 | { 39 | return $this->streetAddress; 40 | } 41 | 42 | public function setStreetAddress($streetAddress) 43 | { 44 | $this->streetAddress = $streetAddress; 45 | } 46 | 47 | public function getLocality() 48 | { 49 | return $this->locality; 50 | } 51 | 52 | public function setLocality($locality) 53 | { 54 | $this->locality = $locality; 55 | } 56 | 57 | public function getRegion() 58 | { 59 | return $this->region; 60 | } 61 | 62 | public function setRegion($region) 63 | { 64 | $this->region = $region; 65 | } 66 | 67 | public function getPostalCode() 68 | { 69 | return $this->postalCode; 70 | } 71 | 72 | public function setPostalCode($postalCode) 73 | { 74 | $this->postalCode = $postalCode; 75 | } 76 | 77 | public function getCountry() 78 | { 79 | return $this->country; 80 | } 81 | 82 | public function setCountry($country) 83 | { 84 | $this->country = $country; 85 | } 86 | 87 | public function jsonSerialize(): mixed 88 | { 89 | return (object)[ 90 | "type" => $this->getType(), 91 | "primary" => $this->getPrimary(), 92 | "display" => $this->getDisplay(), 93 | "value" => $this->getValue(), 94 | "\$ref" => $this->getRef(), 95 | "formatted" => $this->getFormatted(), 96 | "streetAddress" => $this->getStreetAddress(), 97 | "locality" => $this->getLocality(), 98 | "region" => $this->getRegion(), 99 | "postalCode" => $this->getPostalCode(), 100 | "country" => $this->getCountry() 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Users/EnterpriseUser.php: -------------------------------------------------------------------------------- 1 | employeeNumber; 30 | } 31 | 32 | public function setEmployeeNumber($employeeNumber) 33 | { 34 | $this->employeeNumber = $employeeNumber; 35 | } 36 | 37 | public function getCostCenter() 38 | { 39 | return $this->costCenter; 40 | } 41 | 42 | public function setCostCenter($costCenter) 43 | { 44 | $this->costCenter = $costCenter; 45 | } 46 | 47 | public function getOrganization() 48 | { 49 | return $this->organization; 50 | } 51 | 52 | public function setOrganization($organization) 53 | { 54 | $this->organization = $organization; 55 | } 56 | 57 | public function getDivision() 58 | { 59 | return $this->division; 60 | } 61 | 62 | public function setDivision($division) 63 | { 64 | $this->division = $division; 65 | } 66 | 67 | public function getDepartment() 68 | { 69 | return $this->department; 70 | } 71 | 72 | public function setDepartment($department) 73 | { 74 | $this->department = $department; 75 | } 76 | 77 | public function getManager() 78 | { 79 | return $this->manager; 80 | } 81 | 82 | public function setManager($manager) 83 | { 84 | $this->manager = $manager; 85 | } 86 | 87 | public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1') 88 | { 89 | $data = [ 90 | 'schemas' => [Util::ENTERPRISE_USER_SCHEMA], 91 | 'id' => $this->getId(), 92 | 'externalId' => $this->getExternalId(), 93 | 'meta' => null !== $this->getMeta() ? [ 94 | 'resourceType' => null !== $this->getMeta()->getResourceType() 95 | ? $this->getMeta()->getResourceType() : null, 96 | 'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null, 97 | 'location' => $baseLocation . '/Users/' . $this->getId(), 98 | 'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null 99 | ] : null, 100 | 'userName' => $this->getUserName(), 101 | 'name' => null !== $this->getName() ? [ 102 | 'formatted' => null !== $this->getName()->getFormatted() ? $this->getName()->getFormatted() : null, 103 | 'familyName' => null !== $this->getName()->getFamilyName() ? $this->getName()->getFamilyName() : null, 104 | 'givenName' => null !== $this->getName()->getGivenName() ? $this->getName()->getGivenName() : null, 105 | 'middleName' => null !== $this->getName()->getMiddleName() ? $this->getName()->getMiddleName() : null, 106 | 'honorificPrefix' => null !== $this->getName()->getHonorificPrefix() 107 | ? $this->getName()->getHonorificPrefix() 108 | : null, 109 | 'honorificSuffix' => null !== $this->getName()->getHonorificSuffix() 110 | ? $this->getName()->getHonorificSuffix() 111 | : null 112 | ] : null, 113 | 'displayName' => $this->getDisplayName(), 114 | 'nickName' => $this->getNickName(), 115 | 'profileUrl' => $this->getProfileUrl(), 116 | 'title' => $this->getTitle(), 117 | 'userType' => $this->getUserType(), 118 | 'preferredLanguage' => $this->getPreferredLanguage(), 119 | 'locale' => $this->getLocale(), 120 | 'timezone' => $this->getTimezone(), 121 | 'active' => $this->getActive(), 122 | 'password' => $this->getPassword(), 123 | 'emails' => $this->getEmails(), 124 | 'phoneNumbers' => $this->getPhoneNumbers(), 125 | 'ims' => $this->getIms(), 126 | 'photos' => $this->getPhotos(), 127 | 'addresses' => $this->getAddresses(), 128 | 'groups' => $this->getGroups(), 129 | 'entitlements' => $this->getEntitlements(), 130 | 'roles' => $this->getRoles(), 131 | 'x509Certificates' => $this->getX509Certificates(), 132 | 'employeeNumber' => $this->getEmployeeNumber(), 133 | 'costCenter' => $this->getCostCenter(), 134 | 'organization' => $this->getOrganization(), 135 | 'division' => $this->getDivision(), 136 | 'department' => $this->getDepartment(), 137 | 'manager' => null !== $this->getManager() ? [ 138 | 'value' => null !== $this->getManager()->getValue() ? $this->getManager()->getValue() : null, 139 | '\$ref' => null !== $this->getManager()->getRef() ? $this->getManager()->getRef() : null, 140 | 'displayName' => null !== $this->getManager()->getDisplayName() 141 | ? $this->getManager()->getDisplayName() 142 | : null 143 | ] : null 144 | ]; 145 | 146 | if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) { 147 | $data['meta']['updated'] = $this->getMeta()->getLastModified(); 148 | } 149 | 150 | if ($encode) { 151 | $data = json_encode($data); 152 | } 153 | 154 | return $data; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Users/Manager.php: -------------------------------------------------------------------------------- 1 | value; 19 | } 20 | 21 | public function setValue($value) 22 | { 23 | $this->value = $value; 24 | } 25 | 26 | public function getRef() 27 | { 28 | return $this->ref; 29 | } 30 | 31 | public function setRef($ref) 32 | { 33 | $this->ref = $ref; 34 | } 35 | 36 | public function getDisplayName() 37 | { 38 | return $this->displayName; 39 | } 40 | 41 | public function setDisplayName($displayName) 42 | { 43 | $this->displayName = $displayName; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Models/SCIM/Standard/Users/Name.php: -------------------------------------------------------------------------------- 1 | formatted; 28 | } 29 | 30 | public function setFormatted($formatted) 31 | { 32 | $this->formatted = $formatted; 33 | } 34 | 35 | public function getFamilyName() 36 | { 37 | return $this->familyName; 38 | } 39 | 40 | public function setFamilyName($familyName) 41 | { 42 | $this->familyName = $familyName; 43 | } 44 | 45 | public function getGivenName() 46 | { 47 | return $this->givenName; 48 | } 49 | 50 | public function setGivenName($givenName) 51 | { 52 | $this->givenName = $givenName; 53 | } 54 | 55 | public function getMiddleName() 56 | { 57 | return $this->middleName; 58 | } 59 | 60 | public function setMiddleName($middleName) 61 | { 62 | $this->middleName = $middleName; 63 | } 64 | 65 | public function getHonorificPrefix() 66 | { 67 | return $this->honorificPrefix; 68 | } 69 | 70 | public function setHonorificPrefix($honorificPrefix) 71 | { 72 | $this->honorificPrefix = $honorificPrefix; 73 | } 74 | 75 | public function getHonorificSuffix() 76 | { 77 | return $this->honorificSuffix; 78 | } 79 | 80 | public function setHonorificSuffix($honorificSuffix) 81 | { 82 | $this->honorificSuffix = $honorificSuffix; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Repositories/Groups/MockGroupsRepository.php: -------------------------------------------------------------------------------- 1 | dataAccess = $this->container->get('GroupsDataAccess'); 20 | $this->adapter = $this->container->get('GroupsAdapter'); 21 | $this->logger = $this->container->get(Logger::class); 22 | } 23 | 24 | public function getAll( 25 | $filter = '', 26 | $startIndex = 0, 27 | $count = 0, 28 | $attributes = [], 29 | $excludedAttributes = [] 30 | ): array { 31 | // Read all mock groups from the database 32 | $mockGroups = $this->dataAccess->getAll(); 33 | $scimGroups = []; 34 | 35 | foreach ($mockGroups as $mockGroup) { 36 | $scimGroup = $this->adapter->getCoreGroup($mockGroup); 37 | $scimGroups[] = $scimGroup; 38 | } 39 | 40 | if (isset($filter) && !empty($filter)) { 41 | $scimGroupsToFilter = []; 42 | foreach ($scimGroups as $scimGroup) { 43 | $scimGroupsToFilter[] = $scimGroup->toSCIM(false); 44 | } 45 | 46 | $filteredScimData = FilterUtil::performFiltering($filter, $scimGroupsToFilter); 47 | 48 | $scimGroups = []; 49 | foreach ($filteredScimData as $filteredScimGroup) { 50 | $scimGroup = new CoreGroup(); 51 | $scimGroup->fromSCIM($filteredScimGroup); 52 | $scimGroups[] = $scimGroup; 53 | } 54 | 55 | return $scimGroups; 56 | } 57 | 58 | return $scimGroups; 59 | } 60 | 61 | public function getOneById( 62 | string $id, 63 | $filter = '', 64 | $startIndex = 0, 65 | $count = 0, 66 | $attributes = [], 67 | $excludedAttributes = [] 68 | ): ?CoreGroup { 69 | $mockGroup = $this->dataAccess->getOneById($id); 70 | return $this->adapter->getCoreGroup($mockGroup); 71 | } 72 | 73 | public function create($object): ?CoreGroup 74 | { 75 | $scimGroupToCreate = new CoreGroup(); 76 | $scimGroupToCreate->fromSCIM($object); 77 | 78 | $mockGroupToCreate = $this->adapter->getMockGroup($scimGroupToCreate); 79 | 80 | $mockGroupCreated = $this->dataAccess->create($mockGroupToCreate); 81 | 82 | if (isset($mockGroupCreated)) { 83 | return $this->adapter->getCoreGroup($mockGroupCreated); 84 | } 85 | return null; 86 | } 87 | 88 | public function update(string $id, $object): ?CoreGroup 89 | { 90 | $scimGroupToUpdate = new CoreGroup(); 91 | $scimGroupToUpdate->fromSCIM($object); 92 | 93 | $mockGroupToUpdate = $this->adapter->getMockGroup($scimGroupToUpdate); 94 | 95 | $mockGroupUpdated = $this->dataAccess->update($id, $mockGroupToUpdate); 96 | 97 | if (isset($mockGroupUpdated)) { 98 | return $this->adapter->getCoreGroup($mockGroupUpdated); 99 | } 100 | return null; 101 | } 102 | 103 | public function delete(string $id): bool 104 | { 105 | return $this->dataAccess->delete($id); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Repositories/Repository.php: -------------------------------------------------------------------------------- 1 | container = $container; 16 | } 17 | 18 | /** 19 | * @param string $filter Optional parameter which contains a filter expression 20 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2 21 | * 22 | * @param int $startIndex Optional parameter for specifying the start index, used for pagination 23 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4 24 | * 25 | * @param int $count Optional parameter for specifying the number of results, used for pagination 26 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4 27 | * 28 | * @param array $attributes Optional parameter for including only specific attributes in the response 29 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5 30 | * (if $exludedAttributes is not empty, this should be empty) 31 | * 32 | * @param array $excludedAttributes Optional parameter for excluding specific attributes from the response 33 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5 34 | * (if $attributes is not empty, this should be empty) 35 | * 36 | * @return array An array of SCIM resources 37 | */ 38 | abstract public function getAll( 39 | $filter = '', 40 | $startIndex = 0, 41 | $count = 0, 42 | $attributes = [], 43 | $excludedAttributes = [] 44 | ): array; 45 | 46 | /** 47 | * @param string $id Required parameter which contains of a given entity that should be retrieved 48 | * 49 | * @param string $filter Optional parameter which contains a filter expression 50 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.2 51 | * 52 | * @param int $startIndex Optional parameter for specifying the start index, used for pagination 53 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4 54 | * 55 | * @param int $count Optional parameter for specifying the number of results, used for pagination 56 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.4 57 | * 58 | * @param array $attributes Optional parameter for including only specific attributes in the response 59 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5 60 | * (if $exludedAttributes is not empty, this should be empty) 61 | * 62 | * @param array $excludedAttributes Optional parameter for excluding specific attributes from the response 63 | * as per https://www.rfc-editor.org/rfc/rfc7644.html#section-3.4.2.5 64 | * (if $attributes is not empty, this should be empty) 65 | * 66 | * @return object|null A SCIM resource or null if no resource found 67 | */ 68 | abstract public function getOneById( 69 | string $id, 70 | $filter = '', 71 | $startIndex = 0, 72 | $count = 0, 73 | $attributes = [], 74 | $excludedAttributes = [] 75 | ): ?object; 76 | 77 | abstract public function create($object): ?object; 78 | 79 | abstract public function update(string $id, $object): ?object; 80 | 81 | abstract public function delete(string $id): bool; 82 | } 83 | -------------------------------------------------------------------------------- /src/Repositories/Users/MockUsersRepository.php: -------------------------------------------------------------------------------- 1 | dataAccess = $this->container->get('UsersDataAccess'); 20 | $this->adapter = $this->container->get('UsersAdapter'); 21 | $this->logger = $this->container->get(Logger::class); 22 | } 23 | 24 | public function getAll( 25 | $filter = '', 26 | $startIndex = 0, 27 | $count = 0, 28 | $attributes = [], 29 | $excludedAttributes = [] 30 | ): array { 31 | // Read all mock users from the database 32 | $mockUsers = $this->dataAccess->getAll(); 33 | $scimUsers = []; 34 | 35 | foreach ($mockUsers as $mockUser) { 36 | $scimUser = $this->adapter->getCoreUser($mockUser); 37 | $scimUsers[] = $scimUser; 38 | } 39 | 40 | if (isset($filter) && !empty($filter)) { 41 | $scimUsersToFilter = []; 42 | foreach ($scimUsers as $scimUser) { 43 | $scimUsersToFilter[] = $scimUser->toSCIM(false); 44 | } 45 | 46 | $filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter); 47 | 48 | $scimUsers = []; 49 | foreach ($filteredScimData as $filteredScimUser) { 50 | $scimUser = new CoreUser(); 51 | $scimUser->fromSCIM($filteredScimUser); 52 | $scimUsers[] = $scimUser; 53 | } 54 | 55 | return $scimUsers; 56 | } 57 | 58 | return $scimUsers; 59 | } 60 | 61 | public function getOneById( 62 | string $id, 63 | $filter = '', 64 | $startIndex = 0, 65 | $count = 0, 66 | $attributes = [], 67 | $excludedAttributes = [] 68 | ): ?CoreUser { 69 | $mockUser = $this->dataAccess->getOneById($id); 70 | $scimUser = $this->adapter->getCoreUser($mockUser); 71 | 72 | if (isset($filter) && !empty($filter)) { 73 | // Pass the single user as an array of an array, representing the user 74 | $scimUsersToFilter = array($scimUser->toSCIM(false)); 75 | $filteredScimData = FilterUtil::performFiltering($filter, $scimUsersToFilter); 76 | 77 | if (!empty($filteredScimData)) { 78 | $scimUser = new CoreUser(); 79 | $scimUser->fromSCIM($filteredScimData[0]); 80 | return $scimUser; 81 | } 82 | } 83 | 84 | return $scimUser; 85 | } 86 | 87 | public function create($object): ?CoreUser 88 | { 89 | $scimUserToCreate = new CoreUser(); 90 | $scimUserToCreate->fromSCIM($object); 91 | 92 | $mockUserToCreate = $this->adapter->getMockUser($scimUserToCreate); 93 | 94 | $mockUserCreated = $this->dataAccess->create($mockUserToCreate); 95 | 96 | if (isset($mockUserCreated)) { 97 | return $this->adapter->getCoreUser($mockUserCreated); 98 | } 99 | return null; 100 | } 101 | 102 | public function update(string $id, $object): ?CoreUser 103 | { 104 | $scimUserToUpdate = new CoreUser(); 105 | $scimUserToUpdate->fromSCIM($object); 106 | 107 | $mockUserToUpdate = $this->adapter->getMockUser($scimUserToUpdate); 108 | 109 | $mockUserUpdated = $this->dataAccess->update($id, $mockUserToUpdate); 110 | 111 | if (isset($mockUserUpdated)) { 112 | return $this->adapter->getCoreUser($mockUserUpdated); 113 | } 114 | return null; 115 | } 116 | 117 | public function delete(string $id): bool 118 | { 119 | return $this->dataAccess->delete($id); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ScimServer.php: -------------------------------------------------------------------------------- 1 | scimServerPhpRoot = $scimServerPhpRoot; 56 | 57 | /** 58 | * Once we have the root directory of the project that's using 59 | * OPF, we include its autoload file, so that we don't run into 60 | * autoloading issues. 61 | */ 62 | require $this->scimServerPhpRoot . '/vendor/autoload.php'; 63 | } 64 | 65 | public function setConfig(string $configFilePath) 66 | { 67 | if (!isset($configFilePath) || empty($configFilePath)) { 68 | throw new Exception("Config file path must be supplied"); 69 | } 70 | 71 | Util::setConfigFile($configFilePath); 72 | } 73 | 74 | public function setDependencies(array $dependencies = array()) 75 | { 76 | $baseDependencies = require __DIR__ . '/Dependencies/dependencies.php'; 77 | $this->dependencies = array_merge($baseDependencies, $dependencies); 78 | } 79 | 80 | public function setMiddleware(array $middleware = array()) 81 | { 82 | $this->middleware = $middleware; 83 | } 84 | 85 | public function run() 86 | { 87 | session_start(); 88 | 89 | // Instantiate the PHP-DI ContainerBuilder 90 | $containerBuilder = new ContainerBuilder(); 91 | 92 | $config = Util::getConfigFile(); 93 | if ($config['isInProduction']) { 94 | $containerBuilder->enableCompilation(__DIR__ . '/../var/cache'); 95 | } 96 | 97 | // Set up a few Slim-related settings 98 | $settings = [ 99 | 'settings' => [ 100 | 'determineRouteBeforeAppMiddleware' => false, 101 | 'displayErrorDetails' => true, // set to false in production 102 | 'addContentLengthHeader' => false, // Allow the web server to send the content-length header 103 | ] 104 | ]; 105 | $containerBuilder->addDefinitions($settings); 106 | 107 | // Set all necessary dependencies which are provided in this class' 108 | // $dependencies attribute 109 | $containerBuilder->addDefinitions($this->dependencies); 110 | 111 | // Build PHP-DI Container instance 112 | $container = $containerBuilder->build(); 113 | 114 | // Instantiate the app 115 | AppFactory::setContainer($container); 116 | $this->app = AppFactory::create(); 117 | 118 | // Set our app's base path if it's configured 119 | if (isset($config['basePath']) && !empty($config['basePath'])) { 120 | $this->app->setBasePath($config['basePath']); 121 | } 122 | 123 | // Register routes 124 | $routes = require __DIR__ . '/routes.php'; 125 | $routes($this->app); 126 | 127 | 128 | // Iterate through the custom middleware (if any) and set it 129 | if (isset($this->middleware) && !empty($this->middleware)) { 130 | foreach ($this->middleware as $middleware) { 131 | $this->app->addMiddleware($this->app->getContainer()->get($middleware)); 132 | } 133 | } 134 | 135 | // Add Routing Middleware 136 | $this->app->addRoutingMiddleware(); 137 | $this->app->addBodyParsingMiddleware(); 138 | 139 | $callableResolver = $this->app->getCallableResolver(); 140 | $responseFactory = $this->app->getResponseFactory(); 141 | 142 | // Instantiate our custom Http error handler that we need further down below 143 | $errorHandler = new HttpErrorHandler($callableResolver, $responseFactory); 144 | 145 | // Add error middleware 146 | $errorMiddleware = $this->app->addErrorMiddleware( 147 | $config['isInProduction'] ? false : true, 148 | true, 149 | true 150 | ); 151 | $errorMiddleware->setDefaultErrorHandler($errorHandler); 152 | 153 | // Run app 154 | $this->app->run(); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/ScimServerPhp/Firebase/JWT/BeforeValidException.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class CachedKeySet implements ArrayAccess 18 | { 19 | /** 20 | * @var string 21 | */ 22 | private $jwksUri; 23 | /** 24 | * @var ClientInterface 25 | */ 26 | private $httpClient; 27 | /** 28 | * @var RequestFactoryInterface 29 | */ 30 | private $httpFactory; 31 | /** 32 | * @var CacheItemPoolInterface 33 | */ 34 | private $cache; 35 | /** 36 | * @var ?int 37 | */ 38 | private $expiresAfter; 39 | /** 40 | * @var ?CacheItemInterface 41 | */ 42 | private $cacheItem; 43 | /** 44 | * @var array 45 | */ 46 | private $keySet; 47 | /** 48 | * @var string 49 | */ 50 | private $cacheKey; 51 | /** 52 | * @var string 53 | */ 54 | private $cacheKeyPrefix = 'jwks'; 55 | /** 56 | * @var int 57 | */ 58 | private $maxKeyLength = 64; 59 | /** 60 | * @var bool 61 | */ 62 | private $rateLimit; 63 | /** 64 | * @var string 65 | */ 66 | private $rateLimitCacheKey; 67 | /** 68 | * @var int 69 | */ 70 | private $maxCallsPerMinute = 10; 71 | /** 72 | * @var string|null 73 | */ 74 | private $defaultAlg; 75 | 76 | public function __construct( 77 | string $jwksUri, 78 | ClientInterface $httpClient, 79 | RequestFactoryInterface $httpFactory, 80 | CacheItemPoolInterface $cache, 81 | int $expiresAfter = null, 82 | bool $rateLimit = false, 83 | string $defaultAlg = null 84 | ) { 85 | $this->jwksUri = $jwksUri; 86 | $this->httpClient = $httpClient; 87 | $this->httpFactory = $httpFactory; 88 | $this->cache = $cache; 89 | $this->expiresAfter = $expiresAfter; 90 | $this->rateLimit = $rateLimit; 91 | $this->defaultAlg = $defaultAlg; 92 | $this->setCacheKeys(); 93 | } 94 | 95 | /** 96 | * @param string $keyId 97 | * @return Key 98 | */ 99 | public function offsetGet($keyId): Key 100 | { 101 | if (!$this->keyIdExists($keyId)) { 102 | throw new OutOfBoundsException('Key ID not found'); 103 | } 104 | return $this->keySet[$keyId]; 105 | } 106 | 107 | /** 108 | * @param string $keyId 109 | * @return bool 110 | */ 111 | public function offsetExists($keyId): bool 112 | { 113 | return $this->keyIdExists($keyId); 114 | } 115 | 116 | /** 117 | * @param string $offset 118 | * @param Key $value 119 | */ 120 | public function offsetSet($offset, $value): void 121 | { 122 | throw new LogicException('Method not implemented'); 123 | } 124 | 125 | /** 126 | * @param string $offset 127 | */ 128 | public function offsetUnset($offset): void 129 | { 130 | throw new LogicException('Method not implemented'); 131 | } 132 | 133 | private function keyIdExists(string $keyId): bool 134 | { 135 | if (null === $this->keySet) { 136 | $item = $this->getCacheItem(); 137 | // Try to load keys from cache 138 | if ($item->isHit()) { 139 | // item found! Return it 140 | $jwks = $item->get(); 141 | $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); 142 | } 143 | } 144 | 145 | if (!isset($this->keySet[$keyId])) { 146 | if ($this->rateLimitExceeded()) { 147 | return false; 148 | } 149 | $request = $this->httpFactory->createRequest('GET', $this->jwksUri); 150 | $jwksResponse = $this->httpClient->sendRequest($request); 151 | $jwks = (string) $jwksResponse->getBody(); 152 | $this->keySet = JWK::parseKeySet(json_decode($jwks, true), $this->defaultAlg); 153 | 154 | if (!isset($this->keySet[$keyId])) { 155 | return false; 156 | } 157 | 158 | $item = $this->getCacheItem(); 159 | $item->set($jwks); 160 | if ($this->expiresAfter) { 161 | $item->expiresAfter($this->expiresAfter); 162 | } 163 | $this->cache->save($item); 164 | } 165 | 166 | return true; 167 | } 168 | 169 | private function rateLimitExceeded(): bool 170 | { 171 | if (!$this->rateLimit) { 172 | return false; 173 | } 174 | 175 | $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); 176 | if (!$cacheItem->isHit()) { 177 | $cacheItem->expiresAfter(1); // # of calls are cached each minute 178 | } 179 | 180 | $callsPerMinute = (int) $cacheItem->get(); 181 | if (++$callsPerMinute > $this->maxCallsPerMinute) { 182 | return true; 183 | } 184 | $cacheItem->set($callsPerMinute); 185 | $this->cache->save($cacheItem); 186 | return false; 187 | } 188 | 189 | private function getCacheItem(): CacheItemInterface 190 | { 191 | if (\is_null($this->cacheItem)) { 192 | $this->cacheItem = $this->cache->getItem($this->cacheKey); 193 | } 194 | 195 | return $this->cacheItem; 196 | } 197 | 198 | private function setCacheKeys(): void 199 | { 200 | if (empty($this->jwksUri)) { 201 | throw new RuntimeException('JWKS URI is empty'); 202 | } 203 | 204 | // ensure we do not have illegal characters 205 | $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); 206 | 207 | // add prefix 208 | $key = $this->cacheKeyPrefix . $key; 209 | 210 | // Hash keys if they exceed $maxKeyLength of 64 211 | if (\strlen($key) > $this->maxKeyLength) { 212 | $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); 213 | } 214 | 215 | $this->cacheKey = $key; 216 | 217 | if ($this->rateLimit) { 218 | // add prefix 219 | $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; 220 | 221 | // Hash keys if they exceed $maxKeyLength of 64 222 | if (\strlen($rateLimitKey) > $this->maxKeyLength) { 223 | $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); 224 | } 225 | 226 | $this->rateLimitCacheKey = $rateLimitKey; 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/ScimServerPhp/Firebase/JWT/ExpiredException.php: -------------------------------------------------------------------------------- 1 | keyMaterial = $keyMaterial; 44 | $this->algorithm = $algorithm; 45 | } 46 | 47 | /** 48 | * Return the algorithm valid for this key 49 | * 50 | * @return string 51 | */ 52 | public function getAlgorithm(): string 53 | { 54 | return $this->algorithm; 55 | } 56 | 57 | /** 58 | * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate 59 | */ 60 | public function getKeyMaterial() 61 | { 62 | return $this->keyMaterial; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ScimServerPhp/Firebase/JWT/SignatureInvalidException.php: -------------------------------------------------------------------------------- 1 | logger = $container->get(\Monolog\Logger::class); 19 | } 20 | 21 | public function authenticate(string $credentials, array $authorizationInfo): bool 22 | { 23 | $jwtPayload = []; 24 | $jwtSecret = Util::getConfigFile()['jwt']['secret']; 25 | try { 26 | $jwtPayload = (array) JWT::decode($credentials, new Key($jwtSecret, 'HS256')); 27 | } catch (Exception $e) { 28 | // If we land here, something was wrong with the JWT and auth has thus failed 29 | $this->logger->error($e->getMessage()); 30 | return false; 31 | } 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Util/Filters/FilterParser.php: -------------------------------------------------------------------------------- 1 | 3) { 49 | // throw new FilterException("Incorrectly formatted AttributeExpression"); 50 | // } 51 | 52 | // $attributeExpression = new AttributeExpression( 53 | // $splitFilterExpression[0], 54 | // $splitFilterExpression[1], 55 | // $splitFilterExpression[2] 56 | // ); 57 | 58 | return $attributeExpression; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | get('/Users', ListUsersAction::class)->setName('users.list'); 33 | $app->get('/Users/{id}', GetUserAction::class)->setName('users.get'); 34 | $app->post('/Users', CreateUserAction::class)->setName('users.create'); 35 | $app->put('/Users/{id}', UpdateUserAction::class)->setName('users.update'); 36 | $app->delete('/Users/{id}', DeleteUserAction::class)->setName('users.delete'); 37 | } 38 | 39 | // Group routes 40 | if (in_array('Group', $supportedResourceTypes)) { 41 | $app->get('/Groups', ListGroupsAction::class)->setName('groups.list'); 42 | $app->get('/Groups/{id}', GetGroupAction::class)->setName('groups.get'); 43 | $app->post('/Groups', CreateGroupAction::class)->setName('groups.create'); 44 | $app->put('/Groups/{id}', UpdateGroupAction::class)->setName('groups.update'); 45 | $app->delete('/Groups/{id}', DeleteGroupAction::class)->setName('groups.delete'); 46 | } 47 | 48 | if (in_array('Domain', $supportedResourceTypes)) { 49 | $app->get('/Domains', ListDomainsAction::class)->setName('domains.list'); 50 | $app->get('/Domains/{id}', GetDomainAction::class)->setName('domains.get'); 51 | $app->post('/Domains', CreateDomainAction::class)->setName('domains.create'); 52 | $app->put('/Domains/{id}', UpdateDomainAction::class)->setName('domains.update'); 53 | $app->delete('/Domains/{id}', DeleteDomainAction::class)->setName('domains.delete'); 54 | } 55 | 56 | // ServiceProvider routes 57 | $app->get('/ResourceTypes', ListResourceTypesAction::class)->setName('resourceTypes.list'); 58 | $app->get('/Schemas', ListSchemasAction::class)->setName('schemas.list'); 59 | $app->get( 60 | '/ServiceProviderConfig', 61 | ListServiceProviderConfigurationsAction::class 62 | )->setName('serviceProviderConfigs.list'); 63 | }; 64 | -------------------------------------------------------------------------------- /test/integration/.gitignore: -------------------------------------------------------------------------------- 1 | # Custom hosts files 2 | hosts_* 3 | -------------------------------------------------------------------------------- /test/integration/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | inventory = hosts_local 3 | interpreter_python = auto_silent 4 | stdout_callback = yaml 5 | -------------------------------------------------------------------------------- /test/integration/hosts.sample: -------------------------------------------------------------------------------- 1 | [scim_php_servers] 2 | container_scim_php users_address=http:///Users user=testuser pass=testpass -------------------------------------------------------------------------------- /test/integration/tests.yml: -------------------------------------------------------------------------------- 1 | # Ansible integration tests 2 | --- 3 | - import_playbook: users.yml 4 | tags: 5 | users -------------------------------------------------------------------------------- /test/integration/users.yml: -------------------------------------------------------------------------------- 1 | # Tests /Users SCIM endpoint 2 | --- 3 | - hosts: all 4 | ignore_errors: true 5 | gather_facts: false 6 | tasks: 7 | - name: Create test folder 8 | ansible.builtin.file: 9 | dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}" 10 | state: directory 11 | mode: '0755' 12 | delegate_to: 127.0.0.1 13 | 14 | - name: Get all users 15 | uri: 16 | url: "{{ users_address }}" 17 | method: GET 18 | return_content: true 19 | validate_certs: false 20 | delegate_to: 127.0.0.1 21 | register: call_response 22 | 23 | - name: Store reply under /tmp/ 24 | copy: 25 | content: "{{ call_response.content }}" 26 | dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}/\ 27 | get_all_users.json" 28 | delegate_to: 127.0.0.1 29 | 30 | - hosts: all 31 | ignore_errors: true 32 | gather_facts: false 33 | tasks: 34 | - name: Create a user 35 | uri: 36 | body: '{"userName":"testuser","externalId":"testuserexternal","profileUrl":"https://www.example.com/testuser"}' 37 | body_format: json 38 | url: "{{ users_address }}" 39 | method: POST 40 | status_code: 201 # Expect 201 for a create user response 41 | return_content: true 42 | validate_certs: false 43 | delegate_to: 127.0.0.1 44 | register: call_response 45 | 46 | - name: Store reply under /tmp/ 47 | copy: 48 | content: "{{ call_response.content }}" 49 | dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}/\ 50 | create_user.json" 51 | delegate_to: 127.0.0.1 52 | -------------------------------------------------------------------------------- /test/phpunit.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | unit 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/postman/scim-env.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "938b836a-076e-4938-a04a-3f088c363ff0", 3 | "name": "scim-env", 4 | "values": [ 5 | { 6 | "key": "url", 7 | "value": "http://localhost:8888", 8 | "type": "default", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "superadmin_jwt", 13 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWRtaW5AbG9jYWxob3N0Lm9yZyJ9.z5j4P09bk7StVda48g9_0Jt0LhopiNhjmmeguQCrVx8", 14 | "type": "any", 15 | "enabled": true 16 | }, 17 | { 18 | "key": "non_superadmin_jwt", 19 | "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdEB0ZXN0Lm9yZyJ9.Lu1JcCSUiTRPGeuLgs6k6TG5DgCpuAIyA8IKg_nli5M", 20 | "type": "default", 21 | "enabled": true 22 | } 23 | ], 24 | "_postman_variable_scope": "environment", 25 | "_postman_exported_at": "2022-10-07T10:05:35.659Z", 26 | "_postman_exported_using": "Postman/9.31.0" 27 | } 28 | -------------------------------------------------------------------------------- /test/resources/filterTestGroups.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "displayName": "testGroup", 4 | "members": [] 5 | }, 6 | { 7 | "displayName": "testGroup2", 8 | "members": [] 9 | }, 10 | { 11 | "displayName": "testGroup3", 12 | "members": [ 13 | "12345678-9012-3456-7890-12345678", 14 | "87654321-2109-6543-0987-87654321" 15 | ] 16 | } 17 | ] -------------------------------------------------------------------------------- /test/resources/filterTestUsers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "userName": "testuser", 4 | "profileUrl": "http://example.com/testuser" 5 | }, 6 | { 7 | "userName": "testuser2", 8 | "externalId": "testuser2external", 9 | "name": { 10 | "givenName": "given", 11 | "familyName": "family" 12 | } 13 | }, 14 | { 15 | "userName": "testuser3", 16 | "externalId": "testuser3external", 17 | "profileUrl": "http://example.com/testuser3" 18 | }, 19 | { 20 | "userName": "some user", 21 | "externalId": "testsome_userexternal", 22 | "profileUrl": "http://example.com/some_user" 23 | } 24 | ] -------------------------------------------------------------------------------- /test/resources/mock-test-config.php: -------------------------------------------------------------------------------- 1 | false, // Set to true when deploying in production 5 | 'basePath' => null, // If you want to specify a base path for the Slim app, add it here (e.g., '/test/scim') 6 | 'supportedResourceTypes' => ['User', 'Group'], // Specify all the supported SCIM ResourceTypes by their names here 7 | 8 | // SQLite DB settings 9 | 'db' => [ 10 | 'driver' => 'sqlite', // Type of DB 11 | 'databaseFile' => 'db/scim-mock.sqlite' // DB name 12 | ], 13 | 14 | // Monolog settings 15 | 'logger' => [ 16 | 'name' => 'scim-opf', 17 | 'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../../logs/app.log', 18 | 'level' => \Monolog\Logger::DEBUG, 19 | ], 20 | 21 | // Bearer token settings 22 | 'jwt' => [ 23 | 'secret' => 'secret' 24 | ] 25 | ]; 26 | -------------------------------------------------------------------------------- /test/resources/testGroup.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "testGroup", 3 | "members": ["12345678-9012-3456-7890-12345678"] 4 | } -------------------------------------------------------------------------------- /test/resources/testUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "userName": "testusercreate", 3 | "externalId": "testusercreateexternal", 4 | "profileUrl": "http://example.com/testusercreate" 5 | } -------------------------------------------------------------------------------- /test/unit/FilterParserTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(FilterExpression::class, $attributeFilterExpression); 21 | $this->assertInstanceOf(AttributeExpression::class, $attributeFilterExpression); 22 | 23 | $this->assertEquals("userName", $attributeFilterExpression->getAttributePath()); 24 | $this->assertEquals(AttributeOperator::OP_EQ, $attributeFilterExpression->getCompareOperator()); 25 | $this->assertEquals("sometestusername", $attributeFilterExpression->getComparisonValue()); 26 | 27 | 28 | // Test a "pr" filter expression 29 | $filterString = "meta.created pr"; 30 | $attributeFilterExpression = FilterParser::parseFilterExpression($filterString); 31 | 32 | $this->assertInstanceOf(FilterExpression::class, $attributeFilterExpression); 33 | $this->assertInstanceOf(AttributeExpression::class, $attributeFilterExpression); 34 | 35 | $this->assertEquals("meta.created", $attributeFilterExpression->getAttributePath()); 36 | $this->assertEquals(AttributeOperator::OP_PR, $attributeFilterExpression->getCompareOperator()); 37 | $this->assertNull($attributeFilterExpression->getComparisonValue()); 38 | } 39 | 40 | public function testParseTooShortFilterExpression() 41 | { 42 | $this->expectException(FilterException::class); 43 | $this->expectExceptionMessage("Incorrectly formatted AttributeExpression"); 44 | 45 | $filterString = "somestring"; 46 | $parsedFilterExpression = FilterParser::parseFilterExpression($filterString); 47 | } 48 | 49 | public function testFilterExpressionWithSpacesInValue() 50 | { 51 | $filterString = "userName eq \"some value\""; 52 | $parsedFilterExpression = FilterParser::parseFilterExpression($filterString); 53 | 54 | $this->assertInstanceOf(FilterExpression::class, $parsedFilterExpression); 55 | $this->assertInstanceOf(AttributeExpression::class, $parsedFilterExpression); 56 | 57 | $this->assertEquals("userName", $parsedFilterExpression->getAttributePath()); 58 | $this->assertEquals(AttributeOperator::OP_EQ, $parsedFilterExpression->getCompareOperator()); 59 | $this->assertEquals("\"some value\"", $parsedFilterExpression->getComparisonValue()); 60 | } 61 | 62 | public function testParseIncorrectExpression() 63 | { 64 | $this->expectException(FilterException::class); 65 | $this->expectExceptionMessage("Invalid AttributeOperation passed to AttributeExpression"); 66 | 67 | $filterString = "userName blabla \"some moreblabla\""; 68 | $parsedFilterExpression = FilterParser::parseFilterExpression($filterString); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/unit/FilterUtilTest.php: -------------------------------------------------------------------------------- 1 | scimGroups = json_decode(file_get_contents(__DIR__ . '/../resources/filterTestGroups.json'), true); 21 | $this->scimUsers = json_decode(file_get_contents(__DIR__ . '/../resources/filterTestUsers.json'), true); 22 | } 23 | 24 | public function tearDown(): void 25 | { 26 | $this->scimGroups = []; 27 | $this->scimUsers = []; 28 | } 29 | 30 | public function testGroupFiltering() 31 | { 32 | // "ne" filter test 33 | $filterString = "displayName ne testGroup"; 34 | $filteredScimGroups = FilterUtil::performFiltering($filterString, $this->scimGroups); 35 | 36 | $this->assertEquals(array_splice($this->scimGroups, 1, 2), $filteredScimGroups); 37 | } 38 | 39 | public function testUserFiltering() 40 | { 41 | // "sw" filter test 42 | $filterString = "userName sw testuser"; 43 | $filteredScimUsers = FilterUtil::performFiltering($filterString, $this->scimUsers); 44 | 45 | $this->assertEquals(array_splice($this->scimUsers, 0, 3), $filteredScimUsers); 46 | } 47 | 48 | public function testInvalidFiltering() 49 | { 50 | $this->expectException(FilterException::class); 51 | $this->expectExceptionMessage("Invalid AttributeOperation passed to AttributeExpression"); 52 | 53 | $filterString = "externalId bla some value"; 54 | $filteredScimUsers = FilterUtil::performFiltering($filterString, $this->scimUsers); 55 | } 56 | 57 | public function testFilteringWithSpaces() 58 | { 59 | $filterString = "userName eq some user"; 60 | $filteredScimUsers = FilterUtil::performFiltering($filterString, $this->scimUsers); 61 | 62 | $this->assertEquals(array($this->scimUsers[3]), $filteredScimUsers); 63 | } 64 | 65 | public function testIncorrectPRFilterExpression() 66 | { 67 | $this->expectException(FilterException::class); 68 | $this->expectExceptionMessage("\"pr\" filter operator must be used without a comparison value"); 69 | 70 | $filterString = "userName pr \"some blabla\""; 71 | $parsedFilterExpression = FilterUtil::performFiltering($filterString, $this->scimUsers); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/unit/MockGroupsDataAccessTest.php: -------------------------------------------------------------------------------- 1 | coreGroup = new CoreGroup(); 28 | $this->mockGroupAdapter = new MockGroupAdapter(); 29 | $this->mockGroupDataAccess = new MockGroupDataAccess(); 30 | } 31 | 32 | public function tearDown(): void 33 | { 34 | $this->coreGroup = null; 35 | $this->mockGroupAdapter = null; 36 | $this->mockGroupDataAccess = null; 37 | } 38 | 39 | public function testCreateGroup() 40 | { 41 | $testGroupJson = json_decode(file_get_contents(__DIR__ . '/../resources/testGroup.json'), true); 42 | $this->coreGroup->fromSCIM($testGroupJson); 43 | $mockGroup = $this->mockGroupAdapter->getMockGroup($this->coreGroup); 44 | $groupCreateRes = $this->mockGroupDataAccess->create($mockGroup); 45 | $this->assertNotNull($groupCreateRes); 46 | } 47 | 48 | public function testReadAllGroups() 49 | { 50 | $this->assertNotEmpty($this->mockGroupDataAccess->getAll()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/unit/MockUsersDataAccessTest.php: -------------------------------------------------------------------------------- 1 | coreUser = new CoreUser(); 28 | $this->mockUserDataAccess = new MockUserDataAccess(); 29 | $this->mockUserAdapter = new MockUserAdapter(); 30 | } 31 | 32 | public function tearDown(): void 33 | { 34 | $this->coreUser = null; 35 | $this->mockUserDataAccess = null; 36 | $this->mockUserAdapter = null; 37 | } 38 | 39 | public function testCreateUser() 40 | { 41 | $testUserJson = json_decode(file_get_contents(__DIR__ . '/../resources/testUser.json'), true); 42 | $this->coreUser->fromSCIM($testUserJson); 43 | $mockUser = $this->mockUserAdapter->getMockUser($this->coreUser); 44 | $userCreateRes = $this->mockUserDataAccess->create($mockUser); 45 | $this->assertNotNull($userCreateRes); 46 | } 47 | 48 | public function testReadAllUsers() 49 | { 50 | $this->assertNotEmpty($this->mockUserDataAccess->getAll()); 51 | } 52 | } 53 | --------------------------------------------------------------------------------