Publish first version of scim-server-php

- SCIM 2.0 server core library
- Postfix Admin SCIM API
- Mock SCIM server
This commit is contained in:
Julien Schneider 2022-07-01 11:18:23 +02:00
parent 7bd7ed667c
commit 693b732bd2
84 changed files with 13384 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
/vendor/
/logs/
*.sqlite
*.cache
/config/config.php
/.idea/

49
Makefile Normal file
View file

@ -0,0 +1,49 @@
# Default port to start server on
PORT := 8888
.PHONY: clean
clean:
rm -rf ./vendor
.PHONY: install
install:
composer install --prefer-dist
php db/database.php
.PHONY: start-server
start-server:
composer install --prefer-dist
php db/database.php
php -S localhost:$(PORT) -t public/ public/index.php
# linting based on https://github.com/dbfx/github-phplint
.PHONY: lint
lint:
# Make sure we don't lint test classes
composer install --prefer-dist --no-dev
# Lint for installed PHP version
sh -c "! (find . -type f -name \"*.php\" -not -path \"./build/*\" -not -path \"./vendor/*\" $1 -exec php -l -n {} \; | grep -v \"No syntax errors detected\")"
# Make devtools available again
composer install --prefer-dist
# Lint with CodeSniffer
vendor/bin/phpcs --standard=phpcs.xml src/
.PHONY: api_test
api_test:
# If we pass the PFA_API_TEST variable with value 1, we can run the PFA API tests
ifeq ($(PFA_API_TEST),1)
newman run test/postman/scim-opf-pfa.postman_collection.json -e test/postman/scim-env.postman_environment.json
else
newman run test/postman/scim-opf.postman_collection.json -e test/postman/scim-env.postman_environment.json
endif
.PHONY: unit_test
unit_test:
composer install --prefer-dist
vendor/bin/phpunit -c test/phpunit.xml --testdox
.PHONY: fulltest
fulltest: lint api_test unit_test

View file

@ -1 +1,82 @@
# scim-server-php
**scim-server-php** is a PHP library making it easy to implement [SCIM v2.0](https://datatracker.ietf.org/wg/scim/documents/) server endpoints for various systems.
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)
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 😉
The **scim-server-php** project currently includes the following:
* A SCIM 2.0 server core library
* A [Postfix Admin](https://github.com/postfixadmin/postfixadmin) SCIM API
**scim-server-php** also comes with an integrated Mock SCIM server based on a SQLite database.
## SCIM 2.0 server core library
This library provides:
* Standard SCIM resources implementations (*Core User*, *Enterprise User* and *Groups*)
* Custom SCIM resource *Provisioning User* implementation
* Standard CRUD operation on above SCIM resources
* A HTTP server handling requests and responses on defined endpoints, based on the [Slim](https://www.slimframework.com/) framework
* A very simple JWT implementation
* When enabled, a JWT token is generated on the `/jwt` endpoint. You **must** therefore protect this endpoint.
* When enabled, this JWT token needs to be provided in all requests using the Bearer schema (`Authorization: Bearer <token>`)
* An easily reusable code architecture for implementing SCIM servers
## Postfix Admin SCIM API
The [Postfix Admin](https://github.com/postfixadmin/postfixadmin) API enables SCIM server capabilities for [Postfix Admin](https://github.com/postfixadmin/postfixadmin). It uses the core library above.
It supports standard GET, POST, PUT and DELETE operations on SCIM *Provisioning User* resources, which are translated in the corresponding operations on the [Postfix Admin](https://github.com/postfixadmin/postfixadmin) mailboxes.
Example (null values removed for readability):
```
$ curl https://my.postfix.admin.url/Users/aaaa@bli.fr -H 'Authorization: Bearer <token>'
{
"schemas":[
"urn:ietf:params:scim:schemas:core:2.0:User",
"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User"
],
"id":"aaaa@bli.fr",
"meta":{
"resourceType":"User",
"created":"2022-05-27 12:45:08",
"location":"https://my.postfix.admin.url/Users/aaaa@bli.fr",
"updated":"2022-06-15 13:07:30"
},
"userName":"aaaa@bli.fr",
"name":{
"formatted":"Aaaa"
},
"displayName":"Aaaa",
"active":"1",
"urn:audriga:params:scim:schemas:extension:provisioning:2.0:User":{
"sizeQuota":51200000
}
}
```
## Prerequisites
* **scim-server-php** requires PHP 7.4
* Dependencies are managed with [composer](https://getcomposer.org/)
## Installation
### Local installation
* Run `make install` to automatically install dependencies
### Configuration
* To switch from the Mock SCIM server to the Postfix Admin SCIM API, you simply need to adapt the `public/index.php` file (include **one** of the following):
```
// Set up system-specific dependencies
$dependencies = require dirname(__DIR__) . '/src/Dependencies/mock-dependencies.php'; // include that line if you want to use the integrated mock SCIM server
$dependencies = require dirname(__DIR__) . '/src/Dependencies/pfa-dependencies.php'; // include that line if you want to use the Postfix Admin SCIM API
```
## Acknowledgements
This software is part of the [Open Provisioning Framework](https://www.audriga.com/en/User_provisioning/Open_Provisioning_Framework) project that has received funding from the European Union's Horizon 2020 research and innovation program under grant agreement No. 871498.

31
composer.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "audriga/scim-opf",
"description": "An open provisioning framework using the SCIM protocol",
"type": "library",
"require": {
"slim/slim": "^4.10",
"illuminate/database": "^8.83",
"php": "^7.4",
"slim/php-view": "^3.1",
"monolog/monolog": "^2.4",
"tuupola/slim-jwt-auth": "^3.6",
"ramsey/uuid": "^4.2",
"slim/psr7": "^1.5",
"php-di/php-di": "^6.3"
},
"autoload": {
"psr-4": {
"Opf\\": "src/"
}
},
"authors": [
{
"name": "Stanimir Bozhilov",
"email": "stanimir@audriga.com"
}
],
"require-dev": {
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.6"
}
}

5380
composer.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,74 @@
{
"id": "urn:ietf:params:scim:schemas:core:2.0:Group",
"name": "Group",
"description": "Group",
"attributes": [
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "A human-readable name for the Group. REQUIRED.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "members",
"type": "complex",
"multiValued": true,
"description": "A list of members of the Group.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "Identifier of the member of this Group.",
"required": false,
"caseExact": false,
"mutability": "immutable",
"returned": "default",
"uniqueness": "none"
},
{
"name": "$ref",
"type": "reference",
"referenceTypes": [
"User",
"Group"
],
"multiValued": false,
"description": "The URI corresponding to a SCIM resource that is a member of this Group.",
"required": false,
"caseExact": false,
"mutability": "immutable",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the type of resource, e.g., 'User' or 'Group'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"User",
"Group"
],
"mutability": "immutable",
"returned": "default",
"uniqueness": "none"
}
],
"mutability": "readWrite",
"returned": "default"
}
],
"meta": {
"resourceType": "Schema",
"location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:Group"
}
}

View file

@ -0,0 +1,773 @@
{
"id": "urn:ietf:params:scim:schemas:core:2.0:User",
"name": "User",
"description": "User Account",
"attributes": [
{
"name": "userName",
"type": "string",
"multiValued": false,
"description": "Unique identifier for the User, typically used by the user to directly authenticate to the service provider. Each User MUST include a non-empty userName value. This identifier MUST be unique across the service provider's entire set of Users. REQUIRED.",
"required": true,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "server"
},
{
"name": "name",
"type": "complex",
"multiValued": false,
"description": "The components of the user's real name. Providers MAY return just the full name as a single string in the formatted sub-attribute, or they MAY return just the individual component attributes using the other sub-attributes, or they MAY return both. If both variants are returned, they SHOULD be describing the same name, with the formatted name indicating how the component attributes should be combined.",
"required": false,
"subAttributes": [
{
"name": "formatted",
"type": "string",
"multiValued": false,
"description": "The full name, including all middle names, titles, and suffixes as appropriate, formatted for display (e.g., 'Ms. Barbara J Jensen, III').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "familyName",
"type": "string",
"multiValued": false,
"description": "The family name of the User, or last name in most Western languages (e.g., 'Jensen' given the full name 'Ms. Barbara J Jensen, III').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "givenName",
"type": "string",
"multiValued": false,
"description": "The given name of the User, or first name in most Western languages (e.g., 'Barbara' given the full name 'Ms. Barbara J Jensen, III').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "middleName",
"type": "string",
"multiValued": false,
"description": "The middle name(s) of the User (e.g., 'Jane' given the full name 'Ms. Barbara J Jensen, III').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "honorificPrefix",
"type": "string",
"multiValued": false,
"description": "The honorific prefix(es) of the User, or title in most Western languages (e.g., 'Ms.' given the full name 'Ms. Barbara J Jensen, III').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "honorificSuffix",
"type": "string",
"multiValued": false,
"description": "The honorific suffix(es) of the User, or suffix in most Western languages (e.g., 'III' given the full name 'Ms. Barbara J Jensen, III').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
}
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described, if known.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "nickName",
"type": "string",
"multiValued": false,
"description": "The casual way to address the user in real life, e.g., 'Bob' or 'Bobby' instead of 'Robert'. This attribute SHOULD NOT be used to represent a User's username (e.g., 'bjensen' or 'mpepperidge').",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "profileUrl",
"type": "reference",
"referenceTypes": [
"external"
],
"multiValued": false,
"description": "A fully qualified URL pointing to a page representing the User's online profile.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "title",
"type": "string",
"multiValued": false,
"description": "The user's title, such as \"Vice President.\"",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "userType",
"type": "string",
"multiValued": false,
"description": "Used to identify the relationship between the organization and the user. Typical values used might be 'Contractor', 'Employee', 'Intern', 'Temp', 'External', and 'Unknown', but any value may be used.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "preferredLanguage",
"type": "string",
"multiValued": false,
"description": "Indicates the User's preferred written or spoken language. Generally used for selecting a localized user interface; e.g., 'en_US' specifies the language English and country US.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "locale",
"type": "string",
"multiValued": false,
"description": "Used to indicate the User's default location for purposes of localizing items such as currency, date time format, or numerical representations.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "timezone",
"type": "string",
"multiValued": false,
"description": "The User's time zone in the 'Olson' time zone database format, e.g., 'America/Los_Angeles'.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "active",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the User's administrative status.",
"required": false,
"mutability": "readWrite",
"returned": "default"
},
{
"name": "password",
"type": "string",
"multiValued": false,
"description": "The User's cleartext password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User's password.",
"required": false,
"caseExact": false,
"mutability": "writeOnly",
"returned": "never",
"uniqueness": "none"
},
{
"name": "emails",
"type": "complex",
"multiValued": true,
"description": "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., 'bjensen@example.com' instead of 'bjensen@EXAMPLE.COM'. Canonical type values of 'work', 'home', and 'other'.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function, e.g., 'work' or 'home'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"work",
"home",
"other"
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "phoneNumbers",
"type": "complex",
"multiValued": true,
"description": "Phone numbers for the User. The value SHOULD be canonicalized by the service provider according to the format specified in RFC 3966, e.g., 'tel:+1-201-555-0123'. Canonical type values of 'work', 'home', 'mobile', 'fax', 'pager', and 'other'.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "Phone number of the User.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function, e.g., 'work', 'home', 'mobile'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"work",
"home",
"mobile",
"fax",
"pager",
"other"
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred phone number or primary phone number. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default"
},
{
"name": "ims",
"type": "complex",
"multiValued": true,
"description": "Instant messaging addresses for the User.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "Instant messaging address for the User.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function, e.g., 'aim', 'gtalk', 'xmpp'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"aim",
"gtalk",
"icq",
"xmpp",
"msn",
"skype",
"qq",
"yahoo"
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default"
},
{
"name": "photos",
"type": "complex",
"multiValued": true,
"description": "URLs of photos of the User.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "reference",
"referenceTypes": [
"external"
],
"multiValued": false,
"description": "URL of a photo of the User.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function, i.e., 'photo' or 'thumbnail'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"photo",
"thumbnail"
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute, e.g., the preferred photo or thumbnail. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default"
},
{
"name": "addresses",
"type": "complex",
"multiValued": true,
"description": "A physical mailing address for this User. Canonical type values of 'work', 'home', and 'other'. This attribute is a complex type with the following sub-attributes.",
"required": false,
"subAttributes": [
{
"name": "formatted",
"type": "string",
"multiValued": false,
"description": "The full mailing address, formatted for display or use with a mailing label. This attribute MAY contain newlines.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "streetAddress",
"type": "string",
"multiValued": false,
"description": "The full street address component, which may include house number, street name, P.O. box, and multi-line extended street address information. This attribute MAY contain newlines.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "locality",
"type": "string",
"multiValued": false,
"description": "The city or locality component.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "region",
"type": "string",
"multiValued": false,
"description": "The state or region component.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "postalCode",
"type": "string",
"multiValued": false,
"description": "The zip code or postal code component.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "country",
"type": "string",
"multiValued": false,
"description": "The country name component.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function, e.g., 'work' or 'home'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"work",
"home",
"other"
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
}
],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "groups",
"type": "complex",
"multiValued": true,
"description": "A list of groups to which the user belongs, either through direct membership, through nested groups, or dynamically calculated.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "The identifier of the User's group.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "$ref",
"type": "reference",
"referenceTypes": [
"User",
"Group"
],
"multiValued": false,
"description": "The URI of the corresponding 'Group' resource to which the user belongs.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function, e.g., 'direct' or 'indirect'.",
"required": false,
"caseExact": false,
"canonicalValues": [
"direct",
"indirect"
],
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
}
],
"mutability": "readOnly",
"returned": "default"
},
{
"name": "entitlements",
"type": "complex",
"multiValued": true,
"description": "A list of entitlements for the User that represent a thing the User has.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "The value of an entitlement.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default"
},
{
"name": "roles",
"type": "complex",
"multiValued": true,
"description": "A list of roles for the User that collectively represent who the User is, e.g., 'Student', 'Faculty'.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "The value of a role.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function.",
"required": false,
"caseExact": false,
"canonicalValues": [],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default"
},
{
"name": "x509Certificates",
"type": "complex",
"multiValued": true,
"description": "A list of certificates issued to the User.",
"required": false,
"caseExact": false,
"subAttributes": [
{
"name": "value",
"type": "binary",
"multiValued": false,
"description": "The value of an X.509 certificate.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "display",
"type": "string",
"multiValued": false,
"description": "A human-readable name, primarily used for display purposes. READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "A label indicating the attribute's function.",
"required": false,
"caseExact": false,
"canonicalValues": [],
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "primary",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating the 'primary' or preferred attribute value for this attribute. The primary attribute value 'true' MUST appear no more than once.",
"required": false,
"mutability": "readWrite",
"returned": "default"
}
],
"mutability": "readWrite",
"returned": "default"
}
],
"meta": {
"resourceType": "Schema",
"location": "/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"
}
}

