diff --git a/src/SAML2/XML/md/AbstractRoleDescriptorType.php b/src/SAML2/XML/md/AbstractRoleDescriptorType.php new file mode 100644 index 000000000..edfd88ff4 --- /dev/null +++ b/src/SAML2/XML/md/AbstractRoleDescriptorType.php @@ -0,0 +1,167 @@ + $namespacedAttributes + */ + public function __construct( + protected array $protocolSupportEnumeration, + ?string $ID = null, + ?DateTimeImmutable $validUntil = null, + ?string $cacheDuration = null, + ?Extensions $extensions = null, + protected ?string $errorURL = null, + protected array $keyDescriptor = [], + protected ?Organization $organization = null, + protected array $contact = [], + array $namespacedAttributes = [] + ) { + Assert::maxCount($protocolSupportEnumeration, C::UNBOUNDED_LIMIT); + Assert::minCount( + $protocolSupportEnumeration, + 1, + 'At least one protocol must be supported by this ' . static::NS_PREFIX . ':' . static::getLocalName() . '.', + ); + Assert::allValidURI($protocolSupportEnumeration, SchemaViolationException::class); + Assert::nullOrValidURI($errorURL, SchemaViolationException::class); // Covers the empty string + Assert::maxCount($contact, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf( + $contact, + ContactPerson::class, + 'All contacts must be an instance of md:ContactPerson', + ); + Assert::maxCount($keyDescriptor, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf( + $keyDescriptor, + KeyDescriptor::class, + 'All key descriptors must be an instance of md:KeyDescriptor', + ); + + parent::__construct($ID, $validUntil, $cacheDuration, $extensions); + + $this->setAttributesNS($namespacedAttributes); + } + + + /** + * Collect the value of the errorURL property. + * + * @return string|null + */ + public function getErrorURL(): ?string + { + return $this->errorURL; + } + + + /** + * Collect the value of the protocolSupportEnumeration property. + * + * @return string[] + */ + public function getProtocolSupportEnumeration(): array + { + return $this->protocolSupportEnumeration; + } + + + /** + * Collect the value of the Organization property. + * + * @return \SimpleSAML\SAML2\XML\md\Organization|null + */ + public function getOrganization(): ?Organization + { + return $this->organization; + } + + + /** + * Collect the value of the ContactPersons property. + * + * @return \SimpleSAML\SAML2\XML\md\ContactPerson[] + */ + public function getContactPerson(): array + { + return $this->contact; + } + + + /** + * Collect the value of the KeyDescriptors property. + * + * @return \SimpleSAML\SAML2\XML\md\KeyDescriptor[] + */ + public function getKeyDescriptor(): array + { + return $this->keyDescriptor; + } + + + /** + * Add this RoleDescriptor to an EntityDescriptor. + * + * @param \DOMElement $parent The EntityDescriptor we should append this endpoint to. + * @return \DOMElement + */ + public function toUnsignedXML(?DOMElement $parent = null): DOMElement + { + $e = parent::toUnsignedXML($parent); + $e->setAttribute('protocolSupportEnumeration', implode(' ', $this->getProtocolSupportEnumeration())); + + if ($this->getErrorURL() !== null) { + $e->setAttribute('errorURL', $this->getErrorURL()); + } + + foreach ($this->getKeyDescriptor() as $kd) { + $kd->toXML($e); + } + + $this->getOrganization()?->toXML($e); + + foreach ($this->getContactPerson() as $cp) { + $cp->toXML($e); + } + + return $e; + } +} diff --git a/src/SAML2/XML/md/AbstractSSODescriptor.php b/src/SAML2/XML/md/AbstractSSODescriptor.php new file mode 100644 index 000000000..8b7382b66 --- /dev/null +++ b/src/SAML2/XML/md/AbstractSSODescriptor.php @@ -0,0 +1,166 @@ +artifactResolutionService; + } + + + /** + * Collect the value of the SingleLogoutService-property + * + * @return \SimpleSAML\SAML2\XML\md\AbstractEndpointType[] + */ + public function getSingleLogoutService(): array + { + return $this->singleLogoutService; + } + + + /** + * Collect the value of the ManageNameIDService-property + * + * @return \SimpleSAML\SAML2\XML\md\AbstractEndpointType[] + */ + public function getManageNameIDService(): array + { + return $this->manageNameIDService; + } + + + /** + * Collect the value of the NameIDFormat-property + * + * @return \SimpleSAML\SAML2\XML\md\NameIDFormat[] + */ + public function getNameIDFormat(): array + { + return $this->nameIDFormat; + } + + + /** + * Add this SSODescriptorType to an EntityDescriptor. + * + * @param \DOMElement|null $parent The EntityDescriptor we should append this SSODescriptorType to. + * @return \DOMElement The generated SSODescriptor DOMElement. + */ + public function toUnsignedXML(DOMElement $parent = null): DOMElement + { + $e = parent::toUnsignedXML($parent); + + foreach ($this->getArtifactResolutionService() as $ep) { + $ep->toXML($e); + } + + foreach ($this->getSingleLogoutService() as $ep) { + $ep->toXML($e); + } + + foreach ($this->getManageNameIDService() as $ep) { + $ep->toXML($e); + } + + foreach ($this->getNameIDFormat() as $nidFormat) { + $nidFormat->toXML($e); + } + + return $e; + } +} diff --git a/src/SAML2/XML/md/IDPSSODescriptor.php b/src/SAML2/XML/md/IDPSSODescriptor.php index feb4e4266..cbf8cdf5f 100644 --- a/src/SAML2/XML/md/IDPSSODescriptor.php +++ b/src/SAML2/XML/md/IDPSSODescriptor.php @@ -4,101 +4,111 @@ namespace SimpleSAML\SAML2\XML\md; +use DateTimeImmutable; use DOMElement; use SimpleSAML\Assert\Assert; -use SimpleSAML\SAML2\Constants as C; -use SimpleSAML\SAML2\Utils; -use SimpleSAML\SAML2\Utils\XPath; use SimpleSAML\SAML2\XML\saml\Attribute; +use SimpleSAML\XML\Constants as C; +use SimpleSAML\XML\Exception\InvalidDOMElementException; +use SimpleSAML\XML\Exception\TooManyElementsException; +use SimpleSAML\XMLSecurity\XML\ds\Signature; -use function is_bool; +use function preg_split; /** * Class representing SAML 2 IDPSSODescriptor. * - * @package SimpleSAMLphp + * @package simplesamlphp/saml2 */ -class IDPSSODescriptor extends SSODescriptorType +final class IDPSSODescriptor extends AbstractSSODescriptor { /** - * Whether AuthnRequests sent to this IdP should be signed. + * IDPSSODescriptor constructor. * - * @var bool|null - */ - private ?bool $WantAuthnRequestsSigned = null; - - /** - * List of SingleSignOnService endpoints. - * - * Array with SingleSignOnService objects. - * - * @var \SimpleSAML\SAML2\XML\md\SingleSignOnService[] - */ - private array $SingleSignOnService = []; - - /** - * List of NameIDMappingService endpoints. - * - * Array with NameIDMappingService objects. - * - * @var \SimpleSAML\SAML2\XML\md\NameIDMappingService[] - */ - private array $NameIDMappingService = []; - - /** - * List of AssertionIDRequestService endpoints. - * - * Array with AssertionIDRequestService objects. - * - * @var \SimpleSAML\SAML2\XML\md\AssertionIDRequestService[] - */ - private array $AssertionIDRequestService = []; - - /** - * List of supported attribute profiles. - * - * Array with AttributeProfile objects. - * - * @var \SimpleSAML\SAML2\XML\md\AttributeProfile[] - */ - private array $AttributeProfile = []; - - /** - * List of supported attributes. - * - * Array with \SAML2\XML\saml\Attribute objects. - * - * @var \SimpleSAML\SAML2\XML\saml\Attribute[] - */ - private array $Attribute = []; - - - /** - * Initialize an IDPSSODescriptor. - * - * @param \DOMElement|null $xml The XML element we should load. + * @param \SimpleSAML\SAML2\XML\md\SingleSignOnService[] $singleSignOnService + * @param string[] $protocolSupportEnumeration + * @param bool|null $wantAuthnRequestsSigned + * @param \SimpleSAML\SAML2\XML\md\NameIDMappingService[] $nameIDMappingService + * @param \SimpleSAML\SAML2\XML\md\AssertionIDRequestService[] $assertionIDRequestService + * @param \SimpleSAML\SAML2\XML\md\AttributeProfile[] $attributeProfile + * @param \SimpleSAML\SAML2\XML\saml\Attribute[] $attribute + * @param string|null $ID + * @param \DateTimeImmutable|null $validUntil + * @param string|null $cacheDuration + * @param \SimpleSAML\SAML2\XML\md\Extensions|null $extensions + * @param string|null $errorURL + * @param \SimpleSAML\SAML2\XML\md\KeyDescriptor[] $keyDescriptor + * @param \SimpleSAML\SAML2\XML\md\Organization|null $organization + * @param \SimpleSAML\SAML2\XML\md\ContactPerson[] $contact + * @param \SimpleSAML\SAML2\XML\md\ArtifactResolutionService[] $artifactResolutionService + * @param \SimpleSAML\SAML2\XML\md\SingleLogoutService[] $singleLogoutService + * @param \SimpleSAML\SAML2\XML\md\ManageNameIDService[] $manageNameIDService + * @param \SimpleSAML\SAML2\XML\md\NameIDFormat[] $nameIDFormat */ - public function __construct(DOMElement $xml = null) - { - parent::__construct('md:IDPSSODescriptor', $xml); - - if ($xml === null) { - return; - } - - $this->WantAuthnRequestsSigned = Utils::parseBoolean($xml, 'WantAuthnRequestsSigned', null); - - $this->setSingleSignOnService(SingleSignOnService::getChildrenOfClass($xml)); - $this->setNameIDMappingService(NameIDMappingService::getChildrenOfClass($xml)); - $this->setAssertionIDRequestService(AssertionIDRequestService::getChildrenOfClass($xml)); - $this->setAttributeProfile(AttributeProfile::getChildrenOfClass($xml)); - - $xpCache = XPath::getXPath($xml); - - /** @var \DOMElement $a */ - foreach (XPath::xpQuery($xml, './saml_assertion:Attribute', $xpCache) as $a) { - $this->Attribute[] = new Attribute($a); - } + public function __construct( + protected array $singleSignOnService, + array $protocolSupportEnumeration, + protected ?bool $wantAuthnRequestsSigned = null, + protected array $nameIDMappingService = [], + protected array $assertionIDRequestService = [], + protected array $attributeProfile = [], + protected array $attribute = [], + ?string $ID = null, + ?DateTimeImmutable $validUntil = null, + ?string $cacheDuration = null, + ?Extensions $extensions = null, + ?string $errorURL = null, + array $keyDescriptor = [], + ?Organization $organization = null, + array $contact = [], + array $artifactResolutionService = [], + array $singleLogoutService = [], + array $manageNameIDService = [], + array $nameIDFormat = [], + ) { + Assert::maxCount($singleSignOnService, C::UNBOUNDED_LIMIT); + Assert::minCount($singleSignOnService, 1, 'At least one SingleSignOnService must be specified.'); + Assert::allIsInstanceOf( + $singleSignOnService, + SingleSignOnService::class, + 'All md:SingleSignOnService endpoints must be an instance of SingleSignOnService.', + ); + Assert::maxCount($nameIDMappingService, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf( + $nameIDMappingService, + NameIDMappingService::class, + 'All md:NameIDMappingService endpoints must be an instance of NameIDMappingService.', + ); + Assert::maxCount($assertionIDRequestService, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf( + $assertionIDRequestService, + AssertionIDRequestService::class, + 'All md:AssertionIDRequestService endpoints must be an instance of AssertionIDRequestService.', + ); + Assert::maxCount($attributeProfile, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf($attributeProfile, AttributeProfile::class); + Assert::maxCount($attribute, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf( + $attribute, + Attribute::class, + 'All md:Attribute elements must be an instance of Attribute.', + ); + + parent::__construct( + $protocolSupportEnumeration, + $ID, + $validUntil, + $cacheDuration, + $extensions, + $errorURL, + $keyDescriptor, + $organization, + $contact, + $artifactResolutionService, + $singleLogoutService, + $manageNameIDService, + $nameIDFormat, + ); } @@ -109,220 +119,172 @@ public function __construct(DOMElement $xml = null) */ public function wantAuthnRequestsSigned(): ?bool { - return $this->WantAuthnRequestsSigned; - } - - - /** - * Set the value of the WantAuthnRequestsSigned-property - * - * @param bool|null $flag - * @return void - */ - public function setWantAuthnRequestsSigned(bool $flag = null): void - { - $this->WantAuthnRequestsSigned = $flag; + return $this->wantAuthnRequestsSigned; } /** - * Collect the value of the SingleSignOnService-property + * Get the SingleSignOnService endpoints * * @return \SimpleSAML\SAML2\XML\md\SingleSignOnService[] */ public function getSingleSignOnService(): array { - return $this->SingleSignOnService; + return $this->singleSignOnService; } /** - * Set the value of the SingleSignOnService-property - * - * @param \SimpleSAML\SAML2\XML\md\SingleSignOnService[] $singleSignOnService - * @return void - */ - public function setSingleSignOnService(array $singleSignOnService): void - { - Assert::allIsInstanceOf($singleSignOnService, SingleSignOnService::class); - $this->SingleSignOnService = $singleSignOnService; - } - - - /** - * Add the value to the SingleSignOnService-property - * - * @param \SimpleSAML\SAML2\XML\md\SingleSignOnService $singleSignOnService - * @return void - */ - public function addSingleSignOnService(SingleSignOnService $singleSignOnService): void - { - $this->SingleSignOnService[] = $singleSignOnService; - } - - - /** - * Collect the value of the NameIDMappingService-property + * Get the NameIDMappingService endpoints * * @return \SimpleSAML\SAML2\XML\md\NameIDMappingService[] */ public function getNameIDMappingService(): array { - return $this->NameIDMappingService; + return $this->nameIDMappingService; } /** - * Set the value of the NameIDMappingService-property - * - * @param \SimpleSAML\SAML2\XML\md\NameIDMappingService[] $nameIDMappingService - * @return void - */ - public function setNameIDMappingService(array $nameIDMappingService): void - { - Assert::allIsInstanceOf($nameIDMappingService, NameIDMappingService::class); - $this->NameIDMappingService = $nameIDMappingService; - } - - - /** - * Add the value to the NameIDMappingService-property - * - * @param \SimpleSAML\SAML2\XML\md\NameIDMappingService $nameIDMappingService - * @return void - */ - public function addNameIDMappingService(NameIDMappingService $nameIDMappingService): void - { - $this->NameIDMappingService[] = $nameIDMappingService; - } - - - /** - * Collect the value of the AssertionIDRequestService-property + * Collect the AssertionIDRequestService endpoints * * @return \SimpleSAML\SAML2\XML\md\AssertionIDRequestService[] */ public function getAssertionIDRequestService(): array { - return $this->AssertionIDRequestService; + return $this->assertionIDRequestService; } /** - * Set the value of the AssertionIDRequestService-property - * - * @param \SimpleSAML\SAML2\XML\md\AssertionIDRequestService[] $assertionIDRequestService - * @return void - */ - public function setAssertionIDRequestService(array $assertionIDRequestService): void - { - Assert::allIsInstanceOf($assertionIDRequestService, AssertionIDRequestService::class); - $this->AssertionIDRequestService = $assertionIDRequestService; - } - - - /** - * Add the value to the AssertionIDRequestService-property - * - * @param \SimpleSAML\SAML2\XML\md\AssertionIDRequestService $assertionIDRequestService - * @return void - */ - public function addAssertionIDRequestService(AssertionIDRequestService $assertionIDRequestService): void - { - $this->AssertionIDRequestService[] = $assertionIDRequestService; - } - - - /** - * Collect the value of the AttributeProfile-property + * Get the attribute profiles supported * * @return \SimpleSAML\SAML2\XML\md\AttributeProfile[] */ public function getAttributeProfile(): array { - return $this->AttributeProfile; - } - - - /** - * Set the value of the AttributeProfile-property - * - * @param \SimpleSAML\SAML2\XML\md\AttributeProfile[] $attributeProfile - * @return void - */ - public function setAttributeProfile(array $attributeProfile): void - { - Assert::allIsInstanceOf($attributeProfile, AttributeProfile::class); - $this->AttributeProfile = $attributeProfile; + return $this->attributeProfile; } /** - * Collect the value of the Attribute-property + * Get the attributes supported by this IdP * * @return \SimpleSAML\SAML2\XML\saml\Attribute[] */ - public function getAttribute(): array + public function getSupportedAttribute(): array { - return $this->Attribute; + return $this->attribute; } /** - * Set the value of the Attribute-property + * Initialize an IDPSSODescriptor. * - * @param \SimpleSAML\SAML2\XML\saml\Attribute[] $attribute - * @return void - */ - public function setAttribute(array $attribute): void - { - $this->Attribute = $attribute; - } - - - /** - * Addthe value to the Attribute-property + * @param \DOMElement $xml The XML element we should load. + * @return static * - * @param \SimpleSAML\SAML2\XML\saml\Attribute $attribute - * @return void + * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException + * if the qualified name of the supplied element is wrong + * @throws \SimpleSAML\XML\Exception\MissingElementException + * if one of the mandatory child-elements is missing + * @throws \SimpleSAML\XML\Exception\TooManyElementsException + * if too many child-elements of a type are specified */ - public function addAttribute(Attribute $attribute): void + public static function fromXML(DOMElement $xml): static { - $this->Attribute[] = $attribute; + Assert::same($xml->localName, 'IDPSSODescriptor', InvalidDOMElementException::class); + Assert::same($xml->namespaceURI, IDPSSODescriptor::NS, InvalidDOMElementException::class); + + $protocols = self::getAttribute($xml, 'protocolSupportEnumeration'); + $validUntil = self::getOptionalAttribute($xml, 'validUntil', null); + Assert::nullOrValidDateTimeZulu($validUntil); + + $orgs = Organization::getChildrenOfClass($xml); + Assert::maxCount( + $orgs, + 1, + 'More than one Organization found in this descriptor', + TooManyElementsException::class, + ); + + $extensions = Extensions::getChildrenOfClass($xml); + Assert::maxCount( + $extensions, + 1, + 'Only one md:Extensions element is allowed.', + TooManyElementsException::class, + ); + + $signature = Signature::getChildrenOfClass($xml); + Assert::maxCount( + $signature, + 1, + 'Only one ds:Signature element is allowed.', + TooManyElementsException::class, + ); + + $idpssod = new static( + SingleSignOnService::getChildrenOfClass($xml), + preg_split('/[\s]+/', trim($protocols)), + self::getOptionalBooleanAttribute($xml, 'WantAuthnRequestsSigned', null), + NameIDMappingService::getChildrenOfClass($xml), + AssertionIDRequestService::getChildrenOfClass($xml), + AttributeProfile::getChildrenOfClass($xml), + Attribute::getChildrenOfClass($xml), + self::getOptionalAttribute($xml, 'ID', null), + $validUntil !== null ? new DateTimeImmutable($validUntil) : null, + self::getOptionalAttribute($xml, 'cacheDuration', null), + !empty($extensions) ? $extensions[0] : null, + self::getOptionalAttribute($xml, 'errorURL', null), + KeyDescriptor::getChildrenOfClass($xml), + !empty($orgs) ? $orgs[0] : null, + ContactPerson::getChildrenOfClass($xml), + ArtifactResolutionService::getChildrenOfClass($xml), + SingleLogoutService::getChildrenOfClass($xml), + ManageNameIDService::getChildrenOfClass($xml), + NameIDFormat::getChildrenOfClass($xml), + ); + + if (!empty($signature)) { + $idpssod->setSignature($signature[0]); + $idpssod->setXML($xml); + } + return $idpssod; } /** - * Add this IDPSSODescriptor to an EntityDescriptor. + * Convert this assertion to an unsigned XML document. + * This method does not sign the resulting XML document. * - * @param \DOMElement $parent The EntityDescriptor we should append this IDPSSODescriptor to. - * @return \DOMElement + * @return \DOMElement The root element of the DOM tree */ - public function toXML(DOMElement $parent): DOMElement + public function toUnsignedXML(?DOMElement $parent = null): DOMElement { - $e = parent::toXML($parent); + $e = parent::toUnsignedXML($parent); - if (is_bool($this->WantAuthnRequestsSigned)) { - $e->setAttribute('WantAuthnRequestsSigned', $this->WantAuthnRequestsSigned ? 'true' : 'false'); + if (is_bool($this->wantAuthnRequestsSigned)) { + $e->setAttribute('WantAuthnRequestsSigned', $this->wantAuthnRequestsSigned ? 'true' : 'false'); } - foreach ($this->SingleSignOnService as $ssos) { - $ssos->toXML($e); + foreach ($this->getSingleSignOnService() as $ep) { + $ep->toXML($e); } - foreach ($this->NameIDMappingService as $nidms) { - $nidms->toXML($e); + foreach ($this->getNameIDMappingService() as $ep) { + $ep->toXML($e); } - foreach ($this->AssertionIDRequestService as $aidrs) { - $aidrs->toXML($e); + foreach ($this->getAssertionIDRequestService() as $ep) { + $ep->toXML($e); } - foreach ($this->AttributeProfile as $ap) { + foreach ($this->getAttributeProfile() as $ap) { $ap->toXML($e); } - foreach ($this->Attribute as $a) { + foreach ($this->getSupportedAttribute() as $a) { $a->toXML($e); } diff --git a/src/SAML2/XML/md/SPSSODescriptor.php b/src/SAML2/XML/md/SPSSODescriptor.php index 051218229..02b3614b0 100644 --- a/src/SAML2/XML/md/SPSSODescriptor.php +++ b/src/SAML2/XML/md/SPSSODescriptor.php @@ -4,71 +4,106 @@ namespace SimpleSAML\SAML2\XML\md; +use DateTimeImmutable; use DOMElement; use SimpleSAML\Assert\Assert; -use SimpleSAML\SAML2\Utils; -use SimpleSAML\SAML2\Utils\XPath; +use SimpleSAML\XML\Constants as C; +use SimpleSAML\XML\Exception\InvalidDOMElementException; +use SimpleSAML\XML\Exception\TooManyElementsException; +use SimpleSAML\XMLSecurity\XML\ds\Signature; +use function array_filter; use function is_bool; +use function preg_split; /** * Class representing SAML 2 SPSSODescriptor. * - * @package SimpleSAMLphp + * @package simplesamlphp/saml2 */ -class SPSSODescriptor extends SSODescriptorType +final class SPSSODescriptor extends AbstractSSODescriptor { /** - * Whether this SP signs authentication requests. + * SPSSODescriptor constructor. * - * @var bool|null + * @param \SimpleSAML\SAML2\XML\md\AssertionConsumerService[] $assertionConsumerService + * @param string[] $protocolSupportEnumeration + * @param bool|null $authnRequestsSigned + * @param bool|null $wantAssertionsSigned + * @param \SimpleSAML\SAML2\XML\md\AttributeConsumingService[] $attributeConsumingService + * @param string|null $ID + * @param \DateTimeImmutable|null $validUntil + * @param string|null $cacheDuration + * @param \SimpleSAML\SAML2\XML\md\Extensions|null $extensions + * @param string|null $errorURL + * @param \SimpleSAML\SAML2\XML\md\KeyDescriptor[] $keyDescriptors + * @param \SimpleSAML\SAML2\XML\md\Organization|null $organization + * @param \SimpleSAML\SAML2\XML\md\ContactPerson[] $contacts + * @param \SimpleSAML\SAML2\XML\md\ArtifactResolutionService[] $artifactResolutionService + * @param \SimpleSAML\SAML2\XML\md\SingleLogoutService[] $singleLogoutService + * @param \SimpleSAML\SAML2\XML\md\ManageNameIDService[] $manageNameIDService + * @param \SimpleSAML\SAML2\XML\md\NameIDFormat[] $nameIDFormat */ - private ?bool $AuthnRequestsSigned = null; - - /** - * Whether this SP wants the Assertion elements to be signed. - * - * @var bool|null - */ - private ?bool $WantAssertionsSigned = null; - - /** - * List of AssertionConsumerService endpoints for this SP. - * - * Array with AssertionConsumerService objects. - * - * @var \SimpleSAML\SAML2\XML\md\AssertionConsumerService[] - */ - private array $AssertionConsumerService = []; - - /** - * List of AttributeConsumingService descriptors for this SP. - * - * Array with \SimpleSAML\SAML2\XML\md\AttributeConsumingService objects. - * - * @var \SimpleSAML\SAML2\XML\md\AttributeConsumingService[] - */ - private array $AttributeConsumingService = []; - - - /** - * Initialize a SPSSODescriptor. - * - * @param \DOMElement|null $xml The XML element we should load. - */ - public function __construct(DOMElement $xml = null) - { - parent::__construct('md:SPSSODescriptor', $xml); - - if ($xml === null) { - return; - } - - $this->AuthnRequestsSigned = Utils::parseBoolean($xml, 'AuthnRequestsSigned', null); - $this->WantAssertionsSigned = Utils::parseBoolean($xml, 'WantAssertionsSigned', null); - - $this->AssertionConsumerService = AssertionConsumerService::getChildrenOfClass($xml); - $this->AttributeConsumingService = AttributeConsumingService::getChildrenOfClass($xml); + public function __construct( + protected array $assertionConsumerService, + array $protocolSupportEnumeration, + protected ?bool $authnRequestsSigned = null, + protected ?bool $wantAssertionsSigned = null, + protected array $attributeConsumingService = [], + ?string $ID = null, + ?DateTimeImmutable $validUntil = null, + ?string $cacheDuration = null, + ?Extensions $extensions = null, + ?string $errorURL = null, + array $keyDescriptors = [], + ?Organization $organization = null, + array $contacts = [], + array $artifactResolutionService = [], + array $singleLogoutService = [], + array $manageNameIDService = [], + array $nameIDFormat = [], + ) { + parent::__construct( + $protocolSupportEnumeration, + $ID, + $validUntil, + $cacheDuration, + $extensions, + $errorURL, + $keyDescriptors, + $organization, + $contacts, + $artifactResolutionService, + $singleLogoutService, + $manageNameIDService, + $nameIDFormat + ); + + Assert::maxCount($assertionConsumerService, C::UNBOUNDED_LIMIT); + Assert::minCount($assertionConsumerService, 1, 'At least one AssertionConsumerService must be specified.'); + Assert::allIsInstanceOf( + $assertionConsumerService, + AssertionConsumerService::class, + 'All md:AssertionConsumerService endpoints must be an instance of AssertionConsumerService.', + ); + Assert::maxCount($attributeConsumingService, C::UNBOUNDED_LIMIT); + Assert::allIsInstanceOf( + $attributeConsumingService, + AttributeConsumingService::class, + 'All md:AttributeConsumingService endpoints must be an instance of AttributeConsumingService.', + ); + + // test that only one ACS is marked as default + Assert::maxCount( + array_filter( + $attributeConsumingService, + function (AttributeConsumingService $acs) { + return $acs->getIsDefault() === true; + } + ), + 1, + 'Only one md:AttributeConsumingService can be set as default.', + ); } @@ -79,19 +114,7 @@ public function __construct(DOMElement $xml = null) */ public function getAuthnRequestsSigned(): ?bool { - return $this->AuthnRequestsSigned; - } - - - /** - * Set the value of the AuthnRequestsSigned-property - * - * @param bool|null $flag - * @return void - */ - public function setAuthnRequestsSigned(bool $flag = null): void - { - $this->AuthnRequestsSigned = $flag; + return $this->authnRequestsSigned; } @@ -100,21 +123,9 @@ public function setAuthnRequestsSigned(bool $flag = null): void * * @return bool|null */ - public function wantAssertionsSigned(): ?bool + public function getWantAssertionsSigned(): ?bool { - return $this->WantAssertionsSigned; - } - - - /** - * Set the value of the WantAssertionsSigned-property - * - * @param bool|null $flag - * @return void - */ - public function setWantAssertionsSigned(bool $flag = null): void - { - $this->WantAssertionsSigned = $flag; + return $this->wantAssertionsSigned; } @@ -125,32 +136,7 @@ public function setWantAssertionsSigned(bool $flag = null): void */ public function getAssertionConsumerService(): array { - return $this->AssertionConsumerService; - } - - - /** - * Set the value of the AssertionConsumerService-property - * - * @param \SimpleSAML\SAML2\XML\md\AssertionConsumerService[] $acs - * @return void - */ - public function setAssertionConsumerService(array $acs): void - { - Assert::allIsInstanceOf($acs, AssertionConsumerService::class); - $this->AssertionConsumerService = $acs; - } - - - /** - * Add the value to the AssertionConsumerService-property - * - * @param \SimpleSAML\SAML2\XML\md\AssertionConsumerService $acs - * @return void - */ - public function addAssertionConsumerService(AssertionConsumerService $acs): void - { - $this->AssertionConsumerService[] = $acs; + return $this->assertionConsumerService; } @@ -161,58 +147,108 @@ public function addAssertionConsumerService(AssertionConsumerService $acs): void */ public function getAttributeConsumingService(): array { - return $this->AttributeConsumingService; + return $this->attributeConsumingService; } /** - * Add the value to the AttributeConsumingService-property + * Convert XML into a SPSSODescriptor * - * @param \SimpleSAML\SAML2\XML\md\AttributeConsumingService $acs - * @return void - */ - public function addAttributeConsumingService(AttributeConsumingService $acs): void - { - $this->AttributeConsumingService[] = $acs; - } - - - /** - * Set the value of the AttributeConsumingService-property + * @param \DOMElement $xml The XML element we should load + * @return static * - * @param \SimpleSAML\SAML2\XML\md\AttributeConsumingService[] $acs - * @return void + * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException + * if the qualified name of the supplied element is wrong + * @throws \SimpleSAML\XML\Exception\MissingAttributeException + * if the supplied element is missing one of the mandatory attributes + * @throws \SimpleSAML\XML\Exception\TooManyElementsException + * if too many child-elements of a type are specified */ - public function setAttributeConsumingService(array $acs): void + public static function fromXML(DOMElement $xml): static { - Assert::allIsInstanceOf($acs, AttributeConsumingService::class); - $this->AttributeConsumingService = $acs; + Assert::same($xml->localName, 'SPSSODescriptor', InvalidDOMElementException::class); + Assert::same($xml->namespaceURI, SPSSODescriptor::NS, InvalidDOMElementException::class); + + $protocols = self::getAttribute($xml, 'protocolSupportEnumeration'); + $validUntil = self::getOptionalAttribute($xml, 'validUntil', null); + Assert::nullOrValidDateTimeZulu($validUntil); + + $orgs = Organization::getChildrenOfClass($xml); + Assert::maxCount( + $orgs, + 1, + 'More than one Organization found in this descriptor', + TooManyElementsException::class, + ); + + $extensions = Extensions::getChildrenOfClass($xml); + Assert::maxCount( + $extensions, + 1, + 'Only one md:Extensions element is allowed.', + TooManyElementsException::class, + ); + + $signature = Signature::getChildrenOfClass($xml); + Assert::maxCount( + $signature, + 1, + 'Only one ds:Signature element is allowed.', + TooManyElementsException::class, + ); + + $spssod = new static( + AssertionConsumerService::getChildrenOfClass($xml), + preg_split('/[\s]+/', trim($protocols)), + self::getOptionalBooleanAttribute($xml, 'AuthnRequestsSigned', null), + self::getOptionalBooleanAttribute($xml, 'WantAssertionsSigned', null), + AttributeConsumingService::getChildrenOfClass($xml), + self::getOptionalAttribute($xml, 'ID', null), + $validUntil !== null ? new DateTimeImmutable($validUntil) : null, + self::getOptionalAttribute($xml, 'cacheDuration', null), + !empty($extensions) ? $extensions[0] : null, + self::getOptionalAttribute($xml, 'errorURL', null), + KeyDescriptor::getChildrenOfClass($xml), + !empty($orgs) ? $orgs[0] : null, + ContactPerson::getChildrenOfClass($xml), + ArtifactResolutionService::getChildrenOfClass($xml), + SingleLogoutService::getChildrenOfClass($xml), + ManageNameIDService::getChildrenOfClass($xml), + NameIDFormat::getChildrenOfClass($xml), + ); + + if (!empty($signature)) { + $spssod->setSignature($signature[0]); + $spssod->setXML($xml); + } + + return $spssod; } /** - * Add this SPSSODescriptor to an EntityDescriptor. + * Convert this assertion to an unsigned XML document. + * This method does not sign the resulting XML document. * - * @param \DOMElement $parent The EntityDescriptor we should append this SPSSODescriptor to. - * @return \DOMElement + * @return \DOMElement The root element of the DOM tree */ - public function toXML(DOMElement $parent): DOMElement + public function toUnsignedXML(?DOMElement $parent = null): DOMElement { - $e = parent::toXML($parent); + $e = parent::toUnsignedXML($parent); - if (is_bool($this->AuthnRequestsSigned)) { - $e->setAttribute('AuthnRequestsSigned', $this->AuthnRequestsSigned ? 'true' : 'false'); + if (is_bool($this->getAuthnRequestsSigned())) { + $e->setAttribute('AuthnRequestsSigned', $this->getAuthnRequestsSigned() ? 'true' : 'false'); } - if (is_bool($this->WantAssertionsSigned)) { - $e->setAttribute('WantAssertionsSigned', $this->WantAssertionsSigned ? 'true' : 'false'); + if (is_bool($this->getWantAssertionsSigned())) { + $e->setAttribute('WantAssertionsSigned', $this->getWantAssertionsSigned() ? 'true' : 'false'); } - foreach ($this->AssertionConsumerService as $acs) { - $acs->toXML($e); + foreach ($this->getAssertionConsumerService() as $ep) { + $ep->toXML($e); } - foreach ($this->AttributeConsumingService as $acs) { + foreach ($this->getAttributeConsumingService() as $acs) { $acs->toXML($e); } diff --git a/src/SAML2/XML/md/SSODescriptorType.php b/src/SAML2/XML/md/SSODescriptorType.php deleted file mode 100644 index e3e8ed711..000000000 --- a/src/SAML2/XML/md/SSODescriptorType.php +++ /dev/null @@ -1,242 +0,0 @@ -addArtifactResolutionService($ars); - } - - $this->setSingleLogoutService(SingleLogoutService::getChildrenOfClass($xml)); - $this->setManageNameIDService(ManageNameIDService::getChildrenOfClass($xml)); - $this->setNameIDFormat(NameIDFormat::getChildrenOfClass($xml)); - } - - - /** - * Collect the value of the ArtifactResolutionService-property - * - * @return \SimpleSAML\SAML2\XML\md\ArtifactResolutionService[] - */ - public function getArtifactResolutionService(): array - { - return $this->ArtifactResolutionService; - } - - - /** - * Set the value of the ArtifactResolutionService-property - * - * @param \SimpleSAML\SAML2\XML\md\ArtifactResolutionService[] $artifactResolutionService - * @return void - */ - public function setArtifactResolutionService(array $artifactResolutionService): void - { - Assert::allIsInstanceOf($artifactResolutionService, ArtifactResolutionService::class); - $this->ArtifactResolutionService = $artifactResolutionService; - } - - - /** - * Add the value to the ArtifactResolutionService-property - * - * @param \SimpleSAML\SAML2\XML\md\ArtifactResolutionService $artifactResolutionService - * @return void - */ - public function addArtifactResolutionService(ArtifactResolutionService $artifactResolutionService): void - { - $this->ArtifactResolutionService[] = $artifactResolutionService; - } - - - /** - * Collect the value of the SingleLogoutService-property - * - * @return \SimpleSAML\SAML2\XML\md\SingleLogoutService[] - */ - public function getSingleLogoutService(): array - { - return $this->SingleLogoutService; - } - - - /** - * Set the value of the SingleLogoutService-property - * - * @param \SimpleSAML\SAML2\XML\md\SingleLogoutService[] $singleLogoutService - * @return void - */ - public function setSingleLogoutService(array $singleLogoutService): void - { - Assert::allIsInstanceOf($singleLogoutService, SingleLogoutService::class); - $this->SingleLogoutService = $singleLogoutService; - } - - - /** - * Add the value to the SingleLogoutService-property - * - * @param \SimpleSAML\SAML2\XML\md\SingleLogoutService $singleLogoutService - * @return void - */ - public function addSingleLogoutService(SingleLogoutService $singleLogoutService): void - { - $this->SingleLogoutService[] = $singleLogoutService; - } - - - /** - * Collect the value of the ManageNameIDService-property - * - * @return \SimpleSAML\SAML2\XML\md\ManageNameIDService[] - */ - public function getManageNameIDService(): array - { - return $this->ManageNameIDService; - } - - - /** - * Set the value of the ManageNameIDService-property - * - * @param \SimpleSAML\SAML2\XML\md\ManageNameIDService[] $manageNameIDService - * @return void - */ - public function setManageNameIDService(array $manageNameIDService): void - { - Assert::allIsInstanceOf($manageNameIDService, ManageNameIDService::class); - $this->ManageNameIDService = $manageNameIDService; - } - - - /** - * Add the value to the ManageNameIDService-property - * - * @param \SimpleSAML\SAML2\XML\md\ManageNameIDService $manageNameIDService - * @return void - */ - public function addManageNameIDService(ManageNameIDService $manageNameIDService): void - { - $this->ManageNameIDService[] = $manageNameIDService; - } - - - /** - * Collect the value of the NameIDFormat-property - * - * @return \SimpleSAML\SAML2\XML\md\NameIDFormat[] - */ - public function getNameIDFormat(): array - { - return $this->NameIDFormat; - } - - - /** - * Set the value of the NameIDFormat-property - * - * @param \SimpleSAML\SAML2\XML\md\NameIDFormat[] $nameIDFormat - * @return void - */ - public function setNameIDFormat(array $nameIDFormat): void - { - Assert::allIsInstanceOf($nameIDFormat, NameIDFormat::class); - $this->NameIDFormat = $nameIDFormat; - } - - - /** - * Add this SSODescriptorType to an EntityDescriptor. - * - * @param \DOMElement $parent The EntityDescriptor we should append this SSODescriptorType to. - * @return \DOMElement The generated SSODescriptor DOMElement. - */ - public function toXML(DOMElement $parent): DOMElement - { - $e = parent::toXML($parent); - - foreach ($this->ArtifactResolutionService as $ars) { - $ars->toXML($e); - } - - foreach ($this->SingleLogoutService as $sls) { - $sls->toXML($e); - } - - foreach ($this->ManageNameIDService as $mnids) { - $mnids->toXML($e); - } - - foreach ($this->NameIDFormat as $nid) { - $nid->toXML($e); - } - - return $e; - } -} diff --git a/tests/SAML2/XML/md/IDPSSODescriptorTest.php b/tests/SAML2/XML/md/IDPSSODescriptorTest.php new file mode 100644 index 000000000..5d30febbe --- /dev/null +++ b/tests/SAML2/XML/md/IDPSSODescriptorTest.php @@ -0,0 +1,405 @@ +assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($idpssod), + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if no SingleSignOnService endpoints are provided. + */ + public function testMarshallingWithEmptySingleSignOnService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('At least one SingleSignOnService must be specified.'); + new IDPSSODescriptor([], [C::NS_SAMLP]); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if SingleSignOnService endpoints passed have the + * wrong type. + */ + public function testMarshallingWithWrongSingleSignOnService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'All md:SingleSignOnService endpoints must be an instance of SingleSignOnService.', + ); + + /** @psalm-suppress InvalidArgument */ + new IDPSSODescriptor( + [new AssertionIDRequestService(C::BINDING_HTTP_POST, C::LOCATION_A)], + [C::NS_SAMLP], + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if no protocol is passed. + */ + public function testMarshallingWithoutProtocolSupportThrowsException(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('At least one protocol must be supported by this md:IDPSSODescriptor.'); + + /** @psalm-suppress InvalidArgument */ + new IDPSSODescriptor( + [new SingleSignOnService(C::BINDING_HTTP_POST, C::LOCATION_A)], + [], + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if NameIDMappingService endpoints passed have the + * wrong type. + */ + public function testMarshallingWithWrongNameIDMappingService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'All md:NameIDMappingService endpoints must be an instance of NameIDMappingService.', + ); + + /** @psalm-suppress InvalidArgument */ + new IDPSSODescriptor( + singleSignOnService: [new SingleSignOnService(C::BINDING_HTTP_POST, C::LOCATION_A)], + protocolSupportEnumeration: [C::NS_SAMLP], + nameIDMappingService: [new SingleSignOnService(C::BINDING_HTTP_REDIRECT, C::LOCATION_B)], + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if AssertionIDRequestService endpoints passed have the + * wrong type. + */ + public function testMarshallingWithWrongAssertionIDRequestService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'All md:AssertionIDRequestService endpoints must be an instance of AssertionIDRequestService.', + ); + + /** @psalm-suppress InvalidArgument */ + new IDPSSODescriptor( + singleSignOnService: [new SingleSignOnService(C::BINDING_HTTP_POST, C::LOCATION_A)], + protocolSupportEnumeration: [C::NS_SAMLP], + nameIDMappingService: [], + assertionIDRequestService: [new SingleSignOnService(C::BINDING_HTTP_REDIRECT, C::LOCATION_B)], + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if an empty AttributeProfile is provided. + */ + public function testMarshallingWithEmptyAttributeProfile(): void + { + $this->expectException(SchemaViolationException::class); + new IDPSSODescriptor( + singleSignOnService: [new SingleSignOnService(C::BINDING_HTTP_POST, C::LOCATION_A)], + protocolSupportEnumeration: [C::NS_SAMLP], + attributeProfile: [new AttributeProfile('profile1'), new AttributeProfile('')], + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch fails if attributes passed have the wrong type. + */ + public function testMarshallingWithWrongAttributes(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('All md:Attribute elements must be an instance of Attribute.'); + + /** @psalm-suppress InvalidArgument */ + new IDPSSODescriptor( + singleSignOnService: [new SingleSignOnService(C::BINDING_HTTP_POST, C::LOCATION_A)], + protocolSupportEnumeration: [C::NS_SAMLP], + attribute: [new SingleSignOnService(C::BINDING_HTTP_REDIRECT, C::LOCATION_B)] + ); + } + + + /** + * Test that creating an IDPSSODescriptor from scratch works if no optional arguments are provided. + */ + public function testMarshallingWithoutOptionalArguments(): void + { + $idpssod = new IDPSSODescriptor( + [ + new SingleSignOnService(C::BINDING_HTTP_POST, C::LOCATION_A), + new SingleSignOnService(C::BINDING_HTTP_REDIRECT, C::LOCATION_B), + ], + [C::NS_SAMLP, C::PROTOCOL], + ); + $this->assertNull($idpssod->wantAuthnRequestsSigned()); + $this->assertEquals([], $idpssod->getNameIDMappingService()); + $this->assertEquals([], $idpssod->getAssertionIDRequestService()); + $this->assertEquals([], $idpssod->getSupportedAttribute()); + } + + + // test unmarshalling + + + /** + * Test creating an IDPSSODescriptor from XML. + */ + public function testUnmarshalling(): void + { + $idpssod = IDPSSODescriptor::fromXML(self::$xmlRepresentation->documentElement); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($idpssod), + ); + } + + + /** + * Test that creating an IDPSSODescriptor from XML fails if no SingleSignOnService endpoint is provided. + */ + public function testUnmarshallingWithoutSingleSignOnService(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $ssoServiceEps = $xmlRepresentation->getElementsByTagNameNS(C::NS_MD, 'SingleSignOnService'); + /** @psalm-suppress PossiblyNullArgument */ + $xmlRepresentation->documentElement->removeChild($ssoServiceEps->item(1)); + /** @psalm-suppress PossiblyNullArgument */ + $xmlRepresentation->documentElement->removeChild($ssoServiceEps->item(0)); + + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('At least one SingleSignOnService must be specified.'); + + IDPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an IDPSSODescriptor from XML fails if WantAuthnRequestsSigned is not boolean. + */ + public function testUnmarshallingWithWrongWantAuthnRequestsSigned(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $xmlRepresentation->documentElement->setAttribute('WantAuthnRequestsSigned', 'not a boolean'); + + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'The \'WantAuthnRequestsSigned\' attribute of md:IDPSSODescriptor must be a boolean.', + ); + + IDPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an IDPSSODescriptor from XML fails if an empty AttributeProfile is provided. + */ + public function testUnmarshallingWithEmptyAttributeProfile(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $attrProfiles = $xmlRepresentation->getElementsByTagNameNS(C::NS_MD, 'AttributeProfile'); + /** @psalm-suppress PossiblyNullPropertyAssignment */ + $attrProfiles->item(0)->textContent = ''; + + $this->expectException(SchemaViolationException::class); + + IDPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an IDPSSODescriptor from XML works if no optional elements are provided. + */ + public function testUnmarshallingWithoutOptionalArguments(): void + { + $mdns = C::NS_MD; + $document = DOMDocumentFactory::fromString(<< + + +XML + ); + $idpssod = IDPSSODescriptor::fromXML($document->documentElement); + $this->assertCount(1, $idpssod->getSingleSignOnService()); + $this->assertInstanceOf(SingleSignOnService::class, $idpssod->getSingleSignOnService()[0]); + $this->assertNull($idpssod->wantAuthnRequestsSigned()); + $this->assertEquals([], $idpssod->getNameIDMappingService()); + $this->assertEquals([], $idpssod->getAssertionIDRequestService()); + $this->assertEquals([], $idpssod->getAttributeProfile()); + $this->assertEquals([], $idpssod->getSupportedAttribute()); + } +} diff --git a/tests/SAML2/XML/md/RoleDescriptorMock.php b/tests/SAML2/XML/md/RoleDescriptorMock.php deleted file mode 100644 index 8c815f516..000000000 --- a/tests/SAML2/XML/md/RoleDescriptorMock.php +++ /dev/null @@ -1,30 +0,0 @@ -setAttributeNS(C::NS_XSI, 'xsi:type', 'myns:MyElement'); - $xml->setAttributeNS('http://example.org/mynsdefinition', 'myns:tmp', 'tmp'); - $xml->removeAttributeNS('http://example.org/mynsdefinition', 'tmp'); - return $xml; - } -} diff --git a/tests/SAML2/XML/md/RoleDescriptorTest.php b/tests/SAML2/XML/md/RoleDescriptorTest.php index 8a77cc701..0be052d02 100644 --- a/tests/SAML2/XML/md/RoleDescriptorTest.php +++ b/tests/SAML2/XML/md/RoleDescriptorTest.php @@ -4,51 +4,300 @@ namespace SimpleSAML\Test\SAML2\XML\md; +use DateTimeImmutable; +use DOMAttr; +use DOMDocument; use PHPUnit\Framework\TestCase; -use SimpleSAML\SAML2\Constants as C; -use SimpleSAML\SAML2\Utils; -use SimpleSAML\SAML2\Utils\XPath; +use SimpleSAML\Assert\AssertionFailedException; +use SimpleSAML\SAML2\Compat\AbstractContainer; +use SimpleSAML\SAML2\Compat\ContainerSingleton; +use SimpleSAML\SAML2\Exception\ProtocolViolationException; +use SimpleSAML\SAML2\XML\md\AbstractRoleDescriptor; +use SimpleSAML\SAML2\XML\md\Company; +use SimpleSAML\SAML2\XML\md\ContactPerson; +use SimpleSAML\SAML2\XML\md\EmailAddress; +use SimpleSAML\SAML2\XML\md\EncryptionMethod; +use SimpleSAML\SAML2\XML\md\Extensions; +use SimpleSAML\SAML2\XML\md\GivenName; use SimpleSAML\SAML2\XML\md\KeyDescriptor; +use SimpleSAML\SAML2\XML\md\Organization; +use SimpleSAML\SAML2\XML\md\OrganizationDisplayName; +use SimpleSAML\SAML2\XML\md\OrganizationName; +use SimpleSAML\SAML2\XML\md\OrganizationURL; +use SimpleSAML\SAML2\XML\md\SurName; +use SimpleSAML\SAML2\XML\md\TelephoneNumber; +use SimpleSAML\SAML2\XML\md\UnknownRoleDescriptor; +use SimpleSAML\SAML2\XML\saml\Audience; +use SimpleSAML\Test\SAML2\Constants as C; +use SimpleSAML\Test\SAML2\CustomRoleDescriptor; +use SimpleSAML\XML\Attribute as XMLAttribute; +use SimpleSAML\XML\Chunk; use SimpleSAML\XML\DOMDocumentFactory; +use SimpleSAML\XML\Exception\MissingAttributeException; +use SimpleSAML\XML\Exception\SchemaViolationException; +use SimpleSAML\XML\TestUtils\SchemaValidationTestTrait; +use SimpleSAML\XML\TestUtils\SerializableElementTestTrait; use SimpleSAML\XMLSecurity\XML\ds\KeyInfo; -use SimpleSAML\XMLSecurity\XML\ds\X509Certificate; -use SimpleSAML\XMLSecurity\XML\ds\X509Data; +use SimpleSAML\XMLSecurity\XML\ds\KeyName; -class RoleDescriptorTest extends TestCase +use function dirname; +use function strval; + +/** + * This is a test for the UnknownRoleDescriptor class. + * + * @covers \SimpleSAML\SAML2\XML\md\UnknownRoleDescriptor + * @covers \SimpleSAML\SAML2\XML\md\AbstractRoleDescriptor + * @covers \SimpleSAML\SAML2\XML\md\AbstractRoleDescriptorType + * @covers \SimpleSAML\SAML2\XML\md\AbstractMetadataDocument + * @covers \SimpleSAML\SAML2\XML\md\AbstractSignedMdElement + * @covers \SimpleSAML\SAML2\XML\md\AbstractMdElement + * + * @package simplesamlphp/saml2 + */ +final class RoleDescriptorTest extends TestCase { + use SchemaValidationTestTrait; + use SerializableElementTestTrait; + + + /** @var \SimpleSAML\SAML2\Compat\AbstractContainer */ + private static AbstractContainer $containerBackup; + + + /** + */ + public static function setUpBeforeClass(): void + { + self::$containerBackup = ContainerSingleton::getInstance(); + + self::$schemaFile = dirname(dirname(dirname(dirname(__FILE__)))) . '/resources/schemas/simplesamlphp.xsd'; + + self::$testedClass = AbstractRoleDescriptor::class; + + self::$xmlRepresentation = DOMDocumentFactory::fromFile( + dirname(dirname(dirname(dirname(__FILE__)))) . '/resources/xml/md_RoleDescriptor.xml', + ); + + $container = clone self::$containerBackup; + $container->registerExtensionHandler(CustomRoleDescriptor::class); + ContainerSingleton::setContainer($container); + } + + + /** + */ + public static function tearDownAfterClass(): void + { + ContainerSingleton::setContainer(self::$containerBackup); + } + + + // marshalling + + /** - * @return void */ public function testMarshalling(): void { - $roleDescriptor = new RoleDescriptorMock(); - $roleDescriptor->setID('SomeID'); - $roleDescriptor->setValidUntil(1234567890); - $roleDescriptor->setCacheDuration('PT5000S'); - $roleDescriptor->setProtocolSupportEnumeration([ - 'protocol1', - 'protocol2', - ]); - $roleDescriptor->setErrorURL('https://example.org/error'); - $kd = new KeyDescriptor(new KeyInfo([new X509Data([new X509Certificate( - '/CTj03d1DB5e2t7CTo9BEzCf5S9NRzwnBgZRlm32REI=' - )])])); - $roleDescriptor->setKeyDescriptor([$kd]); - - $document = DOMDocumentFactory::fromString(''); - $roleDescriptorElement = $roleDescriptor->toXML($document->firstChild); - - $xpCache = XPath::getXPath($roleDescriptorElement); - $roleDescriptorElement = XPath::xpQuery($roleDescriptorElement, '/root/md:RoleDescriptor', $xpCache); - $this->assertCount(1, $roleDescriptorElement); - /** @var \DOMElement $roleDescriptorElement */ - $roleDescriptorElement = $roleDescriptorElement[0]; - - $this->assertEquals('SomeID', $roleDescriptorElement->getAttribute("ID")); - $this->assertEquals('2009-02-13T23:31:30Z', $roleDescriptorElement->getAttribute("validUntil")); - $this->assertEquals('PT5000S', $roleDescriptorElement->getAttribute("cacheDuration")); - $this->assertEquals('protocol1 protocol2', $roleDescriptorElement->getAttribute("protocolSupportEnumeration")); - $this->assertEquals('myns:MyElement', $roleDescriptorElement->getAttributeNS(C::NS_XSI, "type")); - $this->assertEquals('http://example.org/mynsdefinition', $roleDescriptorElement->lookupNamespaceURI("myns")); + $attr_cp_1 = new XMLAttribute('urn:test:something', 'test', 'attr1', 'testval1'); + $attr_cp_2 = new XMLAttribute('urn:test:something', 'test', 'attr2', 'testval2'); + $attr_3 = new XMLAttribute('urn:x-simplesamlphp:namespace', 'ssp', 'phpunit', 'test'); + + $roleDescriptor = new CustomRoleDescriptor( + [ + new Chunk(DOMDocumentFactory::fromString( + 'Some' + )->documentElement) + ], + [C::NS_SAMLP, C::PROTOCOL], + 'TheID', + new DateTimeImmutable('2009-02-13T23:31:30Z'), + 'PT5000S', + new Extensions([new Chunk( + DOMDocumentFactory::fromString( + 'Some' + )->documentElement + )]), + 'https://error.reporting/', + [ + new KeyDescriptor( + new KeyInfo([new KeyName('IdentityProvider.com SSO Signing Key')]), + 'signing', + ), + new KeyDescriptor( + new KeyInfo([new KeyName('IdentityProvider.com SSO Encryption Key')]), + 'encryption', + [new EncryptionMethod(C::KEY_TRANSPORT_OAEP_MGF1P)], + ), + ], + new Organization( + [new OrganizationName('en', 'Identity Providers R US')], + [new OrganizationDisplayName('en', 'Identity Providers R US, a Division of Lerxst Corp.')], + [new OrganizationURL('en', 'https://IdentityProvider.com')], + ), + [ + new ContactPerson( + contactType: 'other', + company: new Company('Test Company'), + givenName: new GivenName('John'), + surName: new SurName('Doe'), + emailAddress: [ + new EmailAddress('mailto:jdoe@test.company'), + new EmailAddress('mailto:john.doe@test.company'), + ], + telephoneNumber: [new TelephoneNumber('1-234-567-8901')], + namespacedAttribute: [$attr_cp_1, $attr_cp_2], + ), + new ContactPerson( + contactType: 'technical', + telephoneNumber: [new TelephoneNumber('1-234-567-8901')], + ), + ], + [$attr_3], + ); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($roleDescriptor), + ); + } + + + // test unmarshalling + + + /** + * Test unmarshalling a known object as a RoleDescriptor. + */ + public function testUnmarshalling(): void + { + $descriptor = AbstractRoleDescriptor::fromXML(self::$xmlRepresentation->documentElement); + + $this->assertInstanceOf(CustomRoleDescriptor::class, $descriptor); + $this->assertCount(2, $descriptor->getKeyDescriptor()); + $this->assertInstanceOf(KeyDescriptor::class, $descriptor->getKeyDescriptor()[0]); + $this->assertInstanceOf(KeyDescriptor::class, $descriptor->getKeyDescriptor()[1]); + $this->assertEquals( + [C::NS_SAMLP, C::PROTOCOL], + $descriptor->getProtocolSupportEnumeration(), + ); + $this->assertInstanceOf(Organization::class, $descriptor->getOrganization()); + $this->assertCount(2, $descriptor->getContactPerson()); + $this->assertInstanceOf(ContactPerson::class, $descriptor->getContactPerson()[0]); + $this->assertInstanceOf(ContactPerson::class, $descriptor->getContactPerson()[1]); + $this->assertEquals('TheID', $descriptor->getID()); + $this->assertEquals('2009-02-13T23:31:30Z', $descriptor->getValidUntil()->format(C::DATETIME_FORMAT)); + $this->assertEquals('PT5000S', $descriptor->getCacheDuration()); + $this->assertEquals('https://error.reporting/', $descriptor->getErrorURL()); + + $extElement = $descriptor->getExtensions(); + $this->assertInstanceOf(Extensions::class, $extElement); + + $extensions = $extElement->getList(); + $this->assertCount(1, $extensions); + $this->assertInstanceOf(Chunk::class, $extensions[0]); + $this->assertEquals('urn:x-simplesamlphp:namespace', $extensions[0]->getNamespaceURI()); + $this->assertEquals('Chunk', $extensions[0]->getLocalName()); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($descriptor), + ); + } + + + /** + */ + public function testUnmarshallingUnregistered(): void + { + $element = clone self::$xmlRepresentation->documentElement; + $element->setAttributeNS( + 'http://www.w3.org/2000/xmlns/', + 'xmlns:ssp', + 'urn:x-simplesamlphp:namespace', + ); + + $type = new XMLAttribute(C::NS_XSI, 'xsi', 'type', 'ssp:UnknownRoleDescriptorType'); + $type->toXML($element); + + $descriptor = UnknownRoleDescriptor::fromXML($element); + + $this->assertCount(2, $descriptor->getKeyDescriptor()); + $this->assertInstanceOf(KeyDescriptor::class, $descriptor->getKeyDescriptor()[0]); + $this->assertInstanceOf(KeyDescriptor::class, $descriptor->getKeyDescriptor()[1]); + $this->assertEquals( + [C::NS_SAMLP, C::PROTOCOL], + $descriptor->getProtocolSupportEnumeration(), + ); + $this->assertInstanceOf(Organization::class, $descriptor->getOrganization()); + $this->assertCount(2, $descriptor->getContactPerson()); + $this->assertInstanceOf(ContactPerson::class, $descriptor->getContactPerson()[0]); + $this->assertInstanceOf(ContactPerson::class, $descriptor->getContactPerson()[1]); + $this->assertEquals('TheID', $descriptor->getID()); + $this->assertEquals('2009-02-13T23:31:30Z', $descriptor->getValidUntil()->format(C::DATETIME_FORMAT)); + $this->assertEquals('PT5000S', $descriptor->getCacheDuration()); + $this->assertEquals('https://error.reporting/', $descriptor->getErrorURL()); + + $chunk = $descriptor->getRawRoleDescriptor(); + $this->assertEquals('md', $chunk->getPrefix()); + $this->assertEquals('RoleDescriptor', $chunk->getLocalName()); + $this->assertEquals(C::NS_MD, $chunk->getNamespaceURI()); + + $extElement = $descriptor->getExtensions(); + $this->assertInstanceOf(Extensions::class, $extElement); + + $extensions = $extElement->getList(); + $this->assertCount(1, $extensions); + $this->assertInstanceOf(Chunk::class, $extensions[0]); + $this->assertEquals('urn:x-simplesamlphp:namespace', $extensions[0]->getNamespaceURI()); + $this->assertEquals('Chunk', $extensions[0]->getLocalName()); + + $this->assertEquals($element->ownerDocument->saveXML($element), strval($chunk)); + } + + + /** + * Test creating an UnknownRoleDescriptor from an XML that lacks supported protocols. + */ + public function testUnmarshallingWithoutSupportedProtocols(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $xmlRepresentation->documentElement->removeAttribute('protocolSupportEnumeration'); + + $this->expectException(MissingAttributeException::class); + $this->expectExceptionMessage( + 'Missing \'protocolSupportEnumeration\' attribute on md:RoleDescriptor.', + ); + + UnknownRoleDescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test creating an UnknownRoleDescriptor from an XML that lacks supported protocols. + */ + public function testUnmarshallingWithEmptySupportedProtocols(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $xmlRepresentation->documentElement->setAttribute('protocolSupportEnumeration', ''); + + $this->expectException(SchemaViolationException::class); + + UnknownRoleDescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an UnknownRoleDescriptor from XML fails if errorURL is not a valid URL. + */ + public function testUnmarshallingWithInvalidErrorURL(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $xmlRepresentation->documentElement->setAttribute('errorURL', 'not a URL'); + + $this->expectException(SchemaViolationException::class); + + UnknownRoleDescriptor::fromXML($xmlRepresentation->documentElement); } } diff --git a/tests/SAML2/XML/md/SPSSODescriptorTest.php b/tests/SAML2/XML/md/SPSSODescriptorTest.php new file mode 100644 index 000000000..0c95f224f --- /dev/null +++ b/tests/SAML2/XML/md/SPSSODescriptorTest.php @@ -0,0 +1,342 @@ +assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($spssod), + ); + } + + + /** + * Test that creating an SPSSODescriptor from scratch fails without an AssertionConsumerService. + */ + public function testMarshallingWithoutAssertionConsumerService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('At least one AssertionConsumerService must be specified.'); + + new SPSSODescriptor( + [], + [C::NS_SAMLP], + ); + } + + + /** + * Test that creating an SPSSODescriptor from scratch fails with an AssertionConsumerService of the wrong class. + */ + public function testMarshallingWithWrongAssertionConsumerService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'All md:AssertionConsumerService endpoints must be an instance of AssertionConsumerService.', + ); + + /** @psalm-suppress InvalidArgument */ + new SPSSODescriptor( + [new ArtifactResolutionService(0, C::BINDING_HTTP_POST, C::LOCATION_A)], + [C::NS_SAMLP], + ); + } + + + /** + * Test that creating an SPSSODescriptor from scratch fails with an AttributeConsumingService of the wrong class. + */ + public function testMarshallingWithWrongAttributeConsumingService(): void + { + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'All md:AttributeConsumingService endpoints must be an instance of AttributeConsumingService.', + ); + + /** @psalm-suppress InvalidArgument */ + new SPSSODescriptor( + assertionConsumerService: [new AssertionConsumerService(0, C::BINDING_HTTP_POST, C::LOCATION_A)], + protocolSupportEnumeration: [C::NS_SAMLP], + authnRequestsSigned: true, + attributeConsumingService: [new AssertionConsumerService(0, C::BINDING_HTTP_POST, C::LOCATION_B)], + ); + } + + + /** + * Test that creating an SPSSODescriptor from scratch works without any optional arguments. + */ + public function testMarshallingWithoutOptionalArguments(): void + { + $spssod = new SPSSODescriptor( + [new AssertionConsumerService(0, C::BINDING_HTTP_POST, C::LOCATION_A)], + [C::NS_SAMLP], + ); + $this->assertNull($spssod->getAuthnRequestsSigned()); + $this->assertNull($spssod->getWantAssertionsSigned()); + $this->assertEmpty($spssod->getAttributeConsumingService()); + } + + + // test unmarshalling + + + /** + * Test creating an SPSSODescriptor from XML. + */ + public function testUnmarshalling(): void + { + $spssod = SPSSODescriptor::fromXML(self::$xmlRepresentation->documentElement); + + $this->assertEquals( + self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement), + strval($spssod), + ); + } + + + /** + * Test that creating an SPSSODescriptor from XML fails if no AssertionConsumerService is specified. + */ + public function testUnmarshallingWithoutAssertionConsumerService(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $acseps = $xmlRepresentation->getElementsByTagNameNS(C::NS_MD, 'AssertionConsumerService'); + + /** @psalm-suppress PossiblyNullArgument */ + $xmlRepresentation->documentElement->removeChild($acseps->item(1)); + + /** @psalm-suppress PossiblyNullArgument */ + $xmlRepresentation->documentElement->removeChild($acseps->item(0)); + + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('At least one AssertionConsumerService must be specified.'); + + SPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an SPSSODescriptor from XML fails if AuthnRequestsSigned is not boolean. + */ + public function testUnmarshallingWithNonBooleanAuthnRequestsSigned(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $xmlRepresentation->documentElement->setAttribute('AuthnRequestsSigned', 'not a boolean'); + + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('The \'AuthnRequestsSigned\' attribute of md:SPSSODescriptor must be a boolean.'); + + SPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an SPSSODescriptor from XML fails if WantAssertionsSigned is not boolean. + */ + public function testUnmarshallingWithNonBooleanWantAssertionsSigned(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $xmlRepresentation->documentElement->setAttribute('WantAssertionsSigned', 'not a boolean'); + + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage( + 'The \'WantAssertionsSigned\' attribute of md:SPSSODescriptor must be a boolean.' + ); + + SPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } + + + /** + * Test that creating an SPSSODescriptor from XML without any optional elements works. + */ + public function testUnmarshallingWithoutOptionalArguments(): void + { + $mdns = C::NS_MD; + $document = DOMDocumentFactory::fromString(<< + + +XML + ); + + $spssod = SPSSODescriptor::fromXML($document->documentElement); + $this->assertNull($spssod->getAuthnRequestsSigned()); + $this->assertNull($spssod->getWantAssertionsSigned()); + $this->assertEmpty($spssod->getAttributeConsumingService()); + } + + + /** + * Test that creating an SPSSODescriptor from XML fails when more than one AttributeConsumingService is set to be + * the default. + */ + public function testUnmarshallingTwoDefaultACS(): void + { + $xmlRepresentation = clone self::$xmlRepresentation; + $acs = $xmlRepresentation->getElementsByTagNameNS(C::NS_MD, 'AttributeConsumingService'); + /** @psalm-suppress PossiblyNullReference */ + $acs->item(1)->setAttribute('isDefault', 'true'); + + $this->expectException(AssertionFailedException::class); + $this->expectExceptionMessage('Only one md:AttributeConsumingService can be set as default.'); + + SPSSODescriptor::fromXML($xmlRepresentation->documentElement); + } +}