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