View file

@ -0,0 +1,113 @@
{
"id": "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
"name": "EnterpriseUser",
"description": "Enterprise User",
"attributes": [
{
"name": "employeeNumber",
"type": "string",
"multiValued": false,
"description": "Numeric or alphanumeric identifier assigned to a person, typically based on order of hire or association with an organization.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "costCenter",
"type": "string",
"multiValued": false,
"description": "Identifies the name of a cost center.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "organization",
"type": "string",
"multiValued": false,
"description": "Identifies the name of an organization.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "division",
"type": "string",
"multiValued": false,
"description": "Identifies the name of a division.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "department",
"type": "string",
"multiValued": false,
"description": "Identifies the name of a department.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "manager",
"type": "complex",
"multiValued": false,
"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.",
"required": false,
"subAttributes": [
{
"name": "value",
"type": "string",
"multiValued": false,
"description": "The id of the SCIM resource representing the User's manager. REQUIRED.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "$ref",
"type": "reference",
"referenceTypes": [
"User"
],
"multiValued": false,
"description": "The URI of the SCIM resource representing the User's manager. REQUIRED.",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
},
{
"name": "displayName",
"type": "string",
"multiValued": false,
"description": "The displayName of the User's manager. OPTIONAL and READ-ONLY.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
}
],
"mutability": "readWrite",
"returned": "default"
}
],
"meta": {
"resourceType": "Schema",
"location": "/v2/Schemas/urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
}
}

View file

@ -0,0 +1,22 @@
{
"id": "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User",
"name": "ProvisioningUser",
"description": "Provisioning User",
"attributes": [
{
"name": "sizeQuota",
"type": "int",
"multiValued": false,
"description": "Size quota assigned to a user, in bytes",
"required": false,
"caseExact": false,
"mutability": "readWrite",
"returned": "default",
"uniqueness": "none"
}
],
"meta": {
"resourceType": "Schema",
"location": "/v2/Schemas/urn:audriga:params:scim:schemas:extension:provisioning:2.0:User"
}
}

View file

@ -0,0 +1,102 @@
{
"id": "urn:ietf:params:scim:schemas:core:2.0:ResourceType",
"name": "ResourceType",
"description": "Specifies the schema that describes a SCIM resource type",
"attributes": [
{
"name": "id",
"type": "string",
"multiValued": false,
"description": "The resource type's server unique id. May be the same as the 'name' attribute.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "name",
"type": "string",
"multiValued": false,
"description": "The resource type name. When applicable, service providers MUST specify the name, e.g., 'User'.",
"required": true,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "description",
"type": "string",
"multiValued": false,
"description": "The resource type's human-readable description. When applicable, service providers MUST specify the description.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "endpoint",
"type": "reference",
"referenceTypes": [
"uri"
],
"multiValued": false,
"description": "The resource type's HTTP-addressable endpoint relative to the Base URL, e.g., '/Users'.",
"required": true,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "schema",
"type": "reference",
"referenceTypes": [
"uri"
],
"multiValued": false,
"description": "The resource type's primary/base schema URI.",
"required": true,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "schemaExtensions",
"type": "complex",
"multiValued": false,
"description": "A list of URIs of the resource type's schema extensions.",
"required": true,
"mutability": "readOnly",
"returned": "default",
"subAttributes": [
{
"name": "schema",
"type": "reference",
"referenceTypes": [
"uri"
],
"multiValued": false,
"description": "The URI of a schema extension.",
"required": true,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "required",
"type": "boolean",
"multiValued": false,
"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.",
"required": true,
"mutability": "readOnly",
"returned": "default"
}
]
}
]
}

View file

@ -0,0 +1,344 @@
{
"id": "urn:ietf:params:scim:schemas:core:2.0:Schema",
"name": "Schema",
"description": "Specifies the schema that describes a SCIM schema",
"attributes": [
{
"name": "id",
"type": "string",
"multiValued": false,
"description": "The unique URI of the schema. When applicable, service providers MUST specify the URI.",
"required": true,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "name",
"type": "string",
"multiValued": false,
"description": "The schema's human-readable name. When applicable, service providers MUST specify the name, e.g., 'User'.",
"required": true,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "description",
"type": "string",
"multiValued": false,
"description": "The schema's human-readable name. When applicable, service providers MUST specify the name, e.g., 'User'.",
"required": false,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "attributes",
"type": "complex",
"multiValued": true,
"description": "A complex attribute that includes the attributes of a schema.",
"required": true,
"mutability": "readOnly",
"returned": "default",
"subAttributes": [
{
"name": "name",
"type": "string",
"multiValued": false,
"description": "The attribute's name.",
"required": true,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "The attribute's data type. Valid values include 'string', 'complex', 'boolean', 'decimal', 'integer', 'dateTime', 'reference'.",
"required": true,
"canonicalValues": [
"string",
"complex",
"boolean",
"decimal",
"integer",
"dateTime",
"reference"
],
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "multiValued",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating an attribute's plurality.",
"required": true,
"mutability": "readOnly",
"returned": "default"
},
{
"name": "description",
"type": "string",
"multiValued": false,
"description": "A human-readable description of the attribute.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "required",
"type": "boolean",
"multiValued": false,
"description": "A boolean value indicating whether or not the attribute is required.",
"required": false,
"mutability": "readOnly",
"returned": "default"
},
{
"name": "canonicalValues",
"type": "string",
"multiValued": true,
"description": "A collection of canonical values. When applicable, service providers MUST specify the canonical types, e.g., 'work', 'home'.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "caseExact",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating whether or not a string attribute is case sensitive.",
"required": false,
"mutability": "readOnly",
"returned": "default"
},
{
"name": "mutability",
"type": "string",
"multiValued": false,
"description": "Indicates whether or not an attribute is modifiable.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"readOnly",
"readWrite",
"immutable",
"writeOnly"
]
},
{
"name": "returned",
"type": "string",
"multiValued": false,
"description": "Indicates when an attribute is returned in a response (e.g., to a query).",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"always",
"never",
"default",
"request"
]
},
{
"name": "uniqueness",
"type": "string",
"multiValued": false,
"description": "Indicates how unique a value must be.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"none",
"server",
"global"
]
},
{
"name": "referenceTypes",
"type": "string",
"multiValued": true,
"description": "Used only with an attribute of type 'reference'. Specifies a SCIM resourceType that a reference attribute MAY refer to, e.g., 'User'.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "subAttributes",
"type": "complex",
"multiValued": true,
"description": "Used to define the sub-attributes of a complex attribute.",
"required": false,
"mutability": "readOnly",
"returned": "default",
"subAttributes": [
{
"name": "name",
"type": "string",
"multiValued": false,
"description": "The attribute's name.",
"required": true,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "type",
"type": "string",
"multiValued": false,
"description": "The attribute's data type. Valid values include 'string', 'complex', 'boolean', 'decimal', 'integer', 'dateTime', 'reference'.",
"required": true,
"caseExact": false,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"string",
"complex",
"boolean",
"decimal",
"integer",
"dateTime",
"reference"
]
},
{
"name": "multiValued",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating an attribute's plurality.",
"required": true,
"mutability": "readOnly",
"returned": "default"
},
{
"name": "description",
"type": "string",
"multiValued": false,
"description": "A human-readable description of the attribute.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "required",
"type": "boolean",
"multiValued": false,
"description": "A boolean value indicating whether or not the attribute is required.",
"required": false,
"mutability": "readOnly",
"returned": "default"
},
{
"name": "canonicalValues",
"type": "string",
"multiValued": true,
"description": "A collection of canonical values. When applicable, service providers MUST specify the canonical types, e.g., 'work', 'home'.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
},
{
"name": "caseExact",
"type": "boolean",
"multiValued": false,
"description": "A Boolean value indicating whether or not a string attribute is case sensitive.",
"required": false,
"mutability": "readOnly",
"returned": "default"
},
{
"name": "mutability",
"type": "string",
"multiValued": false,
"description": "Indicates whether or not an attribute is modifiable.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"readOnly",
"readWrite",
"immutable",
"writeOnly"
]
},
{
"name": "returned",
"type": "string",
"multiValued": false,
"description": "Indicates when an attribute is returned in a response (e.g., to a query).",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"always",
"never",
"default",
"request"
]
},
{
"name": "uniqueness",
"type": "string",
"multiValued": false,
"description": "Indicates how unique a value must be.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none",
"canonicalValues": [
"none",
"server",
"global"
]
},
{
"name": "referenceTypes",
"type": "string",
"multiValued": false,
"description": "Used only with an attribute of type 'reference'. Specifies a SCIM resourceType that a reference attribute MAY refer to, e.g., 'User'.",
"required": false,
"caseExact": true,
"mutability": "readOnly",
"returned": "default",
"uniqueness": "none"
}
]
}
]
}
]
}

View file

