diff --git a/CHANGELOG.md b/CHANGELOG.md index f77ae14..faa182f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,11 +36,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. UUIDs or creating UUIDs from existing strings, bytes, or integers, if the UUID is an RFC 4122 variant, one of these instances will be returned: * `Rfc4122\UuidV1` + * `Rfc4122\UuidV2` * `Rfc4122\UuidV3` * `Rfc4122\UuidV4` * `Rfc4122\UuidV5` + * `Rfc4122\NilUuid` * Add `Rfc4122\UuidBuilder` to build RFC 4122 variant UUIDs. This replaces the existing `Builder\DefaultUuidBuilder`, which is now deprecated. +* Add ability to generate version 2 (DCE Security) UUIDs. * Add classes to represent GUIDs and nonstandard (non-RFC 4122 variant) UUIDs: * `Guid\Guid` * `Nonstandard\Uuid`. @@ -68,6 +71,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. returns an instance of `\DateTimeImmutable` instead of `\DateTime`. * Add `getFields()` method to `UuidInterface`. * Add `getValidator()` method to `UuidFactoryInterface`. +* Add `uuid2()` method to `UuidFactoryInterface`. * Add `convertTime()` method to `Converter\TimeConverterInterface`. * Add `getTime()` method to `Provider\TimeProviderInterface`. * Change `Uuid::getFields()` to return an instance of `Fields\FieldsInterface`. diff --git a/composer.json b/composer.json index ab38d90..a69ee38 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "mockery/mockery": "^1.3", "moontoast/math": "^1.1", "paragonie/random-lib": "^2", + "php-mock/php-mock-mockery": "^1.3", "php-mock/php-mock-phpunit": "^2.5", "phpstan/extension-installer": "^1.0", "phpstan/phpdoc-parser": "0.4.1", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index ffcd94b..7619958 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + new DegradedTimeConverter() @@ -44,11 +44,18 @@ uuid_parse($uuid) + + + shell_exec('id -u') + shell_exec('id -g') + shell_exec('whoami /user /fo csv /nh') + shell_exec('net user %username% | findstr /b /i "Local Group Memberships"') + shell_exec('wmic group get name,sid | findstr /b /i ' . escapeshellarg($firstGroup)) + + - + $node - constant('PHP_OS') - constant('PHP_OS') $macs diff --git a/src/Exception/DceSecurityException.php b/src/Exception/DceSecurityException.php new file mode 100644 index 0000000..a65f80c --- /dev/null +++ b/src/Exception/DceSecurityException.php @@ -0,0 +1,25 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Exception; + +use RuntimeException as PhpRuntimeException; + +/** + * Thrown to indicate an exception occurred while dealing with DCE Security + * (version 2) UUIDs + */ +class DceSecurityException extends PhpRuntimeException +{ +} diff --git a/src/FeatureSet.php b/src/FeatureSet.php index 0a907bc..921d07a 100644 --- a/src/FeatureSet.php +++ b/src/FeatureSet.php @@ -24,6 +24,8 @@ use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\Converter\Time\GenericTimeConverter; use Ramsey\Uuid\Converter\Time\PhpTimeConverter; use Ramsey\Uuid\Converter\TimeConverterInterface; +use Ramsey\Uuid\Generator\DceSecurityGenerator; +use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface; use Ramsey\Uuid\Generator\PeclUuidTimeGenerator; use Ramsey\Uuid\Generator\RandomGeneratorFactory; use Ramsey\Uuid\Generator\RandomGeneratorInterface; @@ -33,6 +35,8 @@ use Ramsey\Uuid\Guid\GuidBuilder; use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Math\CalculatorInterface; use Ramsey\Uuid\Nonstandard\UuidBuilder as NonstandardUuidBuilder; +use Ramsey\Uuid\Provider\Dce\SystemDceSecurityProvider; +use Ramsey\Uuid\Provider\DceSecurityProviderInterface; use Ramsey\Uuid\Provider\Node\FallbackNodeProvider; use Ramsey\Uuid\Provider\Node\RandomNodeProvider; use Ramsey\Uuid\Provider\Node\SystemNodeProvider; @@ -81,6 +85,11 @@ class FeatureSet */ private $codec; + /** + * @var DceSecurityGeneratorInterface + */ + private $dceSecurityGenerator; + /** * @var NodeProviderInterface */ @@ -145,6 +154,7 @@ class FeatureSet $this->nodeProvider = $this->buildNodeProvider(); $this->randomGenerator = $this->buildRandomGenerator(); $this->setTimeProvider(new SystemTimeProvider()); + $this->setDceSecurityProvider(new SystemDceSecurityProvider()); $this->validator = new GenericValidator(); } @@ -172,6 +182,14 @@ class FeatureSet return $this->codec; } + /** + * Returns the DCE Security generator configured for this environment + */ + public function getDceSecurityGenerator(): DceSecurityGeneratorInterface + { + return $this->dceSecurityGenerator; + } + /** * Returns the node provider configured for this environment */ @@ -222,6 +240,14 @@ class FeatureSet $this->timeConverter = $this->buildTimeConverter($calculator); } + /** + * Sets the DCE Security provider to use in this environment + */ + public function setDceSecurityProvider(DceSecurityProviderInterface $dceSecurityProvider): void + { + $this->dceSecurityGenerator = $this->buildDceSecurityGenerator($dceSecurityProvider); + } + /** * Sets the time provider to use in this environment */ @@ -252,6 +278,19 @@ class FeatureSet return new StringCodec($this->builder); } + /** + * Returns a DCE Security generator configured for this environment + */ + private function buildDceSecurityGenerator( + DceSecurityProviderInterface $dceSecurityProvider + ): DceSecurityGeneratorInterface { + return new DceSecurityGenerator( + $this->numberConverter, + $this->timeGenerator, + $dceSecurityProvider + ); + } + /** * Returns a node provider configured for this environment */ diff --git a/src/Generator/DceSecurityGenerator.php b/src/Generator/DceSecurityGenerator.php new file mode 100644 index 0000000..91bfc4e --- /dev/null +++ b/src/Generator/DceSecurityGenerator.php @@ -0,0 +1,118 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Generator; + +use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Exception\InvalidArgumentException; +use Ramsey\Uuid\Provider\DceSecurityProviderInterface; +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\IntegerValue; +use Ramsey\Uuid\Uuid; + +/** + * DceSecurityGenerator generates strings of binary data based on a local + * domain, local identifier, node ID, clock sequence, and the current time + */ +class DceSecurityGenerator implements DceSecurityGeneratorInterface +{ + private const DOMAINS = [ + Uuid::DCE_DOMAIN_PERSON, + Uuid::DCE_DOMAIN_GROUP, + Uuid::DCE_DOMAIN_ORG, + ]; + + /** + * @var NumberConverterInterface + */ + private $numberConverter; + + /** + * @var TimeGeneratorInterface + */ + private $timeGenerator; + + /** + * @var DceSecurityProviderInterface + */ + private $dceSecurityProvider; + + public function __construct( + NumberConverterInterface $numberConverter, + TimeGeneratorInterface $timeGenerator, + DceSecurityProviderInterface $dceSecurityProvider + ) { + $this->numberConverter = $numberConverter; + $this->timeGenerator = $timeGenerator; + $this->dceSecurityProvider = $dceSecurityProvider; + } + + public function generate( + int $localDomain, + ?IntegerValue $localIdentifier = null, + ?Hexadecimal $node = null, + ?int $clockSeq = null + ): string { + if (!in_array($localDomain, self::DOMAINS)) { + throw new InvalidArgumentException( + 'Local domain must be a valid DCE Security domain' + ); + } + + switch ($localDomain) { + case Uuid::DCE_DOMAIN_ORG: + if ($localIdentifier === null) { + throw new InvalidArgumentException( + 'A local identifier must be provided for the org domain' + ); + } + + break; + case Uuid::DCE_DOMAIN_PERSON: + if ($localIdentifier === null) { + $localIdentifier = $this->dceSecurityProvider->getUid(); + } + + break; + case Uuid::DCE_DOMAIN_GROUP: + default: + if ($localIdentifier === null) { + $localIdentifier = $this->dceSecurityProvider->getGid(); + } + + break; + } + + $domainByte = pack('n', $localDomain)[1]; + $identifierBytes = hex2bin(str_pad( + $this->numberConverter->toHex($localIdentifier->toString()), + 8, + '0', + STR_PAD_LEFT + )); + + if ($node instanceof Hexadecimal) { + $node = $node->toString(); + } + + /** @var string $bytes */ + $bytes = $this->timeGenerator->generate($node, $clockSeq); + + // Replace bytes in the time-based UUID with DCE Security values. + $bytes = substr_replace($bytes, $identifierBytes, 0, 4); + $bytes = substr_replace($bytes, $domainByte, 9, 1); + + return $bytes; + } +} diff --git a/src/Generator/DceSecurityGeneratorInterface.php b/src/Generator/DceSecurityGeneratorInterface.php new file mode 100644 index 0000000..b428ff1 --- /dev/null +++ b/src/Generator/DceSecurityGeneratorInterface.php @@ -0,0 +1,53 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Generator; + +use Ramsey\Uuid\Rfc4122\UuidV2; +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\IntegerValue; + +/** + * A DCE Security generator generates strings of binary data based on a local + * domain, local identifier, node ID, clock sequence, and the current time + * + * @see UuidV2 + */ +interface DceSecurityGeneratorInterface +{ + /** + * Generate a binary string from a local domain, local identifier, node ID, + * clock sequence, and current time + * + * @param int $localDomain The local domain to use when generating bytes, + * according to DCE Security + * @param IntegerValue|null $localIdentifier The local identifier for the + * given domain; this may be a UID or GID on POSIX systems, if the local + * domain is person or group, or it may be a site-defined identifier + * if the local domain is org + * @param Hexadecimal|null $node A 48-bit number representing the hardware + * address + * @param int|null $clockSeq A 14-bit number used to help avoid duplicates + * that could arise when the clock is set backwards in time or if the + * node ID changes + * + * @return string A binary string + */ + public function generate( + int $localDomain, + ?IntegerValue $localIdentifier = null, + ?Hexadecimal $node = null, + ?int $clockSeq = null + ): string; +} diff --git a/src/Generator/TimeGeneratorInterface.php b/src/Generator/TimeGeneratorInterface.php index afcd87e..7abd8d8 100644 --- a/src/Generator/TimeGeneratorInterface.php +++ b/src/Generator/TimeGeneratorInterface.php @@ -23,11 +23,12 @@ interface TimeGeneratorInterface /** * Generate a binary string from a node ID, clock sequence, and current time * - * @param int|string $node A 48-bit number representing the hardware address; - * this number may be represented as an integer or a hexadecimal string - * @param int $clockSeq A 14-bit number used to help avoid duplicates that - * could arise when the clock is set backwards in time or if the node ID - * changes + * @param int|string|null $node A 48-bit number representing the hardware + * address; this number may be represented as an integer or a + * hexadecimal string + * @param int|null $clockSeq A 14-bit number used to help avoid duplicates + * that could arise when the clock is set backwards in time or if the + * node ID changes * * @return string A binary string */ diff --git a/src/Provider/Dce/SystemDceSecurityProvider.php b/src/Provider/Dce/SystemDceSecurityProvider.php new file mode 100644 index 0000000..ce78104 --- /dev/null +++ b/src/Provider/Dce/SystemDceSecurityProvider.php @@ -0,0 +1,223 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Provider\Dce; + +use Ramsey\Uuid\Exception\DceSecurityException; +use Ramsey\Uuid\Provider\DceSecurityProviderInterface; +use Ramsey\Uuid\Type\IntegerValue; + +/** + * SystemDceSecurityProvider retrieves the user or group identifiers from the system + */ +class SystemDceSecurityProvider implements DceSecurityProviderInterface +{ + /** + * @throws DceSecurityException if unable to get a user identifier + * + * @inheritDoc + */ + public function getUid(): IntegerValue + { + static $uid = null; + + if ($uid instanceof IntegerValue) { + return $uid; + } + + if ($uid === null) { + $uid = $this->getSystemUid(); + } + + if ($uid === '') { + throw new DceSecurityException( + 'Unable to get a user identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + } + + $uid = new IntegerValue($uid); + + return $uid; + } + + /** + * @throws DceSecurityException if unable to get a group identifier + * + * @inheritDoc + */ + public function getGid(): IntegerValue + { + static $gid = null; + + if ($gid instanceof IntegerValue) { + return $gid; + } + + if ($gid === null) { + $gid = $this->getSystemGid(); + } + + if ($gid === '') { + throw new DceSecurityException( + 'Unable to get a group identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + } + + $gid = new IntegerValue($gid); + + return $gid; + } + + /** + * Returns the UID from the system + */ + private function getSystemUid(): string + { + if (!$this->hasShellExec()) { + return ''; + } + + switch ($this->getOs()) { + case 'WIN': + return $this->getWindowsUid(); + case 'DAR': + case 'FRE': + case 'LIN': + default: + return trim((string) shell_exec('id -u')); + } + } + + /** + * Returns the GID from the system + */ + private function getSystemGid(): string + { + if (!$this->hasShellExec()) { + return ''; + } + + switch ($this->getOs()) { + case 'WIN': + return $this->getWindowsGid(); + case 'DAR': + case 'FRE': + case 'LIN': + default: + return trim((string) shell_exec('id -g')); + } + } + + /** + * Returns true if shell_exec() is available for use + */ + private function hasShellExec(): bool + { + $disabledFunctions = strtolower((string) ini_get('disable_functions')); + + return strpos($disabledFunctions, 'shell_exec') === false; + } + + /** + * Returns the PHP_OS string + */ + private function getOs(): string + { + return strtoupper(substr(constant('PHP_OS'), 0, 3)); + } + + /** + * Returns the user identifier for a user on a Windows system + * + * Windows does not have the same concept as an effective POSIX UID for the + * running script. Instead, each user is uniquely identified by an SID + * (security identifier). The SID includes three 32-bit unsigned integers + * that make up a unique domain identifier, followed by an RID (relative + * identifier) that we will use as the UID. The primary caveat is that this + * UID may not be unique to the system, since it is, instead, unique to the + * domain. + * + * @link https://www.lifewire.com/what-is-an-sid-number-2626005 What Is an SID Number? + * @link https://bit.ly/30vE7NM Well-known SID Structures + * @link https://bit.ly/2FWcYKJ Well-known security identifiers in Windows operating systems + * @link https://www.windows-commandline.com/get-sid-of-user/ Get SID of user + */ + private function getWindowsUid(): string + { + $response = shell_exec('whoami /user /fo csv /nh'); + + if ($response === null) { + return ''; + } + + /** @var string $sid */ + $sid = str_getcsv(trim($response))[1] ?? ''; + + if (($lastHyphen = strrpos($sid, '-')) === false) { + return ''; + } + + return trim(substr($sid, $lastHyphen + 1)); + } + + /** + * Returns a group identifier for a user on a Windows system + * + * Since Windows does not have the same concept as an effective POSIX GID + * for the running script, we will get the local group memberships for the + * user running the script. Then, we will get the SID (security identifier) + * for the first group. that appears in that list. Finally, we will return + * the RID (relative identifier) for the group and use that as the GID. + * + * @link https://www.windows-commandline.com/list-of-user-groups-command-line/ List of user groups command line + */ + private function getWindowsGid(): string + { + $response = shell_exec('net user %username% | findstr /b /i "Local Group Memberships"'); + + if ($response === null) { + return ''; + } + + /** @var string[] $userGroups */ + $userGroups = preg_split('/\s{2,}/', $response, -1, PREG_SPLIT_NO_EMPTY); + + $firstGroup = trim($userGroups[1] ?? '', "* \t\n\r\0\x0B"); + + if ($firstGroup === '') { + return ''; + } + + $response = shell_exec('wmic group get name,sid | findstr /b /i ' . escapeshellarg($firstGroup)); + + if ($response === null) { + return ''; + } + + /** @var string[] $userGroup */ + $userGroup = preg_split('/\s{2,}/', $response, -1, PREG_SPLIT_NO_EMPTY); + + $sid = $userGroup[1] ?? ''; + + if (($lastHyphen = strrpos($sid, '-')) === false) { + return ''; + } + + return trim((string) substr($sid, $lastHyphen + 1)); + } +} diff --git a/src/Provider/DceSecurityProviderInterface.php b/src/Provider/DceSecurityProviderInterface.php new file mode 100644 index 0000000..a6174ae --- /dev/null +++ b/src/Provider/DceSecurityProviderInterface.php @@ -0,0 +1,41 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Provider; + +use Ramsey\Uuid\Rfc4122\UuidV2; +use Ramsey\Uuid\Type\IntegerValue; + +/** + * A DCE provider provides access to local domain identifiers for version 2, + * DCE Security, UUIDs + * + * @see UuidV2 + */ +interface DceSecurityProviderInterface +{ + /** + * Returns a user identifier for the system + * + * @link https://en.wikipedia.org/wiki/User_identifier User identifier + */ + public function getUid(): IntegerValue; + + /** + * Returns a group identifier for the system + * + * @link https://en.wikipedia.org/wiki/Group_identifier Group identifier + */ + public function getGid(): IntegerValue; +} diff --git a/src/Rfc4122/UuidBuilder.php b/src/Rfc4122/UuidBuilder.php index 3af789e..fa4d9e5 100644 --- a/src/Rfc4122/UuidBuilder.php +++ b/src/Rfc4122/UuidBuilder.php @@ -21,7 +21,6 @@ use Ramsey\Uuid\Converter\TimeConverterInterface; use Ramsey\Uuid\Exception\UnableToBuildUuidException; use Ramsey\Uuid\Exception\UnsupportedOperationException; use Ramsey\Uuid\Rfc4122\UuidInterface as Rfc4122UuidInterface; -use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Throwable; @@ -79,7 +78,7 @@ class UuidBuilder implements UuidBuilderInterface case 1: return new UuidV1($fields, $this->numberConverter, $codec, $this->timeConverter); case 2: - return new Uuid($fields, $this->numberConverter, $codec, $this->timeConverter); + return new UuidV2($fields, $this->numberConverter, $codec, $this->timeConverter); case 3: return new UuidV3($fields, $this->numberConverter, $codec, $this->timeConverter); case 4: diff --git a/src/Rfc4122/UuidV2.php b/src/Rfc4122/UuidV2.php new file mode 100644 index 0000000..47bf644 --- /dev/null +++ b/src/Rfc4122/UuidV2.php @@ -0,0 +1,33 @@ + + * @license http://opensource.org/licenses/MIT MIT + */ + +declare(strict_types=1); + +namespace Ramsey\Uuid\Rfc4122; + +use Ramsey\Uuid\Uuid; + +/** + * DCE Security version, or version 2, UUIDs include local domain identifier, + * local ID for the specified domain, and node values that are combined into a + * 128-bit unsigned integer + * + * @link https://publications.opengroup.org/c311 DCE 1.1: Authentication and Security Services + * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01 DCE 1.1, §5.2.1.1 + * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 + * @link https://github.com/google/uuid Go package for UUIDs based on RFC 4122 and DCE 1.1: Auth and Security Services + * + * @psalm-immutable + */ +final class UuidV2 extends Uuid implements UuidInterface +{ +} diff --git a/src/Uuid.php b/src/Uuid.php index a61cfac..13f19b4 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -143,6 +143,27 @@ class Uuid implements UuidInterface */ public const UUID_TYPE_HASH_SHA1 = 5; + /** + * DCE Security principal domain + * + * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 + */ + public const DCE_DOMAIN_PERSON = 0; + + /** + * DCE Security group domain + * + * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 + */ + public const DCE_DOMAIN_GROUP = 1; + + /** + * DCE Security organization domain + * + * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1, §11.5.1.1 + */ + public const DCE_DOMAIN_ORG = 2; + /** * @var UuidFactoryInterface|null */ diff --git a/src/UuidFactory.php b/src/UuidFactory.php index 19bcad4..e5e4164 100644 --- a/src/UuidFactory.php +++ b/src/UuidFactory.php @@ -17,9 +17,12 @@ namespace Ramsey\Uuid; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; +use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface; use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Generator\TimeGeneratorInterface; use Ramsey\Uuid\Provider\NodeProviderInterface; +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\IntegerValue; use Ramsey\Uuid\Validator\ValidatorInterface; class UuidFactory implements UuidFactoryInterface @@ -29,6 +32,11 @@ class UuidFactory implements UuidFactoryInterface */ private $codec; + /** + * @var DceSecurityGeneratorInterface + */ + private $dceSecurityGenerator; + /** * @var NodeProviderInterface */ @@ -67,6 +75,7 @@ class UuidFactory implements UuidFactoryInterface $features = $features ?: new FeatureSet(); $this->codec = $features->getCodec(); + $this->dceSecurityGenerator = $features->getDceSecurityGenerator(); $this->nodeProvider = $features->getNodeProvider(); $this->numberConverter = $features->getNumberConverter(); $this->randomGenerator = $features->getRandomGenerator(); @@ -236,6 +245,24 @@ class UuidFactory implements UuidFactoryInterface return $this->uuidFromHashedName($hex, 1); } + public function uuid2( + int $localDomain, + ?IntegerValue $localIdentifier, + ?Hexadecimal $node = null, + ?int $clockSeq = null + ): UuidInterface { + $bytes = $this->dceSecurityGenerator->generate( + $localDomain, + $localIdentifier, + $node, + $clockSeq + ); + + $hex = bin2hex($bytes); + + return $this->uuidFromHashedName($hex, 2); + } + /** * @inheritDoc */ diff --git a/src/UuidFactoryInterface.php b/src/UuidFactoryInterface.php index 9552261..88a308c 100644 --- a/src/UuidFactoryInterface.php +++ b/src/UuidFactoryInterface.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Ramsey\Uuid; +use Ramsey\Uuid\Type\Hexadecimal; +use Ramsey\Uuid\Type\IntegerValue; use Ramsey\Uuid\Validator\ValidatorInterface; /** @@ -44,6 +46,29 @@ interface UuidFactoryInterface */ public function uuid1($node = null, ?int $clockSeq = null): UuidInterface; + /** + * Returns a version 2 (DCE Security) UUID from a local domain, local + * identifier, host ID, clock sequence, and the current time + * + * @param int $localDomain The local domain to use when generating bytes, + * according to DCE Security + * @param IntegerValue|null $localIdentifier The local identifier for the + * given domain; this may be a UID or GID on POSIX systems, if the local + * domain is person or group, or it may be a site-defined identifier + * if the local domain is org + * @param Hexadecimal|null $node A 48-bit number representing the hardware + * address + * @param int|null $clockSeq A 14-bit number used to help avoid duplicates + * that could arise when the clock is set backwards in time or if the + * node ID changes + */ + public function uuid2( + int $localDomain, + ?IntegerValue $localIdentifier, + ?Hexadecimal $node = null, + ?int $clockSeq = null + ): UuidInterface; + /** * Returns a version 3 (name-based) UUID based on the MD5 hash of a * namespace ID and a name diff --git a/tests/Generator/DceSecurityGeneratorTest.php b/tests/Generator/DceSecurityGeneratorTest.php new file mode 100644 index 0000000..d52ede1 --- /dev/null +++ b/tests/Generator/DceSecurityGeneratorTest.php @@ -0,0 +1,157 @@ + new IntegerValue($uid), + 'getGid' => new IntegerValue($gid), + ]); + + $nodeProvider = Mockery::mock(NodeProviderInterface::class, [ + 'getNode' => $node, + ]); + + $timeProvider = new FixedTimeProvider(new Time($seconds, $microseconds)); + + $calculator = new BrickMathCalculator(); + $numberConverter = new GenericNumberConverter($calculator); + $timeConverter = new GenericTimeConverter($calculator); + $timeGenerator = new DefaultTimeGenerator($nodeProvider, $timeConverter, $timeProvider); + + $dceSecurityGenerator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $bytes = $dceSecurityGenerator->generate($providedDomain, $providedId, $providedNode, $providedClockSeq); + + $this->assertSame($expectedId, bin2hex(substr($bytes, 0, 4))); + $this->assertSame($expectedDomain, bin2hex(substr($bytes, 9, 1))); + $this->assertSame($expectedNode, bin2hex(substr($bytes, 10))); + $this->assertSame($expectedTimeMidHi, bin2hex(substr($bytes, 4, 4))); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideValuesForDceSecurityGenerator(): array + { + return [ + [ + 'uid' => '1001', + 'gid' => '2001', + 'node' => '001122334455', + 'seconds' => 1579132397, + 'microseconds' => 500000, + 'providedDomain' => Uuid::DCE_DOMAIN_PERSON, + 'providedId' => null, + 'providedNode' => null, + 'providedClockSeq' => null, + 'expectedId' => '000003e9', + 'expectedDomain' => '00', + 'expectedNode' => '001122334455', + 'expectedTimeMidHi' => '37f211ea', + ], + [ + 'uid' => '1001', + 'gid' => '2001', + 'node' => '001122334455', + 'seconds' => 1579132397, + 'microseconds' => 500000, + 'providedDomain' => Uuid::DCE_DOMAIN_GROUP, + 'providedId' => null, + 'providedNode' => null, + 'providedClockSeq' => null, + 'expectedId' => '000007d1', + 'expectedDomain' => '01', + 'expectedNode' => '001122334455', + 'expectedTimeMidHi' => '37f211ea', + ], + [ + 'uid' => 0, + 'gid' => 0, + 'node' => '001122334455', + 'seconds' => 1579132397, + 'microseconds' => 500000, + 'providedDomain' => Uuid::DCE_DOMAIN_ORG, + 'providedId' => new IntegerValue('4294967295'), + 'providedNode' => null, + 'providedClockSeq' => null, + 'expectedId' => 'ffffffff', + 'expectedDomain' => '02', + 'expectedNode' => '001122334455', + 'expectedTimeMidHi' => '37f211ea', + ], + ]; + } + + public function testGenerateThrowsExceptionForInvalidDomain(): void + { + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + + $generator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Local domain must be a valid DCE Security domain'); + + $generator->generate(42); + } + + public function testGenerateThrowsExceptionForOrgWithoutIdentifier(): void + { + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $dceSecurityProvider = Mockery::mock(DceSecurityProviderInterface::class); + + $generator = new DceSecurityGenerator($numberConverter, $timeGenerator, $dceSecurityProvider); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A local identifier must be provided for the org domain'); + + $generator->generate(Uuid::DCE_DOMAIN_ORG); + } +} diff --git a/tests/Provider/Dce/SystemDceSecurityProviderTest.php b/tests/Provider/Dce/SystemDceSecurityProviderTest.php new file mode 100644 index 0000000..092d14f --- /dev/null +++ b/tests/Provider/Dce/SystemDceSecurityProviderTest.php @@ -0,0 +1,502 @@ +with('disable_functions')->once()->andReturn('foo bar shell_exec baz'); + + $provider = new SystemDceSecurityProvider(); + + // Test that we catch the exception multiple times, but the ini_get() + // function is called only once. + $caughtException = 0; + + for ($i = 1; $i <= 5; $i++) { + try { + $provider->getUid(); + } catch (DceSecurityException $e) { + $caughtException++; + + $this->assertSame( + 'Unable to get a user identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider', + $e->getMessage() + ); + } + } + + $this->assertSame(5, $caughtException); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testGetUidForPosixThrowsExceptionIfShellExecReturnsNull(): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Linux'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('id -u')->once()->andReturnNull(); + + $provider = new SystemDceSecurityProvider(); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Unable to get a user identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + + $provider->getUid(); + } + + /** + * @param mixed $value + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider provideWindowsBadValues + */ + public function testGetUidForWindowsThrowsExceptionIfShellExecForWhoAmIReturnsBadValues($value): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Windows_NT'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('whoami /user /fo csv /nh')->once()->andReturn($value); + + $provider = new SystemDceSecurityProvider(); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Unable to get a user identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + + $provider->getUid(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider provideWindowsGoodWhoAmIValues + */ + public function testGetUidForWindowsWhenShellExecForWhoAmIReturnsGoodValues( + string $value, + string $expectedId + ): void { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Windows_NT'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('whoami /user /fo csv /nh')->once()->andReturn($value); + + $provider = new SystemDceSecurityProvider(); + + $uid = $provider->getUid(); + + $this->assertInstanceOf(IntegerValue::class, $uid); + $this->assertSame($expectedId, $uid->toString()); + $this->assertSame($uid, $provider->getUid()); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideWindowsGoodWhoAmIValues(): array + { + return [ + [ + 'value' => '"Melilot Sackville","S-1-5-21-7375663-6890924511-1272660413-2944159"', + 'expectedId' => '2944159', + ], + [ + 'value' => '"Brutus Sandheaver","S-1-3-12-1234525106-3567804255-30012867-1437"', + 'expectedId' => '1437', + ], + [ + 'value' => '"Cora Rumble","S-345"', + 'expectedId' => '345', + ], + ]; + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider providePosixTestValues + */ + public function testGetUidForPosixSystems(string $os, string $id): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn($os); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('id -u')->once()->andReturn($id); + + $provider = new SystemDceSecurityProvider(); + + $uid = $provider->getUid(); + + $this->assertInstanceOf(IntegerValue::class, $uid); + $this->assertSame($id, $uid->toString()); + $this->assertSame($uid, $provider->getUid()); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testGetGidThrowsExceptionIfShellExecDisabled(): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('foo bar shell_exec baz'); + + $provider = new SystemDceSecurityProvider(); + + // Test that we catch the exception multiple times, but the ini_get() + // function is called only once. + $caughtException = 0; + + for ($i = 1; $i <= 5; $i++) { + try { + $provider->getGid(); + } catch (DceSecurityException $e) { + $caughtException++; + + $this->assertSame( + 'Unable to get a group identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider', + $e->getMessage() + ); + } + } + + $this->assertSame(5, $caughtException); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testGetGidForPosixThrowsExceptionIfShellExecReturnsNull(): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Linux'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('id -g')->once()->andReturnNull(); + + $provider = new SystemDceSecurityProvider(); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Unable to get a group identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + + $provider->getGid(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider providePosixTestValues + */ + public function testGetGidForPosixSystems(string $os, string $id): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn($os); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('id -g')->once()->andReturn($id); + + $provider = new SystemDceSecurityProvider(); + + $gid = $provider->getGid(); + + $this->assertInstanceOf(IntegerValue::class, $gid); + $this->assertSame($id, $gid->toString()); + $this->assertSame($gid, $provider->getGid()); + } + + /** + * @param mixed $value + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider provideWindowsBadValues + */ + public function testGetGidForWindowsThrowsExceptionWhenShellExecForNetUserReturnsBadValues($value): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Windows_NT'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'shell_exec' + )->with('net user %username% | findstr /b /i "Local Group Memberships"')->once()->andReturn($value); + + $provider = new SystemDceSecurityProvider(); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Unable to get a group identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + + $provider->getGid(); + } + + /** + * @param mixed $value + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider provideWindowsBadGroupValues + */ + public function testGetGidForWindowsThrowsExceptionWhenShellExecForWmicGroupGetReturnsBadValues($value): void + { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Windows_NT'); + + $shellExec = PHPMockery::mock('Ramsey\Uuid\Provider\Dce', 'shell_exec'); + + $shellExec + ->with('net user %username% | findstr /b /i "Local Group Memberships"') + ->once() + ->andReturn('Local Group Memberships *Users'); + + $shellExec + ->with("wmic group get name,sid | findstr /b /i 'Users'") + ->once() + ->andReturn($value); + + $provider = new SystemDceSecurityProvider(); + + $this->expectException(DceSecurityException::class); + $this->expectExceptionMessage( + 'Unable to get a group identifier using the system DCE ' + . 'Security provider; please provide a custom identifier or ' + . 'use a different provider' + ); + + $provider->getGid(); + } + + /** + * @runInSeparateProcess + * @preserveGlobalState disabled + * @dataProvider provideWindowsGoodNetUserAndWmicGroupValues + */ + public function testGetGidForWindowsSucceeds( + string $netUserResponse, + string $wmicGroupResponse, + string $expectedGroup, + string $expectedId + ): void { + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'ini_get' + )->with('disable_functions')->once()->andReturn('nothing'); + + PHPMockery::mock( + 'Ramsey\Uuid\Provider\Dce', + 'constant' + )->with('PHP_OS')->once()->andReturn('Windows_NT'); + + $shellExec = PHPMockery::mock('Ramsey\Uuid\Provider\Dce', 'shell_exec'); + + $shellExec + ->with('net user %username% | findstr /b /i "Local Group Memberships"') + ->once() + ->andReturn($netUserResponse); + + $shellExec + ->with("wmic group get name,sid | findstr /b /i '{$expectedGroup}'") + ->once() + ->andReturn($wmicGroupResponse); + + $provider = new SystemDceSecurityProvider(); + + $gid = $provider->getGid(); + + $this->assertInstanceOf(IntegerValue::class, $gid); + $this->assertSame($expectedId, $gid->toString()); + $this->assertSame($gid, $provider->getGid()); + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideWindowsGoodNetUserAndWmicGroupValues(): array + { + return [ + [ + 'netUserResponse' => 'Local Group Memberships *Administrators *Users', + 'wmicGroupResponse' => 'Administrators S-1-5-32-544', + 'expectedGroup' => 'Administrators', + 'expectedId' => '544', + ], + [ + 'netUserResponse' => 'Local Group Memberships Users', + 'wmicGroupResponse' => 'Users S-1-5-32-545', + 'expectedGroup' => 'Users', + 'expectedId' => '545', + ], + [ + 'netUserResponse' => 'Local Group Memberships Guests Nobody', + 'wmicGroupResponse' => 'Guests S-1-5-32-546', + 'expectedGroup' => 'Guests', + 'expectedId' => '546', + ], + [ + 'netUserReponse' => 'Local Group Memberships Some Group Another Group', + 'wmicGroupResponse' => 'Some Group S-1-5-80-19088743-1985229328-4294967295-1324', + 'expectedGroup' => 'Some Group', + 'expectedId' => '1324', + ], + ]; + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function providePosixTestValues(): array + { + return [ + ['os' => 'Darwin', 'id' => '1042'], + ['os' => 'FreeBSD', 'id' => '672'], + ['os' => 'GNU', 'id' => '1008'], + ['os' => 'Linux', 'id' => '567'], + ['os' => 'NetBSD', 'id' => '7234'], + ['os' => 'OpenBSD', 'id' => '2347'], + ['os' => 'OS400', 'id' => '1234'], + ]; + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideWindowsBadValues(): array + { + return [ + ['value' => null], + ['value' => 'foobar'], + ['value' => 'foo,bar,baz'], + ['value' => ''], + ['value' => '1234'], + ['value' => 'Local Group Memberships'], + ['value' => 'Local Group Memberships **** Foo'], + ]; + } + + /** + * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification + */ + public function provideWindowsBadGroupValues(): array + { + return array_merge( + $this->provideWindowsBadValues(), + [ + ['value' => 'Users Not a valid SID string'], + ['value' => 'Users 344aab9758bb0d018b93739e7893fb3a'], + ] + ); + } +} diff --git a/tests/Rfc4122/UuidBuilderTest.php b/tests/Rfc4122/UuidBuilderTest.php index 5f58e21..ac6b6dd 100644 --- a/tests/Rfc4122/UuidBuilderTest.php +++ b/tests/Rfc4122/UuidBuilderTest.php @@ -12,11 +12,11 @@ use Ramsey\Uuid\Math\BrickMathCalculator; use Ramsey\Uuid\Rfc4122\Fields; use Ramsey\Uuid\Rfc4122\UuidBuilder; use Ramsey\Uuid\Rfc4122\UuidV1; +use Ramsey\Uuid\Rfc4122\UuidV2; use Ramsey\Uuid\Rfc4122\UuidV3; use Ramsey\Uuid\Rfc4122\UuidV4; use Ramsey\Uuid\Rfc4122\UuidV5; use Ramsey\Uuid\Test\TestCase; -use Ramsey\Uuid\Uuid; class UuidBuilderTest extends TestCase { @@ -57,7 +57,7 @@ class UuidBuilderTest extends TestCase ], [ 'uuid' => 'ff6f8cb0-c57d-21e1-9b21-0800200c9a66', - 'expectedClass' => Uuid::class, + 'expectedClass' => UuidV2::class, 'expectedVersion' => 2, ], [ diff --git a/tests/UuidFactoryTest.php b/tests/UuidFactoryTest.php index 26b8f00..bd434ce 100644 --- a/tests/UuidFactoryTest.php +++ b/tests/UuidFactoryTest.php @@ -4,15 +4,18 @@ declare(strict_types=1); namespace Ramsey\Uuid\Test; +use Mockery; use PHPUnit\Framework\MockObject\MockObject; use Ramsey\Uuid\Builder\UuidBuilderInterface; use Ramsey\Uuid\Codec\CodecInterface; use Ramsey\Uuid\Converter\NumberConverterInterface; use Ramsey\Uuid\FeatureSet; +use Ramsey\Uuid\Generator\DceSecurityGeneratorInterface; use Ramsey\Uuid\Generator\RandomGeneratorInterface; use Ramsey\Uuid\Generator\TimeGeneratorInterface; use Ramsey\Uuid\Provider\NodeProviderInterface; use Ramsey\Uuid\UuidFactory; +use Ramsey\Uuid\Validator\ValidatorInterface; class UuidFactoryTest extends TestCase { @@ -49,37 +52,46 @@ class UuidFactoryTest extends TestCase public function testGettersReturnValueFromFeatureSet(): void { - $codec = $this->getMockBuilder(CodecInterface::class)->getMock(); - $nodeProvider = $this->getMockBuilder(NodeProviderInterface::class)->getMock(); - $randomGenerator = $this->getMockBuilder(RandomGeneratorInterface::class)->getMock(); - $timeGenerator = $this->getMockBuilder(TimeGeneratorInterface::class)->getMock(); + $codec = Mockery::mock(CodecInterface::class); + $nodeProvider = Mockery::mock(NodeProviderInterface::class); + $randomGenerator = Mockery::mock(RandomGeneratorInterface::class); + $timeGenerator = Mockery::mock(TimeGeneratorInterface::class); + $dceSecurityGenerator = Mockery::mock(DceSecurityGeneratorInterface::class); + $numberConverter = Mockery::mock(NumberConverterInterface::class); + $builder = Mockery::mock(UuidBuilderInterface::class); + $validator = Mockery::mock(ValidatorInterface::class); - $featureSet = $this->getMockBuilder(FeatureSet::class)->getMock(); - $featureSet->method('getCodec')->willReturn($codec); - $featureSet->method('getNodeProvider')->willReturn($nodeProvider); - $featureSet->method('getRandomGenerator')->willReturn($randomGenerator); - $featureSet->method('getTimeGenerator')->willReturn($timeGenerator); + $featureSet = Mockery::mock(FeatureSet::class, [ + 'getCodec' => $codec, + 'getNodeProvider' => $nodeProvider, + 'getRandomGenerator' => $randomGenerator, + 'getTimeGenerator' => $timeGenerator, + 'getDceSecurityGenerator' => $dceSecurityGenerator, + 'getNumberConverter' => $numberConverter, + 'getBuilder' => $builder, + 'getValidator' => $validator, + ]); $uuidFactory = new UuidFactory($featureSet); - $this->assertEquals( + $this->assertSame( $codec, $uuidFactory->getCodec(), 'getCodec did not return CodecInterface from FeatureSet' ); - $this->assertEquals( + $this->assertSame( $nodeProvider, $uuidFactory->getNodeProvider(), 'getNodeProvider did not return NodeProviderInterface from FeatureSet' ); - $this->assertEquals( + $this->assertSame( $randomGenerator, $uuidFactory->getRandomGenerator(), 'getRandomGenerator did not return RandomGeneratorInterface from FeatureSet' ); - $this->assertEquals( + $this->assertSame( $timeGenerator, $uuidFactory->getTimeGenerator(), 'getTimeGenerator did not return TimeGeneratorInterface from FeatureSet'