@ -0,0 +1,51 @@
{
"schemas": [
"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"
],
"documentationUri": null,
"patch": {
"supported": false
},
"bulk": {
"supported": false,
"maxOperations": 0,
"maxPayloadSize": 0
},
"filter": {
"supported": true,
"maxResults": 200
},
"changePassword": {
"supported": false
},
"sort": {
"supported": false
},
"etag": {
"supported": false
},
"authenticationSchemes": [
{
"name": "HTTP Basic",
"description": "Authentication scheme using the HTTP Basic Standard",
"specUri": "http://www.rfc-editor.org/info/rfc2617",
"documentationUri": null,
"type": "httpbasic"
},
{
"name": "OAuth Bearer Token",
"description": "Authentication scheme using the OAuth Bearer Token Standard",
"specUri": "http://www.rfc-editor.org/info/rfc6750",
"documentationUri": "http://example.com/help/oauth.html",
"type": "oauthbearertoken",
"primary": true
}
],
"meta": {
"location": "https://example.com/v2/ServiceProviderConfig",
"resourceType": "ServiceProviderConfig",
"created": "2010-01-23T04:56:22Z",
"lastModified": "2011-05-13T04:42:34Z",
"version": "W\/\"3694e05e9dff594\""
}
}

36
config/config.default.php Normal file
View file

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

29
db/database.php Normal file
View file

@ -0,0 +1,29 @@
<?php
$database = new SQLite3('db/scim-mock.sqlite');
// Reset
// $database->exec("DROP TABLE users");
// $database->exec("DROP TABLE groups");
$user_db_sql = "CREATE TABLE IF NOT EXISTS users (
id varchar(160) NOT NULL UNIQUE,
userName varchar(160) NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
externalId varchar(160) NULL,
profileUrl varchar(160) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NULL
)";
$database->exec($user_db_sql);
$group_db_sql = "CREATE TABLE IF NOT EXISTS groups (
id varchar(160) NOT NULL UNIQUE,
displayName varchar(160) NOT NULL DEFAULT '',
members TEXT NOT NULL DEFAULT '',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NULL
)";
$database->exec($group_db_sql);

11
phpcs.xml Normal file
View file

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<ruleset name="Audriga Standard" namespace="Audriga\CS\Standard">
<description>The coding standard for audriga.</description>
<!--<config name="php_version" value="70033"/>-->
<!-- Include the whole PSR12 standard -->
<rule ref="PSR12">
<!-- This does not seem to care that we set PHP 7 above -->
<exclude name="PSR12.Properties.ConstantVisibility"/>
</rule>
</ruleset>

78
public/index.php Normal file
View file

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

View file

@ -0,0 +1,11 @@
<?php
namespace Opf\Adapters;
/**
* This class serves as the basis for the implementation of adapters that translate between
* the SCIM data model and custom provisioning data models
*/
abstract class AbstractAdapter
{
}

View file

@ -0,0 +1,78 @@
<?php
namespace Opf\Adapters\Groups;
use Opf\Adapters\AbstractAdapter;
use Opf\DataAccess\Groups\MockGroupDataAccess;
class MockGroupAdapter extends AbstractAdapter
{
/** @var Opf\Models\MockGroup $group */
private $group;
public function getGroup()
{
return $this->group;
}
public function setGroup(MockGroupDataAccess $group)
{
$this->group = $group;
}
public function getId()
{
if (isset($this->group->id) && !empty($this->group->id)) {
return $this->group->id;
}
}
public function setId($id)
{
if (isset($id) && !empty($id)) {
$this->group->id = $id;
}
}
public function getCreatedAt()
{
if (isset($this->group->created_at) && !empty($this->group->created_at)) {
return $this->group->created_at;
}
}
public function setCreatedAt($createdAt)
{
if (isset($createdAt) && !empty($createdAt)) {
$this->group->created_at = $createdAt;
}
}
public function getDisplayName()
{
if (isset($this->group->displayName) && !empty($this->group->displayName)) {
return $this->group->displayName;
}
}
public function setDisplayName($displayName)
{
if (isset($displayName) && !empty($displayName)) {
$this->group->displayName = $displayName;
}
}
public function getMembers()
{
if (isset($this->group->members) && !empty($this->group->members)) {
return $this->group->members;
}
}
public function setMembers($members)
{
if (isset($members) && !empty($members)) {
$this->group->members = $members;
}
}
}

View file

@ -0,0 +1,106 @@
<?php
namespace Opf\Adapters\Users;
use Opf\Adapters\AbstractAdapter;
use Opf\DataAccess\Users\MockUserDataAccess;
class MockUserAdapter extends AbstractAdapter
{
/** @var Opf\Models\MockUser $user */
private $user;
public function getUser()
{
return $this->user;
}
public function setUser(MockUserDataAccess $user)
{
$this->user = $user;
}
public function getId()
{
if (isset($this->user->id) && !empty($this->user->id)) {
return $this->user->id;
}
}
public function setId($id)
{
if (isset($id) && !empty($id)) {
$this->user->id = $id;
}
}
public function getUserName()
{
if (isset($this->user->userName) && !empty($this->user->userName)) {
return $this->user->userName;
}
}
public function setUserName($userName)
{
if (isset($userName) && !empty($userName)) {
$this->user->userName = $userName;
}
}
public function getCreatedAt()
{
if (isset($this->user->created_at) && !empty($this->user->created_at)) {
return $this->user->created_at;
}
}
public function setCreatedAt($createdAt)
{
if (isset($createdAt) && !empty($createdAt)) {
$this->user->created_at = $createdAt;
}
}
public function getActive()
{
if (isset($this->user->active) && !empty($this->user->active)) {
return boolval($this->user->active);
}
}
public function setActive($active)
{
if (isset($active) && !empty($active)) {
$this->user->active = $active;
}
}
public function getExternalId()
{
if (isset($this->user->externalId) && !empty($this->user->externalId)) {
return $this->user->externalId;
}
}
public function setExternalId($externalId)
{
if (isset($externalId) && !empty($externalId)) {
$this->user->externalId = $externalId;
}
}
public function getProfileUrl()
{
if (isset($this->user->profileUrl) && !empty($this->user->profileUrl)) {
return $this->user->profileUrl;
}
}
public function setProfileUrl($profileUrl)
{
if (isset($profileUrl) && !empty($profileUrl)) {
$this->user->profileUrl = $profileUrl;
}
}
}

View file

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

View file

@ -0,0 +1,28 @@
<?php
namespace Opf\Controllers;
use Monolog\Logger;
use Opf\Util\Util;
use Psr\Container\ContainerInterface;
class Controller
{
protected $container;
protected $logger;
protected $repository;
// The Slim app's base path that we need when constructing the SCIM "location" property
protected $basePath;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->logger = $this->container->get(Logger::class);
$config = Util::getConfigFile();
if (isset($config['basePath']) && !empty($config['basePath'])) {
$this->basePath = $config['basePath'];
}
}
}

View file

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

View file

@ -0,0 +1,32 @@
<?php
namespace Opf\Controllers\Groups;
use Opf\Controllers\Controller;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class DeleteGroupAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('GroupsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("DELETE Group");
$id = $request->getAttribute('id');
$this->logger->info("ID: " . $id);
$deleteRes = $this->repository->delete($id);
if (!$deleteRes) {
$this->logger->info("Not found");
return $response->withStatus(404);
}
$this->logger->info("Group deleted");
return $response->withStatus(200);
}
}

View file

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

View file

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

View file

@ -0,0 +1,65 @@
<?php
namespace Opf\Controllers\Groups;
use Opf\Controllers\Controller;
use Opf\Repositories\Groups\MockGroupsRepository;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class UpdateGroupAction extends Controller
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->repository = $this->container->get('GroupsRepository');
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("UPDATE Group");
$this->logger->info($request->getBody());
$uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
$id = $request->getAttribute('id');
$this->logger->info("ID: " . $id);
$group = $this->repository->getOneById($id);
if (!isset($group) || empty($group)) {
$this->logger->info("Not found");
return $response->withStatus(404);
}
try {
$group = $this->repository->update($id, $request->getParsedBody());
if (isset($group) && !empty($group)) {
$scimGroup = $group->toSCIM(false, $baseUrl);
$responseBody = json_encode($scimGroup, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 201);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
} else {
$this->logger->error("Error updating group");
$errorResponseBody = json_encode(["Errors" => ["decription" => "Error updating group", "code" => 400]]);
$response = new Response($status = 400);
$response->getBody()->write($errorResponseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
} catch (\Exception $e) {
$this->logger->error("Error updating group: " . $e->getMessage());
$errorResponseBody = json_encode(["Errors" => ["description" => $e->getMessage(), "code" => 400]]);
$response = new Response($status = 400);
$response->getBody()->write($errorResponseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}
}

View file

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

View file

@ -0,0 +1,78 @@
<?php
namespace Opf\Controllers\ServiceProviders;
use Opf\Controllers\Controller;
use Opf\Models\SCIM\Standard\CoreCollection;
use Opf\Models\SCIM\Standard\Service\CoreResourceType;
use Opf\Models\SCIM\Standard\Service\CoreSchemaExtension;
use Opf\Util\Util;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
// TODO: Refactor this action class at some point to properly deliver configurable ResourceType entities
final class ListResourceTypesAction extends Controller
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("GET ResourceTypes");
$uri = $request->getUri();
$baseUrl = sprintf('%s://%s', $uri->getScheme(), $uri->getAuthority() . $this->basePath);
// Check which resource types are supported via the config file and in this method further down below
// make sure to only return those that are indeed supported
$config = Util::getConfigFile();
$supportedResourceTypes = $config['supportedResourceTypes'];
$scimResourceTypes = [];
if (in_array('User', $supportedResourceTypes)) {
$userResourceType = new CoreResourceType();
$userResourceType->setId("User");
$userResourceType->setName("User");
$userResourceType->setEndpoint("/Users");
$userResourceType->setDescription("User Account");
$userResourceType->setSchema(Util::USER_SCHEMA);
if (in_array('EnterpriseUser', $supportedResourceTypes)) {
$enterpriseUserSchemaExtension = new CoreSchemaExtension();
$enterpriseUserSchemaExtension->setSchema(Util::ENTERPRISE_USER_SCHEMA);
$enterpriseUserSchemaExtension->setRequired(true);
$userResourceType->setSchemaExtensions(array($enterpriseUserSchemaExtension));
}
if (in_array('ProvisioningUser', $supportedResourceTypes)) {
$provisioningUserSchemaExtension = new CoreSchemaExtension();
$provisioningUserSchemaExtension->setSchema(Util::PROVISIONING_USER_SCHEMA);
$provisioningUserSchemaExtension->setRequired(true);
$userResourceType->setSchemaExtensions(array($provisioningUserSchemaExtension));
}
$scimResourceTypes[] = $userResourceType->toSCIM(false, $baseUrl);
}
if (in_array('Group', $supportedResourceTypes)) {
$groupResourceType = new CoreResourceType();
$groupResourceType->setId("Group");
$groupResourceType->setName("Group");
$groupResourceType->setEndpoint("/Groups");
$groupResourceType->setDescription("Group");
$groupResourceType->setSchema("urn:ietf:params:scim:schemas:core:2.0:Group");
$groupResourceType->setSchemaExtensions([]);
$scimResourceTypes[] = $groupResourceType->toSCIM(false, $baseUrl);
}
$scimResourceTypeCollection = (new CoreCollection($scimResourceTypes))->toSCIM(false);
$responseBody = json_encode($scimResourceTypeCollection, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Opf\Controllers\ServiceProviders;
use Opf\Controllers\Controller;
use Opf\Models\SCIM\Standard\CoreCollection;
use Opf\Util\Util;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class ListSchemasAction extends Controller
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("GET Schemas");
$config = Util::getConfigFile();
$supportedSchemas = $config['supportedResourceTypes'];
$mandatorySchemas = ['Schema', 'ResourceType'];
$scimSchemas = [];
// We store the schemas that the SCIM server supports in separate JSON files
// That's why we try to read them here and add them to $scimSchemas, which
// in turn is then put into the SCIM response body
$pathToSchemasDir = dirname(__DIR__, 3) . '/config/Schema';
$schemaFiles = scandir($pathToSchemasDir, SCANDIR_SORT_NONE);
// If scandir() failed (i.e., it returned false), then return 404 (is this spec-compliant?)
if ($schemaFiles === false) {
$this->logger->info("No Schemas found");
$response = new Response($status = 404);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
foreach ($schemaFiles as $schemaFile) {
if (!in_array($schemaFile, array('.', '..'))) {
$scimSchemaJsonDecoded = json_decode(file_get_contents($pathToSchemasDir . '/' . $schemaFile), true);
// Only return schemas that are either mandatory (like the 'Schema' and 'ResourceType' ones)
// or supported by the server
if (in_array($scimSchemaJsonDecoded['name'], array_merge($supportedSchemas, $mandatorySchemas))) {
$scimSchemas[] = $scimSchemaJsonDecoded;
}
}
}
$scimSchemasCollection = (new CoreCollection($scimSchemas))->toSCIM(false);
$responseBody = json_encode($scimSchemasCollection, JSON_UNESCAPED_SLASHES);
$this->logger->info($responseBody);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Opf\Controllers\ServiceProviders;
use Opf\Controllers\Controller;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Psr7\Response;
final class ListServiceProviderConfigurationsAction extends Controller
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$this->logger->info("GET ServiceProviderConfigurations");
$pathToServiceProviderConfigurationFile =
dirname(__DIR__, 3) . '/config/ServiceProviderConfig/serviceProviderConfig.json';
$scimServiceProviderConfigurationFile = file_get_contents($pathToServiceProviderConfigurationFile);
if ($scimServiceProviderConfigurationFile === false) {
$this->logger->info("No ServiceProviderConfiguration found");
$response = new Response($status = 404);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
$responseBody = $scimServiceProviderConfigurationFile;
$this->logger->info($responseBody);
$response = new Response($status = 200);
$response->getBody()->write($responseBody);
$response = $response->withHeader('Content-Type', 'application/scim+json');
return $response;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,60 @@
<?php
namespace Opf\DataAccess\Groups;
use Illuminate\Database\Eloquent\Model;
use Opf\Util\Util;
class MockGroupDataAccess extends Model
{
protected $table = 'groups';
protected $fillable = ['id', 'displayName', 'members', 'created_at'];
public $incrementing = false;
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0:Group"];
private $baseLocation;
public function fromArray($data)
{
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid());
$this->displayName = $data['displayName'];
$this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']);
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW');
}
public function fromSCIM($data)
{
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid());
$this->displayName = $data['displayName'];
$this->members = is_string($data['members']) ? $data['members'] : implode(",", $data['members']);
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created']) : new \DateTime('NOW');
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'schemas' => $this->schemas,
'id' => $this->id,
'displayName' => $this->displayName,
'members' => [],
'meta' => [
'created' => Util::dateTime2string($this->created_at),
'location' => $baseLocation . '/Groups/' . $this->id
]
];
if (!empty($this->members)) {
$data['members'] = explode(',', $this->members);
}
if (isset($this->updated_at)) {
$data['meta']['updated'] = Util::dateTime2string($this->updated_at);
}
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Opf\DataAccess\Users;
use Illuminate\Database\Eloquent\Model;
use Opf\Util\Util;
class MockUserDataAccess extends Model
{
protected $table = 'users';
protected $fillable = ['id', 'userName', 'created_at', 'active',
'externalId', 'profileUrl'];
public $incrementing = false;
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0:User"];
private $baseLocation;
public function fromArray($data)
{
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid());
$this->userName = isset($data['userName']) ? $data['userName'] : null;
$this->created_at = isset($data['created']) ? Util::string2dateTime($data['created'])
: (isset($this->created_at) ? $this->created_at : new \DateTime('NOW'));
$this->active = isset($data['active']) ? $data['active'] : true;
$this->externalId = isset($data['externalId']) ? $data['externalId'] : null;
$this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null;
}
public function fromSCIM($data)
{
$this->id = isset($data['id']) ? $data['id'] : (isset($this->id) ? $this->id : Util::genUuid());
$this->userName = isset($data['userName']) ? $data['userName'] : null;
$this->created_at = isset($data['meta']) && isset($data['meta']['created'])
? Util::string2dateTime($data['meta']['created'])
: (isset($this->created_at) ? $this->created_at : new \DateTime('NOW'));
$this->active = isset($data['active']) ? $data['active'] : true;
$this->externalId = isset($data['externalId']) ? $data['externalId'] : null;
$this->profileUrl = isset($data['profileUrl']) ? $data['profileUrl'] : null;
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'schemas' => $this->schemas,
'id' => $this->id,
'externalId' => $this->externalId,
'meta' => [
'created' => Util::dateTime2string($this->created_at),
'location' => $baseLocation . '/Users/' . $this->id
],
'userName' => $this->userName,
'profileUrl' => $this->profileUrl,
'active' => (bool) $this->active
];
if (isset($this->updated_at)) {
$data['meta']['updated'] = Util::dateTime2string($this->updated_at);
}
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

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

View file

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

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
use DI\ContainerBuilder;
use Opf\Adapters\Groups\MockGroupAdapter;
use Opf\Adapters\Users\MockUserAdapter;
use Opf\Controllers\Controller;
use Opf\DataAccess\Groups\MockGroupDataAccess;
use Opf\DataAccess\Users\MockUserDataAccess;
use Opf\Repositories\Groups\MockGroupsRepository;
use Opf\Repositories\Users\MockUsersRepository;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Tuupola\Middleware\JwtAuthentication;
return function (ContainerBuilder $containerBuilder) {
$containerBuilder->addDefinitions([
// Repositories
'UsersRepository' => function (ContainerInterface $c) {
return new MockUsersRepository($c);
},
'GroupsRepository' => function (ContainerInterface $c) {
return new MockGroupsRepository($c);
},
// Data access classes
'UsersDataAccess' => function () {
return new MockUserDataAccess();
},
'GroupsDataAccess' => function () {
return new MockGroupDataAccess();
},
// Adapters
'UsersAdapter' => function () {
return new MockUserAdapter();
},
'GroupsAdapter' => function () {
return new MockGroupAdapter();
}
]);
};

View file

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

View file

@ -0,0 +1,76 @@
<?php
namespace Opf\Handlers;
use Psr\Http\Message\ResponseInterface;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpException;
use Slim\Exception\HttpForbiddenException;
use Slim\Exception\HttpMethodNotAllowedException;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpNotImplementedException;
use Slim\Exception\HttpUnauthorizedException;
use Slim\Handlers\ErrorHandler;
use Exception;
use Throwable;
class HttpErrorHandler extends ErrorHandler
{
public const BAD_REQUEST = 'BAD_REQUEST';
public const INSUFFICIENT_PRIVILEGES = 'INSUFFICIENT_PRIVILEGES';
public const NOT_ALLOWED = 'NOT_ALLOWED';
public const NOT_IMPLEMENTED = 'NOT_IMPLEMENTED';
public const RESOURCE_NOT_FOUND = 'RESOURCE_NOT_FOUND';
public const SERVER_ERROR = 'SERVER_ERROR';
public const UNAUTHENTICATED = 'UNAUTHENTICATED';
protected function respond(): ResponseInterface
{
$exception = $this->exception;
$statusCode = 500;
$type = self::SERVER_ERROR;
$description = 'An internal error has occurred while processing your request.';
if ($exception instanceof HttpException) {
$statusCode = $exception->getCode();
$description = $exception->getMessage();
if ($exception instanceof HttpNotFoundException) {
$type = self::RESOURCE_NOT_FOUND;
} elseif ($exception instanceof HttpMethodNotAllowedException) {
$type = self::NOT_ALLOWED;
} elseif ($exception instanceof HttpUnauthorizedException) {
$type = self::UNAUTHENTICATED;
} elseif ($exception instanceof HttpForbiddenException) {
$type = self::UNAUTHENTICATED;
} elseif ($exception instanceof HttpBadRequestException) {
$type = self::BAD_REQUEST;
} elseif ($exception instanceof HttpNotImplementedException) {
$type = self::NOT_IMPLEMENTED;
}
}
if (
!($exception instanceof HttpException)
&& ($exception instanceof Exception || $exception instanceof Throwable)
&& $this->displayErrorDetails
) {
$description = $exception->getMessage();
}
$error = [
'statusCode' => $statusCode,
'error' => [
'type' => $type,
'description' => $description,
],
];
$payload = json_encode($error, JSON_PRETTY_PRINT);
$response = $this->responseFactory->createResponse($statusCode);
$response->getBody()->write($payload);
return $response;
}
}

364
src/Models/PFA/PfaUser.php Normal file
View file

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

View file

@ -0,0 +1,8 @@
<?php
namespace Opf\Models\SCIM\Custom\Domains;
// TODO: This is currently a dummy class to demonstrate how to add custom SCIM resources to the codebase
class Domain
{
}

View file

@ -0,0 +1,54 @@
<?php
namespace Opf\Models\SCIM\Custom\Users;
use Opf\Models\SCIM\Standard\Users\CoreUser;
use Opf\Util\Util;
class ProvisioningUser extends CoreUser
{
/** @var ?int $sizeQuota */
private ?int $sizeQuota = null;
/**
* @return int|null
*/
public function getSizeQuota(): ?int
{
return $this->sizeQuota;
}
/**
* @param int|null $sizeQuota
*/
public function setSizeQuota(?int $sizeQuota): void
{
$this->sizeQuota = $sizeQuota;
}
public function fromSCIM($data)
{
parent::fromSCIM($data);
if (isset($data[Util::PROVISIONING_USER_SCHEMA])) {
$provisioningUserData = $data[Util::PROVISIONING_USER_SCHEMA];
$this->setSizeQuota(isset($provisioningUserData['sizeQuota']) ? $provisioningUserData['sizeQuota'] : null);
}
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = parent::toSCIM($encode, $baseLocation);
$data['schemas'][] = Util::PROVISIONING_USER_SCHEMA;
$data[Util::PROVISIONING_USER_SCHEMA]['sizeQuota'] = $this->getSizeQuota();
if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) {
$data['meta']['updated'] = $this->getMeta()->getLastModified();
}
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Opf\Models\SCIM\Standard;
/**
* This class contains all common attributes of SCIM resources,
* as well as the "Schemas" attribute
*
* @see https://datatracker.ietf.org/doc/html/rfc7643#section-3
*/
class CommonEntity
{
/** @var array<string> $schemas */
private $schemas;
/** @var string $id */
private $id;
/** @var string $externalId */
private $externalId;
/** @var \Opf\Models\SCIM\Meta $meta */
private $meta;
public function getSchemas()
{
return $this->schemas;
}
public function setSchemas($schemas)
{
$this->schemas = $schemas;
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getExternalId()
{
return $this->externalId;
}
public function setExternalId($externalId)
{
$this->externalId = $externalId;
}
public function getMeta()
{
return $this->meta;
}
public function setMeta($meta)
{
$this->meta = $meta;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Opf\Models\SCIM\Standard;
class CoreCollection
{
public $schemas = ["urn:ietf:params:scim:schemas:core:2.0"];
private $scimItems;
public function __construct($scimItems = [])
{
$this->scimItems = $scimItems;
}
public function toSCIM($encode = true)
{
$data = [
'totalResults' => count($this->scimItems),
'startIndex' => 1,
'schemas' => $this->schemas,
'Resources' => $this->scimItems
];
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Opf\Models\SCIM\Standard\Groups;
use Opf\Util\Util;
use Opf\Models\SCIM\Standard\CommonEntity;
use Opf\Models\SCIM\Standard\Meta;
class CoreGroup extends CommonEntity
{
/** @var string $displayName */
private $displayName;
/** @var \Opf\Models\SCIM\MultiValuedAttribute $members */
private $members;
public function getDisplayName()
{
return $this->displayName;
}
public function setDisplayName($displayName)
{
$this->displayName = $displayName;
}
public function getMembers()
{
return $this->members;
}
public function setMembers($members)
{
$this->members = $members;
}
public function fromSCIM($data)
{
if (isset($data['id'])) {
$this->setId($data['id']);
} elseif ($this->getId() !== null) {
$this->setId($this->getId());
} else {
$this->setId(Util::genUuid());
}
$this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null);
$meta = new Meta();
if (isset($data['meta']) && isset($data['meta']['created'])) {
$meta->setCreated(Util::string2dateTime($data['meta']['created']));
} else {
$meta->setCreated(Util::dateTime2string(new \DateTime('NOW')));
}
$this->setMeta($meta);
$this->setMembers(isset($data['members']) ? $data['members'] : true);
$this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null);
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'schemas' => [Util::GROUP_SCHEMA],
'id' => $this->getId(),
'externalId' => $this->getExternalId(),
'meta' => [
'resourceType' => $this->getMeta()->getResourceType(),
'created' => $this->getMeta()->getCreated(),
'location' => $baseLocation . '/Groups/' . $this->getId(),
'version' => $this->getMeta()->getVersion()
],
'displayName' => $this->getDisplayName(),
'members' => $this->getMembers()
];
if (null !== $this->getMeta()->getLastModified()) {
$data['meta']['updated'] = $this->getMeta()->getLastModified();
}
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

@ -0,0 +1,67 @@
<?php
namespace Opf\Models\SCIM\Standard;
/**
* This class represents the common SCIM attribute "meta"
*
* @see https://datatracker.ietf.org/doc/html/rfc7643#section-3.1
*/
class Meta
{
/** @var string $resourceType */
private $resourceType;
/** @var string $created */
private $created;
/** @var string $lastModified */
private $lastModified;
// location is determined when converting to JSON
// /** @var string $location */
// private $location;
/** @var string $version */
private $version;
public function getResourceType()
{
return $this->resourceType;
}
public function setResourceType($resourceType)
{
$this->resourceType = $resourceType;
}
public function getCreated()
{
return $this->created;
}
public function setCreated($created)
{
$this->created = $created;
}
public function getLastModified()
{
return $this->lastModified;
}
public function setLastModified($lastModified)
{
$this->lastModified = $lastModified;
}
public function getVersion()
{
return $this->version;
}
public function setVersion($version)
{
$this->version = $version;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Opf\Models\SCIM\Standard;
use JsonSerializable;
class MultiValuedAttribute implements JsonSerializable
{
/** @var string $type */
private $type;
/** @var bool $primary */
private $primary;
/** @var string $display */
private $display;
/** @var string $value */
private $value;
/** @var string $ref */
private $ref;
public function getType()
{
return $this->type;
}
public function setType($type)
{
$this->type = $type;
}
public function getPrimary()
{
return $this->primary;
}
public function setPrimary($primary)
{
$this->primary = $primary;
}
public function getDisplay()
{
return $this->display;
}
public function setDisplay($display)
{
$this->display = $display;
}
public function getValue()
{
return $this->value;
}
public function setValue($value)
{
$this->value = $value;
}
public function getRef()
{
return $this->ref;
}
public function setRef($ref)
{
$this->ref = $ref;
}
public function jsonSerialize()
{
return (object)[
"type" => $this->getType(),
"primary" => $this->getPrimary(),
"display" => $this->getDisplay(),
"value" => $this->getValue(),
"\$ref" => $this->getRef()
];
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
use JsonSerializable;
/**
* This class represents the SCIM object used for the "attributes" property of the "Schema" resource
*
* @see https://datatracker.ietf.org/doc/html/rfc7643#section-7
*/
class Attribute implements JsonSerializable
{
/** @var string $name */
private $name;
/** @var string $type */
private $type;
/** @var array<Attribute> $subAttributes */
private $subAttributes;
/** @var boolean $multiValued */
private $multiValued;
/** @var string $description */
private $description;
/** @var boolean $required */
private $required;
/** @var array<string> $canonicalValues */
private $canonicalValues;
/** @var boolean $caseExact */
private $caseExact;
/** @var string $mutability */
private $mutability;
/** @var string $returned */
private $returned;
/** @var string $uniqueness */
private $uniqueness;
/** @var array<string> $referenceTypes */
private $referenceTypes;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getType()
{
return $this->type;
}
public function setType($type)
{
$this->type = $type;
}
public function getSubAttributes()
{
return $this->subAttributes;
}
public function setSubAttributes($subAttributes)
{
$this->subAttributes = $subAttributes;
}
public function getMultiValued()
{
return $this->multiValued;
}
public function setMultiValued($multiValued)
{
$this->multiValued = $multiValued;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getRequired()
{
return $this->required;
}
public function setRequired($required)
{
$this->required = $required;
}
public function getCanonicalValues()
{
return $this->canonicalValues;
}
public function setCanonicalValues($canonicalValues)
{
$this->canonicalValues = $canonicalValues;
}
public function getCaseExact()
{
return $this->caseExact;
}
public function setCaseExact($caseExact)
{
$this->caseExact = $caseExact;
}
public function getMutability()
{
return $this->mutability;
}
public function setMutability($mutability)
{
$this->mutability = $mutability;
}
public function getReturned()
{
return $this->returned;
}
public function setReturned($returned)
{
$this->returned = $returned;
}
public function getUniqueness()
{
return $this->uniqueness;
}
public function setUniqueness($uniqueness)
{
$this->uniqueness = $uniqueness;
}
public function getReferenceTypes()
{
return $this->referenceTypes;
}
public function setReferenceTypes($referenceTypes)
{
$this->referenceTypes = $referenceTypes;
}
public function jsonSerialize()
{
return (object)[
"name" => $this->getName(),
"type" => $this->getType(),
"subAttributes" => $this->getSubAttributes(),
"multiValued" => $this->getMultiValued(),
"description" => $this->getDescription(),
"required" => $this->getRequired(),
"canonicalValues" => $this->getCanonicalValues(),
"caseExact" => $this->getCaseExact(),
"mutability" => $this->getMutability(),
"returned" => $this->getReturned(),
"uniqueness" => $this->getUniqueness(),
"referenceTypes" => $this->getReferenceTypes()
];
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
class AuthenticationScheme
{
/** @var string $type */
private $type;
/** @var string $name */
private $name;
/** @var string $description */
private $description;
/** @var string $specUri */
private $specUri;
/** @var string $documentationUri */
private $documentationUri;
public function getType()
{
return $this->type;
}
public function setType($type)
{
$this->type = $type;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getSpecUri()
{
return $this->specUri;
}
public function setSpecUri($specUri)
{
$this->specUri = $specUri;
}
public function getDocumentationUri()
{
return $this->documentationUri;
}
public function setDocumentationUri($documentationUri)
{
$this->documentationUri = $documentationUri;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
class Bulk extends SupportableConfigProperty
{
/** @var int $maxOperations */
private $maxOperations;
/** @var int $maxPayloadSize */
private $maxPayloadSize;
public function getMaxOperations()
{
return $this->maxOperations;
}
public function setMaxOperations($maxOperations)
{
$this->maxOperations = $maxOperations;
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
use Opf\Util\Util;
// TODO: Refactor this model to be not only hardcode-usable like now, but to be configurable
// and usable for various configurable data storages
class CoreResourceType
{
/** @var array<string> */
private $schemas = [Util::RESOURCE_TYPE_SCHEMA];
/** @var string */
private $id;
/** @var string */
private $name;
/** @var string */
private $description;
/** @var string */
private $endpoint;
/** @var string */
private $schema;
/** @var array<SchemaExtension> */
private $schemaExtensions;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getEndpoint()
{
return $this->endpoint;
}
public function setEndpoint($endpoint)
{
$this->endpoint = $endpoint;
}
public function getSchema()
{
return $this->schema;
}
public function setSchema($schema)
{
$this->schema = $schema;
}
public function getSchemaExtensions()
{
return $this->schemaExtensions;
}
public function setSchemaExtensions($schemaExtensions)
{
$this->schemaExtensions = $schemaExtensions;
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
if (isset($this->schemaExtensions) && !empty($this->schemaExtensions)) {
$transformedSchemaExtensions = [];
foreach ($this->schemaExtensions as $schemaExtension) {
$transformedSchemaExtensions[] = $schemaExtension->toSCIM();
}
$this->setSchemaExtensions($transformedSchemaExtensions);
}
$data = [
'schemas' => $this->schemas,
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'endpoint' => $this->endpoint,
'schema' => $this->schema,
'schemaExtensions' => $this->schemaExtensions,
'meta' => [
'location' => $baseLocation . '/' . $this->id,
'resourceType' => 'ResourceType'
]
];
if ($encode) {
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
}
return $data;
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
/**
* This class represents the SCIM "Schema" resource object
*
* @see https://datatracker.ietf.org/doc/html/rfc7643#section-7
*/
class CoreSchema
{
/** @var string $id */
private $id;
/** @var string $name */
private $name;
/** @var string $description */
private $description;
/** @var array<\Opf\Models\SCIM\Attribute> $attributes */
private $attributes;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDescription()
{
return $this->description;
}
public function setDescription($description)
{
$this->description = $description;
}
public function getAttributes()
{
return $this->attributes;
}
public function setAttributes($attributes)
{
$this->attributes = $attributes;
}
public function toSCIM($encode = true)
{
$data = [
"id" => $this->id,
"name" => $this->name,
"description" => $this->description,
"attributes" => $this->attributes
];
if ($encode) {
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
}
return $data;
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
class CoreSchemaExtension
{
/** @var string */
private $schema;
/** @var boolean */
private $required;
public function getSchema()
{
return $this->schema;
}
public function setSchema($schema)
{
$this->schema = $schema;
}
public function getRequired()
{
return $this->required;
}
public function setRequired($required)
{
$this->required = $required;
}
public function toSCIM($encode = false)
{
$data = [
'schema' => $this->schema,
'required' => $this->required
];
if ($encode) {
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
}
return $data;
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
use Opf\Util\Util;
class CoreServiceProviderConfiguration
{
/** @var array<string> $schemas */
private $schemas = [Util::SERVICE_PROVIDER_CONFIGURATION_SCHEMA];
/** @var string $documentationUri */
private $documentationUri;
/** @var \Opf\Models\SCIM\SupportableConfigProperty $patch */
private $patch;
/** @var \Opf\Models\SCIM\Bulk $bulk */
private $bulk;
/** @var \Opf\Models\SCIM\Filter $filter */
private $filter;
/** @var \Opf\Models\SCIM\SupportableConfigProperty $changePassword */
private $changePassword;
/** @var \Opf\Models\SCIM\SupportableConfigProperty $sort */
private $sort;
/** @var \Opf\Models\SCIM\SupportableConfigProperty $etag */
private $etag;
/** @var array<\Opf\Models\SCIM\AuthenticationScheme> $authenticationSchemes */
private $authenticationSchemes;
public function getSchemas()
{
return $this->schemas;
}
public function setSchemas($schemas)
{
$this->schemas = $schemas;
}
public function getDocumentationUri()
{
return $this->documentationUri;
}
public function setDocumentationUri($documentationUri)
{
$this->documentationUri = $documentationUri;
}
public function getPatch()
{
return $this->patch;
}
public function setPatch($patch)
{
$this->patch = $patch;
}
public function getBulk()
{
return $this->bulk;
}
public function setBulk($bulk)
{
$this->bulk = $bulk;
}
public function getFilter()
{
return $this->filter;
}
public function setFilter($filter)
{
$this->filter = $filter;
}
public function getChangePassword()
{
return $this->changePassword;
}
public function setChangePassword($changePassword)
{
$this->changePassword = $changePassword;
}
public function getSort()
{
return $this->sort;
}
public function setSort($sort)
{
$this->sort = $sort;
}
public function getEtag()
{
return $this->etag;
}
public function setEtag($etag)
{
$this->etag = $etag;
}
public function getAuthenticationSchemes()
{
return $this->authenticationSchemes;
}
public function setAuthenticationSchemes($authenticationSchemes)
{
$this->authenticationSchemes = $authenticationSchemes;
}
public function toSCIM($encode = true)
{
$data = [
"schemas" => $this->schemas,
"documentationUri" => $this->documentationUri,
"patch" => $this->patch,
"bulk" => $this->bulk,
"filter" => $this->filter,
"changePassword" => $this->changePassword,
"sort" => $this->sort,
"etag" => $this->etag,
"authenticationSchemes" => $this->authenticationSchemes
];
if ($encode) {
$data = json_encode($data, JSON_UNESCAPED_SLASHES);
}
return $data;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
class Filter extends SupportableConfigProperty
{
/** @var int $maxResults */
private $maxResults;
public function getMaxResults()
{
return $this->maxResults;
}
public function setMaxResults($maxResults)
{
$this->maxResults = $maxResults;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Opf\Models\SCIM\Standard\Service;
class SupportableConfigProperty
{
/** @var boolean $supported */
private $supported;
public function getSupported()
{
return $this->supported;
}
public function setSupported($supported)
{
$this->supported = $supported;
}
}

View file

@ -0,0 +1,103 @@
<?php
namespace Opf\Models\SCIM\Standard\Users;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
class Address extends MultiValuedAttribute
{
/** @var string $formatted */
private $formatted;
/** @var string $streetAddress */
private $streetAddress;
/** @var string $locality */
private $locality;
/** @var string $region */
private $region;
/** @var string $postalCode */
private $postalCode;
/** @var string $country */
private $country;
public function getFormatted()
{
return $this->formatted;
}
public function setFormatted($formatted)
{
$this->formatted = $formatted;
}
public function getStreetAddress()
{
return $this->streetAddress;
}
public function setStreetAddress($streetAddress)
{
$this->streetAddress = $streetAddress;
}
public function getLocality()
{
return $this->locality;
}
public function setLocality($locality)
{
$this->locality = $locality;
}
public function getRegion()
{
return $this->region;
}
public function setRegion($region)
{
$this->region = $region;
}
public function getPostalCode()
{
return $this->postalCode;
}
public function setPostalCode($postalCode)
{
$this->postalCode = $postalCode;
}
public function getCountry()
{
return $this->country;
}
public function setCountry($country)
{
$this->country = $country;
}
public function jsonSerialize(): mixed
{
return (object)[
"type" => $this->getType(),
"primary" => $this->getPrimary(),
"display" => $this->getDisplay(),
"value" => $this->getValue(),
"\$ref" => $this->getRef(),
"formatted" => $this->getFormatted(),
"streetAddress" => $this->getStreetAddress(),
"locality" => $this->getLocality(),
"region" => $this->getRegion(),
"postalCode" => $this->getPostalCode(),
"country" => $this->getCountry()
];
}
}

View file

@ -0,0 +1,403 @@
<?php
namespace Opf\Models\SCIM\Standard\Users;
use Opf\Util\Util;
use Opf\Models\SCIM\Standard\CommonEntity;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Models\SCIM\Standard\MultiValuedAttribute;
// TODO: Also implement support for enterprise user schema
class CoreUser extends CommonEntity
{
/** @var string $userName */
private $userName;
/** @var \Opf\Models\SCIM\Name $name */
private $name;
/** @var string $displayName */
private $displayName;
/** @var string $nickName */
private $nickName;
/** @var string $profileUrl */
private $profileUrl;
/** @var string $title */
private $title;
/** @var string $userType */
private $userType;
/** @var string $preferredLanguage */
private $preferredLanguage;
/** @var string $locale */
private $locale;
/** @var string $timezone */
private $timezone;
/** @var bool $active */
private $active;
/** @var string $password */
private $password;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $emails */
private $emails;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $phoneNumbers */
private $phoneNumbers;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $ims */
private $ims;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $photos */
private $photos;
/** @var \Opf\Models\SCIM\Address[] $addresses */
private $addresses;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $groups */
private $groups;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $entitlements */
private $entitlements;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $roles */
private $roles;
/** @var \Opf\Models\SCIM\MultiValuedAttribute[] $x509Certificates */
private $x509Certificates;
public function getUserName()
{
return $this->userName;
}
public function setUserName($userName)
{
$this->userName = $userName;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getDisplayName()
{
return $this->displayName;
}
public function setDisplayName($displayName)
{
$this->displayName = $displayName;
}
public function getNickName()
{
return $this->nickName;
}
public function setNickName($nickName)
{
$this->nickName = $nickName;
}
public function getProfileUrl()
{
return $this->profileUrl;
}
public function setProfileUrl($profileUrl)
{
$this->profileUrl = $profileUrl;
}
public function getTitle()
{
return $this->title;
}
public function setTitle($title)
{
$this->title = $title;
}
public function getUserType()
{
return $this->userType;
}
public function setUserType($userType)
{
$this->userType = $userType;
}
public function getPreferredLanguage()
{
return $this->preferredLanguage;
}
public function setPreferredLanguage($preferredLanguage)
{
$this->preferredLanguage = $preferredLanguage;
}
public function getLocale()
{
return $this->locale;
}
public function setLocale($locale)
{
$this->locale = $locale;
}
public function getTimezone()
{
return $this->timezone;
}
public function setTimezone($timezone)
{
$this->timezone = $timezone;
}
public function getActive()
{
return $this->active;
}
public function setActive($active)
{
$this->active = $active;
}
public function getPassword()
{
return $this->password;
}
public function setPassword($password)
{
$this->password = $password;
}
public function getEmails()
{
return $this->emails;
}
public function setEmails($emails)
{
$this->emails = $emails;
}
public function getPhoneNumbers()
{
return $this->phoneNumbers;
}
public function setPhoneNumbers($phoneNumbers)
{
$this->phoneNumbers = $phoneNumbers;
}
public function getIms()
{
return $this->ims;
}
public function setIms($ims)
{
$this->ims = $ims;
}
public function getPhotos()
{
return $this->photos;
}
public function setPhotos($photos)
{
$this->photos = $photos;
}
public function getAddresses()
{
return $this->addresses;
}
public function setAddresses($addresses)
{
$this->addresses = $addresses;
}
public function getGroups()
{
return $this->groups;
}
public function setGroups($groups)
{
$this->groups = $groups;
}
public function getEntitlements()
{
return $this->entitlements;
}
public function setEntitlements($entitlements)
{
$this->entitlements = $entitlements;
}
public function getRoles()
{
return $this->roles;
}
public function setRoles($roles)
{
$this->roles = $roles;
}
public function getX509Certificates()
{
return $this->x509Certificates;
}
public function setX509Certificates($x509Certificates)
{
$this->x509Certificates = $x509Certificates;
}
// TODO: Finish the implementation of this method for all properties
// TODO: Maybe rename this method to a more meaningful one,
// since it's currently just translating a SCIM JSON object to a PHP SCIM object
public function fromSCIM($data)
{
if (isset($data['id'])) {
$this->setId($data['id']);
} elseif ($this->getId() !== null) {
$this->setId($this->getId());
} else {
$this->setId(Util::genUuid());
}
$this->setUserName(isset($data['userName']) ? $data['userName'] : null);
$name = new Name();
$name->setFamilyName($data['name']['familyName']);
$name->setFormatted($data['name']['formatted']);
$name->setGivenName($data['name']['givenName']);
$name->setHonorificPrefix($data['name']['honorificPrefix']);
$name->setHonorificSuffix($data['name']['honorificSuffix']);
$this->setName($name);
$meta = new Meta();
if (isset($data['meta']) && isset($data['meta']['created'])) {
$meta->setCreated(Util::string2dateTime($data['meta']['created']));
} else {
$meta->setCreated(Util::dateTime2string(new \DateTime('NOW')));
}
$this->setMeta($meta);
$this->setActive(isset($data['active']) ? $data['active'] : true);
$emails = [];
if (isset($data['emails']) && !empty($data['emails'])) {
foreach ($data['emails'] as $email) {
$scimEmail = new MultiValuedAttribute();
$scimEmail->setType(isset($email['type']) && !empty($email['type']) ? $email['type'] : null);
$scimEmail->setPrimary(
isset($email['primary']) && !empty($email['primary']) ? $email['primary'] : null
);
$scimEmail->setDisplay(
isset($email['display']) && !empty($email['display']) ? $email['display'] : null
);
$scimEmail->setValue(isset($email['value']) && !empty($email['value']) ? $email['value'] : null);
$emails[] = $scimEmail;
}
}
$this->setEmails($emails);
$this->setDisplayName(isset($data['displayName']) ? $data['displayName'] : null);
$this->setPassword(isset($data['password']) ? strval($data['password']) : null);
$this->setExternalId(isset($data['externalId']) ? $data['externalId'] : null);
$this->setProfileUrl(isset($data['profileUrl']) ? $data['profileUrl'] : null);
}
// TODO: Maybe rename this method to a more meaningful one,
// since it's currently just translating a SCIM PHP object to a JSON SCIM object
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'schemas' => [Util::USER_SCHEMA],
'id' => $this->getId(),
'externalId' => $this->getExternalId(),
'meta' => null !== $this->getMeta() ? [
'resourceType' => null !== $this->getMeta()->getResourceType()
? $this->getMeta()->getResourceType() : null,
'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null,
'location' => $baseLocation . '/Users/' . $this->getId(),
'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null
] : null,
'userName' => $this->getUserName(),
'name' => null !== $this->getName() ? [
'formatted' => null !== $this->getName()->getFormatted() ? $this->getName()->getFormatted() : null,
'familyName' => null !== $this->getName()->getFamilyName() ? $this->getName()->getFamilyName() : null,
'givenName' => null !== $this->getName()->getGivenName() ? $this->getName()->getGivenName() : null,
'middleName' => null !== $this->getName()->getMiddleName() ? $this->getName()->getMiddleName() : null,
'honorificPrefix' => null !== $this->getName()->getHonorificPrefix()
? $this->getName()->getHonorificPrefix()
: null,
'honorificSuffix' => null !== $this->getName()->getHonorificSuffix()
? $this->getName()->getHonorificSuffix()
: null
] : null,
'displayName' => $this->getDisplayName(),
'nickName' => $this->getNickName(),
'profileUrl' => $this->getProfileUrl(),
'title' => $this->getTitle(),
'userType' => $this->getUserType(),
'preferredLanguage' => $this->getPreferredLanguage(),
'locale' => $this->getLocale(),
'timezone' => $this->getTimezone(),
'active' => $this->getActive(),
'password' => $this->getPassword(),
'emails' => $this->getEmails(),
'phoneNumbers' => $this->getPhoneNumbers(),
'ims' => $this->getIms(),
'photos' => $this->getPhotos(),
'addresses' => $this->getAddresses(),
'groups' => $this->getGroups(),
'entitlements' => $this->getEntitlements(),
'roles' => $this->getRoles(),
'x509Certificates' => $this->getX509Certificates()
];
if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) {
$data['meta']['updated'] = $this->getMeta()->getLastModified();
}
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

@ -0,0 +1,156 @@
<?php
namespace Opf\Models\SCIM\Standard\Users;
use Opf\Util\Util;
class EnterpriseUser extends CoreUser
{
/** @var string $employeeNumber */
private $employeeNumber;
/** @var string $costCenter */
private $costCenter;
/** @var string $organization */
private $organization;
/** @var string $division */
private $division;
/** @var string $department */
private $department;
/** @var \Opf\Models\SCIM\Manager $manager */
private $manager;
public function getEmployeeNumber()
{
return $this->employeeNumber;
}
public function setEmployeeNumber($employeeNumber)
{
$this->employeeNumber = $employeeNumber;
}
public function getCostCenter()
{
return $this->costCenter;
}
public function setCostCenter($costCenter)
{
$this->costCenter = $costCenter;
}
public function getOrganization()
{
return $this->organization;
}
public function setOrganization($organization)
{
$this->organization = $organization;
}
public function getDivision()
{
return $this->division;
}
public function setDivision($division)
{
$this->division = $division;
}
public function getDepartment()
{
return $this->department;
}
public function setDepartment($department)
{
$this->department = $department;
}
public function getManager()
{
return $this->manager;
}
public function setManager($manager)
{
$this->manager = $manager;
}
public function toSCIM($encode = true, $baseLocation = 'http://localhost:8888/v1')
{
$data = [
'schemas' => [Util::ENTERPRISE_USER_SCHEMA],
'id' => $this->getId(),
'externalId' => $this->getExternalId(),
'meta' => null !== $this->getMeta() ? [
'resourceType' => null !== $this->getMeta()->getResourceType()
? $this->getMeta()->getResourceType() : null,
'created' => null !== $this->getMeta()->getCreated() ? $this->getMeta()->getCreated() : null,
'location' => $baseLocation . '/Users/' . $this->getId(),
'version' => null !== $this->getMeta()->getVersion() ? $this->getMeta()->getVersion() : null
] : null,
'userName' => $this->getUserName(),
'name' => null !== $this->getName() ? [
'formatted' => null !== $this->getName()->getFormatted() ? $this->getName()->getFormatted() : null,
'familyName' => null !== $this->getName()->getFamilyName() ? $this->getName()->getFamilyName() : null,
'givenName' => null !== $this->getName()->getGivenName() ? $this->getName()->getGivenName() : null,
'middleName' => null !== $this->getName()->getMiddleName() ? $this->getName()->getMiddleName() : null,
'honorificPrefix' => null !== $this->getName()->getHonorificPrefix()
? $this->getName()->getHonorificPrefix()
: null,
'honorificSuffix' => null !== $this->getName()->getHonorificSuffix()
? $this->getName()->getHonorificSuffix()
: null
] : null,
'displayName' => $this->getDisplayName(),
'nickName' => $this->getNickName(),
'profileUrl' => $this->getProfileUrl(),
'title' => $this->getTitle(),
'userType' => $this->getUserType(),
'preferredLanguage' => $this->getPreferredLanguage(),
'locale' => $this->getLocale(),
'timezone' => $this->getTimezone(),
'active' => $this->getActive(),
'password' => $this->getPassword(),
'emails' => $this->getEmails(),
'phoneNumbers' => $this->getPhoneNumbers(),
'ims' => $this->getIms(),
'photos' => $this->getPhotos(),
'addresses' => $this->getAddresses(),
'groups' => $this->getGroups(),
'entitlements' => $this->getEntitlements(),
'roles' => $this->getRoles(),
'x509Certificates' => $this->getX509Certificates(),
'employeeNumber' => $this->getEmployeeNumber(),
'costCenter' => $this->getCostCenter(),
'organization' => $this->getOrganization(),
'division' => $this->getDivision(),
'department' => $this->getDepartment(),
'manager' => null !== $this->getManager() ? [
'value' => null !== $this->getManager()->getValue() ? $this->getManager()->getValue() : null,
'\$ref' => null !== $this->getManager()->getRef() ? $this->getManager()->getRef() : null,
'displayName' => null !== $this->getManager()->getDisplayName()
? $this->getManager()->getDisplayName()
: null
] : null
];
if (null !== $this->getMeta() && null !== $this->getMeta()->getLastModified()) {
$data['meta']['updated'] = $this->getMeta()->getLastModified();
}
if ($encode) {
$data = json_encode($data);
}
return $data;
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace opf\Models\SCIM\Standard\Users;
class Manager
{
/** @var string $value */
private $value;
/** @var string $ref */
private $ref;
/** @var string $displayName */
private $displayName;
public function getValue()
{
return $this->value;
}
public function setValue($value)
{
$this->value = $value;
}
public function getRef()
{
return $this->ref;
}
public function setRef($ref)
{
$this->ref = $ref;
}
public function getDisplayName()
{
return $this->displayName;
}
public function setDisplayName($displayName)
{
$this->displayName = $displayName;
}
}

View file

@ -0,0 +1,84 @@
<?php
namespace Opf\Models\SCIM\Standard\Users;
class Name
{
/** @var string $formatted */
private $formatted;
/** @var string $familyName */
private $familyName;
/** @var string $givenName */
private $givenName;
/** @var string $middleName */
private $middleName;
/** @var string $honorificPrefix */
private $honorificPrefix;
/** @var string $honorificSuffix */
private $honorificSuffix;
public function getFormatted()
{
return $this->formatted;
}
public function setFormatted($formatted)
{
$this->formatted = $formatted;
}
public function getFamilyName()
{
return $this->familyName;
}
public function setFamilyName($familyName)
{
$this->familyName = $familyName;
}
public function getGivenName()
{
return $this->givenName;
}
public function setGivenName($givenName)
{
$this->givenName = $givenName;
}
public function getMiddleName()
{
return $this->middleName;
}
public function setMiddleName($middleName)
{
$this->middleName = $middleName;
}
public function getHonorificPrefix()
{
return $this->honorificPrefix;
}
public function setHonorificPrefix($honorificPrefix)
{
$this->honorificPrefix = $honorificPrefix;
}
public function getHonorificSuffix()
{
return $this->honorificSuffix;
}
public function setHonorificSuffix($honorificSuffix)
{
$this->honorificSuffix = $honorificSuffix;
}
}

View file

@ -0,0 +1,137 @@
<?php
namespace Opf\Repositories\Groups;
use Opf\Models\SCIM\Standard\Groups\CoreGroup;
use Opf\Models\SCIM\Standard\Meta;
use Opf\Repositories\Repository;
use Psr\Container\ContainerInterface;
class MockGroupsRepository extends Repository
{
public function __construct(ContainerInterface $container)
{
parent::__construct($container);
$this->dataAccess = $this->container->get('GroupsDataAccess');
$this->adapter = $this->container->get('GroupsAdapter');
}
public function getAll(): array
{
// Read all mock groups from the database
$mockGroups = $this->dataAccess::all();
$scimGroups = [];
// Transform each mock group to a SCIM group via the injected adapter
foreach ($mockGroups as $mockGroup) {
$this->adapter->setGroup($mockGroup);
$scimGroup = new CoreGroup();
$scimGroup->setId($this->adapter->getId());
$scimGroup->setDisplayName($this->adapter->getDisplayName());
$scimGroup->setMembers($this->adapter->getMembers());
$scimGroupMeta = new Meta();
$scimGroupMeta->setCreated($this->adapter->getCreatedAt());
$scimGroup->setMeta($scimGroupMeta);
$scimGroups[] = $scimGroup;
}
return $scimGroups;
}
public function getOneById(string $id): ?CoreGroup
{
if (isset($id) && !empty($id)) {
$mockGroup = $this->dataAccess::find($id);
if (isset($mockGroup) && !empty($mockGroup)) {
$this->adapter->setGroup($mockGroup);
$scimGroup = new CoreGroup();
$scimGroup->setId($this->adapter->getId());
$scimGroup->setDisplayName($this->adapter->getDisplayName());
$scimGroup->setMembers($this->adapter->getMembers());
$scimGroupMeta = new Meta();
$scimGroupMeta->setCreated($this->adapter->getCreatedAt());
$scimGroup->setMeta($scimGroupMeta);
return $scimGroup;
} else {
return null;
}
} else {
return null;
}
}
public function create($object): ?CoreGroup
{
if (isset($object) && !empty($object)) {
$scimGroup = new CoreGroup();
$scimGroup->fromSCIM($object);
$this->adapter->setGroup($this->dataAccess);
$this->adapter->setId($scimGroup->getId());
$this->adapter->setDisplayName($scimGroup->getDisplayName());
$this->adapter->setMembers($scimGroup->getMembers());
$this->adapter->setCreatedAt($scimGroup->getMeta()->getCreated());
$this->dataAccess = $this->adapter->getGroup();
if ($this->dataAccess->save()) {
return $scimGroup;
} else {
return null;
}
}
}
public function update(string $id, $object): ?CoreGroup
{
if (isset($id) && !empty($id)) {
$mockGroup = $this->dataAccess::find($id);
if (isset($mockGroup) && !empty($mockGroup)) {
$scimGroup = new CoreGroup();
$scimGroup->fromSCIM($object);
$this->adapter->setGroup($mockGroup);
$scimGroup->setId($this->adapter->getId());
$this->adapter->setDisplayName($scimGroup->getDisplayName());
$this->adapter->setMembers($scimGroup->getMembers());
$this->adapter->setCreatedAt($scimGroup->getMeta()->getCreated());
$mockGroup = $this->adapter->getGroup();
if ($mockGroup->save()) {
return $scimGroup;
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
}
public function delete(string $id): bool
{
if (isset($id) && !empty($id)) {
$mockGroup = $this->dataAccess::find($id);
if (!isset($mockGroup) || empty($mockGroup)) {
return false;
}
$mockGroup->delete();
return true;
} else {
return false;
}
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Opf\Repositories;
use Psr\Container\ContainerInterface;
abstract class Repository
{
protected $container;
protected $dataAccess;
protected $adapter;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
abstract public function getAll(): array;
abstract public function getOneById(string $id): ?object;
abstract public function create($object): ?object;
abstract public function update(string $id, $object): ?object;
abstract public function delete(string $id): bool;
}

View file

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

View file

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

163
src/Util/Util.php Normal file
View file

@ -0,0 +1,163 @@
<?php
namespace Opf\Util;
abstract class Util
{
public const USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
public const ENTERPRISE_USER_SCHEMA = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User";
public const PROVISIONING_USER_SCHEMA = "urn:audriga:params:scim:schemas:extension:provisioning:2.0:User";
public const GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
public const RESOURCE_TYPE_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:ResourceType";
public const SERVICE_PROVIDER_CONFIGURATION_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig";
// Note: The name below probably doesn't make much sense,
// but I went for it for consistency's sake as with the other names above
public const SCHEMA_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Schema";
/**
* @param \DateTime $dateTime
*
* @return string
*/
public static function dateTime2string(\DateTime $dateTime = null)
{
if (!isset($dateTime)) {
$dateTime = new \DateTime("NOW");
}
if ($dateTime->getTimezone()->getName() === \DateTimeZone::UTC) {
return $dateTime->format('Y-m-d\Th:i:s\Z');
} else {
return $dateTime->format('Y-m-d\TH:i:sP');
}
}
/**
* @param string $string
* @param \DateTimeZone $zone
*
* @return \DateTime
*/
public static function string2dateTime($string, \DateTimeZone $zone = null)
{
if (!$zone) {
$zone = new \DateTimeZone('UTC');
}
$dt = new \DateTime('now', $zone);
$dt->setTimestamp(self::string2timestamp($string));
return $dt;
}
/**
* @param $string
*
* @return int
*/
public static function string2timestamp($string)
{
$matches = array();
if (
!preg_match(
'/^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)T(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?Z$/D',
$string,
$matches
)
) {
throw new \InvalidArgumentException('Invalid timestamp: ' . $string);
}
$year = intval($matches[1]);
$month = intval($matches[2]);
$day = intval($matches[3]);
$hour = intval($matches[4]);
$minute = intval($matches[5]);
$second = intval($matches[6]);
// Use gmmktime because the timestamp will always be given in UTC?
$ts = gmmktime($hour, $minute, $second, $month, $day, $year);
return $ts;
}
public static function getUserNameFromFilter($filter)
{
$username = null;
if (preg_match('/userName eq \"([a-z0-9\_\.\-\@]*)\"/i', $filter, $matches) === 1) {
$username = $matches[1];
}
return $username;
}
public static function genUuid(): string
{
$uuid4 = \Ramsey\Uuid\Uuid::uuid4();
return $uuid4->toString();
}
public static function buildDbDsn(): ?string
{
$config = self::getConfigFile();
if (isset($config) && !empty($config)) {
if (isset($config['db']) && !empty($config['db'])) {
if (
isset($config['db']['driver']) && !empty($config['db']['driver'])
&& isset($config['db']['host']) && !empty($config['db']['host'])
&& isset($config['db']['port']) && !empty($config['db']['port'])
&& isset($config['db']['database']) && !empty($config['db']['database'])
) {
return $config['db']['driver'] . ':host='
. $config['db']['host'] . ';port='
. $config['db']['port'] . ';dbname='
. $config['db']['database'];
}
}
}
// In case we can't build a DSN, just return null
// Note: make sure to check for null equality in the caller
return null;
}
public static function getDomainFromEmail($email)
{
$parts = explode("@", $email);
if (count($parts) != 2) {
return null;
}
return $parts[1];
}
public static function getLocalPartFromEmail($email)
{
$parts = explode("@", $email);
if (count($parts) != 2) {
return null;
}
return $parts[0];
}
/**
* This function can (and should) be used for obtaining the config file of the scim-server-php
* It tries to fetch the custom-defined config file and return its contents
* If no custom config file exists, it resorts to the config.default.php file as a fallback
*
* Either way, it returns the config file's contents in the form of an associative array
*/
public static function getConfigFile()
{
$defaultConfigFilePath = dirname(__DIR__) . '/../config/config.default.php';
$customConfigFilePath = dirname(__DIR__) . '/../config/config.php';
$config = [];
// In case we don't have a custom config, we just rely on the default one
if (!file_exists($customConfigFilePath)) {
$config = require($defaultConfigFilePath);
} else {
$config = require($customConfigFilePath);
}
return $config;
}
}

15
src/eloquent.php Normal file
View file

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

54
src/routes.php Normal file
View file

@ -0,0 +1,54 @@
<?php
use Opf\Controllers\Groups\CreateGroupAction;
use Opf\Controllers\Groups\DeleteGroupAction;
use Opf\Controllers\Groups\GetGroupAction;
use Opf\Controllers\Groups\ListGroupsAction;
use Opf\Controllers\Groups\UpdateGroupAction;
use Opf\Controllers\JWT\GenerateJWTAction;
use Opf\Controllers\ServiceProviders\ListResourceTypesAction;
use Opf\Controllers\ServiceProviders\ListSchemasAction;
use Opf\Controllers\ServiceProviders\ListServiceProviderConfigurationsAction;
use Opf\Controllers\Users\CreateUserAction;
use Opf\Controllers\Users\DeleteUserAction;
use Opf\Controllers\Users\GetUserAction;
use Opf\Controllers\Users\ListUsersAction;
use Opf\Controllers\Users\UpdateUserAction;
use Opf\Util\Util;
use Slim\App;
return function (App $app) {
// We need this information so that we can only expose endpoints
// for resource types that the deployment actually supports
$config = Util::getConfigFile();
$supportedResourceTypes = $config['supportedResourceTypes'];
// Users routes
if (in_array('User', $supportedResourceTypes)) {
$app->get('/Users', ListUsersAction::class)->setName('users.list');
$app->get('/Users/{id}', GetUserAction::class)->setName('users.get');
$app->post('/Users', CreateUserAction::class)->setName('users.create');
$app->put('/Users/{id}', UpdateUserAction::class)->setName('users.update');
$app->delete('/Users/{id}', DeleteUserAction::class)->setName('users.delete');
}
// Group routes
if (in_array('Group', $supportedResourceTypes)) {
$app->get('/Groups', ListGroupsAction::class)->setName('groups.list');
$app->get('/Groups/{id}', GetGroupAction::class)->setName('groups.get');
$app->post('/Groups', CreateGroupAction::class)->setName('groups.create');
$app->put('/Groups/{id}', UpdateGroupAction::class)->setName('groups.update');
$app->delete('/Groups/{id}', DeleteGroupAction::class)->setName('groups.delete');
}
// ServiceProvider routes
$app->get('/ResourceTypes', ListResourceTypesAction::class)->setName('resourceTypes.list');
$app->get('/Schemas', ListSchemasAction::class)->setName('schemas.list');
$app->get(
'/ServiceProviderConfig',
ListServiceProviderConfigurationsAction::class
)->setName('serviceProviderConfigs.list');
// JWT
$app->get('/jwt', GenerateJWTAction::class)->setName('jwt.generate');
};

2
test/integration/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
# Custom hosts files
hosts_*

View file

@ -0,0 +1,4 @@
[defaults]
inventory = hosts_local
interpreter_python = auto_silent
stdout_callback = yaml

View file

@ -0,0 +1,2 @@
[scim_php_servers]
container_scim_php users_address=http://<insert-path-here>/Users user=testuser pass=testpass

View file

@ -0,0 +1,5 @@
# Ansible integration tests
---
- import_playbook: users.yml
tags:
users

View file

@ -0,0 +1,51 @@
# Tests /Users SCIM endpoint
---
- hosts: all
ignore_errors: true
gather_facts: false
tasks:
- name: Create test folder
ansible.builtin.file:
dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}"
state: directory
mode: '0755'
delegate_to: 127.0.0.1
- name: Get all users
uri:
url: "{{ users_address }}"
method: GET
return_content: true
validate_certs: false
delegate_to: 127.0.0.1
register: call_response
- name: Store reply under /tmp/
copy:
content: "{{ call_response.content }}"
dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}/\
get_all_users.json"
delegate_to: 127.0.0.1
- hosts: all
ignore_errors: true
gather_facts: false
tasks:
- name: Create a user
uri:
body: '{"userName":"testuser","externalId":"testuserexternal","profileUrl":"https://www.example.com/testuser"}'
body_format: json
url: "{{ users_address }}"
method: POST
status_code: 201 # Expect 201 for a create user response
return_content: true
validate_certs: false
delegate_to: 127.0.0.1
register: call_response
- name: Store reply under /tmp/
copy:
content: "{{ call_response.content }}"
dest: "/tmp/opf_ansible_output/{{ inventory_hostname }}/\
create_user.json"
delegate_to: 127.0.0.1

12
test/phpunit.xml Normal file
View file

@ -0,0 +1,12 @@
<phpunit
bootstrap="../vendor/autoload.php"
colors="true"
cacheResultFile=".phpunit.result.cache"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false">
<testsuites>
<testsuite name="Unit">
<directory>unit</directory>
</testsuite>
</testsuites>
</phpunit>

View file

@ -0,0 +1,15 @@
{
"id": "7880a4e5-d9ce-42f8-8ed0-57f886616527",
"name": "scim-env",
"values": [
{
"key": "url",
"value": "http://localhost:8888",
"type": "default",
"enabled": true
}
],
"_postman_variable_scope": "environment",
"_postman_exported_at": "2022-04-14T14:54:36.929Z",
"_postman_exported_using": "Postman/9.15.2"
}

View file

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

View file

@ -0,0 +1,717 @@
{
"info": {
"_postman_id": "73043646-f766-4adc-96ee-05316cc59bdd",
"name": "SCIM PHP Collection",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Users",
"item": [
{
"name": "Create a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 201\", () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");",
"});",
"",
"pm.test(\"Response body contains a valid non-null user ID (the ID of the user which was created)\", () => {",
" pm.expect(pm.response.json().id).to.not.be.null;",
"});",
"",
"pm.collectionVariables.set(\"testUserId\", pm.response.json().id);"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"userName\": \"createdtestuser\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Users",
"host": [
"{{url}}"
],
"path": [
"Users"
]
}
},
"response": []
},
{
"name": "Read a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
" pm.expect(pm.response.json().userName).to.eql(\"createdtestuser\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Users/{{testUserId}}",
"host": [
"{{url}}"
],
"path": [
"Users",
"{{testUserId}}"
]
}
},
"response": []
},
{
"name": "Read all users",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"createdtestuser\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].userName).to.eql(\"createdtestuser\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Users",
"host": [
"{{url}}"
],
"path": [
"Users"
]
}
},
"response": []
},
{
"name": "Update a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains the user ID of the user we want to read\", () => {",
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testUserId'));",
"});",
"",
"pm.test(\"Response body contains user with userName \\\"updatedtestuser\\\"\", () => {",
" pm.expect(pm.response.json().userName).to.eql(\"updatedtestuser\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "PUT",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"userName\": \"updatedtestuser\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Users/{{testUserId}}",
"host": [
"{{url}}"
],
"path": [
"Users",
"{{testUserId}}"
]
}
},
"response": []
},
{
"name": "Delete a single user",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 204\", () => {",
" pm.response.to.have.status(204);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "DELETE",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"userName\": \"updatedtestuser\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Users/{{testUserId}}",
"host": [
"{{url}}"
],
"path": [
"Users",
"{{testUserId}}"
]
}
},
"response": []
}
]
},
{
"name": "Groups",
"item": [
{
"name": "Create a single group",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 201\", () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
" pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");",
"});",
"",
"pm.test(\"Response body contains a valid non-null group ID (the ID of the group which was created)\", () => {",
" pm.expect(pm.response.json().id).to.not.be.null;",
"});",
"",
"pm.collectionVariables.set(\"testGroupId\", pm.response.json().id);"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/scim+json",
"type": "default"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"displayName\": \"createdtestgroup\",\n \"members\": []\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Groups",
"host": [
"{{url}}"
],
"path": [
"Groups"
]
}
},
"response": []
},
{
"name": "Read a single group",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains the group ID of the group we want to read\", () => {",
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));",
"});",
"",
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
" pm.expect(pm.response.json().displayName).to.eql(\"createdtestgroup\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Groups/{{testGroupId}}",
"host": [
"{{url}}"
],
"path": [
"Groups",
"{{testGroupId}}"
]
}
},
"response": []
},
{
"name": "Read all groups",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains group with displayName \\\"createdtestgroup\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].displayName).to.eql(\"createdtestgroup\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Groups",
"host": [
"{{url}}"
],
"path": [
"Groups"
]
}
},
"response": []
},
{
"name": "Update a single group",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 201\", () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains the group ID of the group we want to read\", () => {",
" pm.expect(pm.response.json().id).to.eql(pm.collectionVariables.get('testGroupId'));",
"});",
"",
"pm.test(\"Response body contains group with displayName \\\"updatedtestgroup\\\"\", () => {",
" pm.expect(pm.response.json().displayName).to.eql(\"updatedtestgroup\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"displayName\": \"updatedtestgroup\",\n \"members\": []\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/Groups/{{testGroupId}}",
"host": [
"{{url}}"
],
"path": [
"Groups",
"{{testGroupId}}"
]
}
},
"response": []
},
{
"name": "Delete a single group",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{url}}/Groups/{{testGroupId}}",
"host": [
"{{url}}"
],
"path": [
"Groups",
"{{testGroupId}}"
]
}
},
"response": []
}
]
},
{
"name": "ResourceTypes",
"item": [
{
"name": "Read all ResourceTypes",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains exactly two entries\", () => {",
" pm.expect(pm.response.json().Resources.length).to.eql(2);",
"});",
"",
"pm.test(\"Response body contains ResourceType with id \\\"User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"User\");",
"});",
"",
"pm.test(\"Response body contains ResourceType with id \\\"Group\\\"\", () => {",
" pm.expect(pm.response.json().Resources[1].id).to.eql(\"Group\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/ResourceTypes",
"host": [
"{{url}}"
],
"path": [
"ResourceTypes"
]
}
},
"response": []
}
]
},
{
"name": "Schemas",
"item": [
{
"name": "Read all Schemas",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json().Resources).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains exactly five entries\", () => {",
" pm.expect(pm.response.json().Resources.length).to.eql(5);",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Group\\\"\", () => {",
" pm.expect(pm.response.json().Resources[0].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Group\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\\\"\", () => {",
" pm.expect(pm.response.json().Resources[1].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:ResourceType\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:Schema\\\"\", () => {",
" pm.expect(pm.response.json().Resources[2].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:Schema\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:core:2.0:User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[3].id).to.eql(\"urn:ietf:params:scim:schemas:core:2.0:User\");",
"});",
"",
"pm.test(\"Response body contains Schema with id \\\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\\\"\", () => {",
" pm.expect(pm.response.json().Resources[4].id).to.eql(\"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/Schemas",
"host": [
"{{url}}"
],
"path": [
"Schemas"
]
}
},
"response": []
}
]
},
{
"name": "ServiceProviderConfigs",
"item": [
{
"name": "Read all ServiceProviderConfigs",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Response status code is 200\", () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"Content-Type header is application/scim+json\", () => {",
" pm.expect(pm.response.headers.get('Content-Type')).to.eql('application/scim+json');",
"});",
"",
"pm.test(\"Response body is not empty\", () => {",
" pm.expect(pm.response.json()).to.not.be.empty;",
"});",
"",
"pm.test(\"Response body contains a ServiceProviderConfig with a correct schema\", () => {",
" pm.expect(pm.response.json().schemas).to.include(\"urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/ServiceProviderConfig",
"host": [
"{{url}}"
],
"path": [
"ServiceProviderConfig"
]
}
},
"response": []
}
]
}
],
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
],
"variable": [
{
"key": "testUserId",
"value": ""
},
{
"key": "testGroupId",
"value": ""
}
]
}

View file

@ -0,0 +1,4 @@
{
"displayName": "testGroup",
"members": ["12345678-9012-3456-7890-12345678"]
}

View file

@ -0,0 +1,5 @@
{
"userName": "testusercreate",
"externalId": "testusercreateexternal",
"profileUrl": "http://example.com/testusercreate"
}

View file

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

View file

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