Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,8 @@
"scripts": {
"pre-commit": [
"vendor/bin/phpcs -p",
"vendor/bin/composer-require-checker check composer.json",
"vendor/bin/phpstan analyze -c phpstan.neon",
"vendor/bin/phpstan analyze -c phpstan-dev.neon",
"vendor/bin/phpstan analyze -c phpstan.neon --memory-limit=1024M",
"vendor/bin/phpstan analyze -c phpstan-dev.neon --memory-limit=1024M",
"vendor/bin/composer-unused",
"vendor/bin/phpunit --no-coverage --testdox"
]
Expand Down
35 changes: 35 additions & 0 deletions src/XML/fed/AbstractWebServiceDescriptorType.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
use SimpleSAML\SAML2\XML\md\AbstractRoleDescriptor;
use SimpleSAML\SAML2\XML\md\Extensions;
use SimpleSAML\SAML2\XML\md\Organization;
use SimpleSAML\XML\Attribute as XMLAttribute;
use SimpleSAML\XMLSchema\Constants as C;
use SimpleSAML\XMLSchema\Type\DurationValue;
use SimpleSAML\XMLSchema\Type\IDValue;
use SimpleSAML\XMLSchema\Type\QNameValue;
Expand All @@ -23,6 +25,24 @@
*/
abstract class AbstractWebServiceDescriptorType extends AbstractRoleDescriptor
{
/**
* The element is md:RoleDescriptor, but its content model lives in the WS-Federation schema — that is
* what the xsi:type points at. Validating against the inherited metadata schema alone can never resolve
* it, because md:RoleDescriptorType is abstract. ws-federation.xsd imports the metadata namespace.
*/
public const string SCHEMA = 'resources/schemas/ws-federation.xsd';

/**
* The exclusions for the xs:anyAttribute element
*
* xsi:type is modeled by AbstractRoleDescriptor as $type and returned by getXsiType(); without this
* exclusion it would also be swept into the extendable-attributes bucket and written a second time.
*/
public const array XS_ANY_ATTR_EXCLUSIONS = [
[C::NS_XSI, 'type'],
];


/**
* WebServiceDescriptorType constructor.
*
Expand Down Expand Up @@ -198,6 +218,21 @@ public function toUnsignedXML(?DOMElement $parent = null): DOMElement
{
$e = parent::toUnsignedXML($parent);

// md:RoleDescriptor requires an xsi:type. AbstractRoleDescriptor stores it and demands it back in
// fromXML(), but nothing ever writes it, so the element cannot round-trip its own output.
//
// Re-express it with this type's own prefix rather than the caller's: a QName is identified by its
// {namespace, local name}, so the prefix is lexical only and the value keeps its meaning — and this
// is the one prefix AbstractSignedMdElement::toXML() is guaranteed to declare for us. A caller's
// prefix, or none at all, would otherwise be left unbound and the QName unresolvable.
$xsiType = QNameValue::fromParts(
$this->getXsiType()->getLocalName(),
$this->getXsiType()->getNamespaceURI(),
static::getXsiTypePrefix(),
);

(new XMLAttribute(C::NS_XSI, 'xsi', 'type', $xsiType))->toXML($e);

$this->getLogicalServiceNamesOffered()?->toXML($e);
$this->getTokenTypesOffered()?->toXML($e);
$this->getClaimDialectsOffered()?->toXML($e);
Expand Down
207 changes: 207 additions & 0 deletions tests/Federation/XML/fed/SecurityTokenServiceTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Test\WebServices\Federation\XML\fed;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;
use SimpleSAML\SAML2\Type\SAMLAnyURIListValue;
use SimpleSAML\SAML2\Type\SAMLStringValue;
use SimpleSAML\WebServices\Addressing\XML\wsa_200508\Address;
use SimpleSAML\WebServices\Addressing\XML\wsa_200508\EndpointReference;
use SimpleSAML\WebServices\Federation\Constants as C;
use SimpleSAML\WebServices\Federation\XML\fed\AbstractSecurityTokenServiceType;
use SimpleSAML\WebServices\Federation\XML\fed\AbstractWebServiceDescriptorType;
use SimpleSAML\WebServices\Federation\XML\fed\PassiveRequestorEndpoint;
use SimpleSAML\WebServices\Federation\XML\fed\SecurityTokenServiceEndpoint;
use SimpleSAML\WebServices\Federation\XML\fed\SecurityTokenServiceType;
use SimpleSAML\XML\DOMDocumentFactory;
use SimpleSAML\XML\TestUtils\SchemaValidationTestTrait;
use SimpleSAML\XML\TestUtils\SerializableElementTestTrait;
use SimpleSAML\XMLSchema\Constants as C_XSI;
use SimpleSAML\XMLSchema\Type\AnyURIValue;
use SimpleSAML\XMLSchema\Type\NCNameValue;
use SimpleSAML\XMLSchema\Type\QNameValue;

use function dirname;
use function strval;

/**
* Tests for fed:SecurityTokenServiceType.
*
* @package simplesamlphp/xml-ws-federation
*/
#[Group('fed')]
#[CoversClass(SecurityTokenServiceType::class)]
#[CoversClass(AbstractSecurityTokenServiceType::class)]
#[CoversClass(AbstractWebServiceDescriptorType::class)]
final class SecurityTokenServiceTypeTest extends TestCase
{
use SchemaValidationTestTrait;
use SerializableElementTestTrait;


/**
*/
public static function setUpBeforeClass(): void
{
self::$testedClass = SecurityTokenServiceType::class;

self::$xmlRepresentation = DOMDocumentFactory::FromFile(
dirname(__FILE__, 4) . '/resources/xml/fed/SecurityTokenServiceType.xml',
);
}


/**
* Build the object used by the tests below.
*/
private static function buildSecurityTokenServiceType(): SecurityTokenServiceType
{
return new SecurityTokenServiceType(
QNameValue::fromParts(
NCNameValue::fromString(AbstractSecurityTokenServiceType::XSI_TYPE_NAME),
AnyURIValue::fromString(AbstractSecurityTokenServiceType::XSI_TYPE_NAMESPACE),
NCNameValue::fromString(AbstractSecurityTokenServiceType::XSI_TYPE_PREFIX),
),
SAMLAnyURIListValue::fromString(C::NS_FED),
serviceDisplayName: SAMLStringValue::fromString('SimpleSAMLphp ADFS IdP'),
securityTokenServiceEndpoint: [
new SecurityTokenServiceEndpoint([
new EndpointReference(
new Address(AnyURIValue::fromString('https://idp.example.org/adfs/services/trust')),
),
]),
],
passiveRequestorEndpoint: [
new PassiveRequestorEndpoint([
new EndpointReference(
new Address(AnyURIValue::fromString('https://idp.example.org/adfs/ls/')),
),
]),
],
);
}


// test marshalling


/**
* Test creating a SecurityTokenServiceType object from scratch.
*/
public function testMarshalling(): void
{
$this->assertEquals(
self::$xmlRepresentation->saveXML(self::$xmlRepresentation->documentElement),
strval(self::buildSecurityTokenServiceType()),
);
}


/**
* md:RoleDescriptor carries its element type in xsi:type, and AbstractRoleDescriptor::fromXML()
* rejects the element outright when it is absent. Serialising has to emit it.
*/
public function testMarshallingWritesXsiType(): void
{
$element = self::buildSecurityTokenServiceType()->toXML();

$this->assertEquals(
'fed:SecurityTokenServiceType',
$element->getAttributeNS(C_XSI::NS_XSI, 'type'),
);

// The prefix used inside the attribute value only counts if it is bound on the element.
$this->assertEquals(C::NS_FED, $element->lookupNamespaceURI('fed'));
}


/**
* The element must be able to read back what it writes. Before the xsi:type was emitted this threw
* a SchemaViolationException on the object's own output.
*/
public function testRoundTrip(): void
{
$original = self::buildSecurityTokenServiceType();

$this->assertEquals(
strval($original),
strval(SecurityTokenServiceType::fromXML($original->toXML())),
);
}


/**
* An xsi:type may legally be unprefixed, resolving through the document's default namespace. The
* element must not lose that namespace on the way out — a default namespace cannot be re-declared
* here without capturing unqualified descendants, so the type is re-expressed with its own prefix.
*/
public function testUnprefixedXsiTypeKeepsItsNamespace(): void
{
$xml = <<<XML
<md:RoleDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
xmlns="http://docs.oasis-open.org/wsfed/federation/200706"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:wsa10="http://www.w3.org/2005/08/addressing"
xsi:type="SecurityTokenServiceType"
protocolSupportEnumeration="http://docs.oasis-open.org/wsfed/federation/200706">
<SecurityTokenServiceEndpoint>
<wsa10:EndpointReference>
<wsa10:Address>https://idp.example.org/adfs/services/trust</wsa10:Address>
</wsa10:EndpointReference>
</SecurityTokenServiceEndpoint>
</md:RoleDescriptor>
XML;

$parsed = SecurityTokenServiceType::fromXML(
DOMDocumentFactory::fromString($xml)->documentElement,
);
$this->assertNull($parsed->getXsiType()->getNamespacePrefix());

$element = $parsed->toXML();
$this->assertEquals(
'fed:SecurityTokenServiceType',
$element->getAttributeNS(C_XSI::NS_XSI, 'type'),
);

// No default namespace may be introduced, or unqualified descendants would change meaning.
$this->assertNull($element->lookupNamespaceURI(null));

// What matters is that the QName still denotes the same {namespace, local name}.
$this->assertEquals(
C::NS_FED,
SecurityTokenServiceType::fromXML($element)->getXsiType()->getNamespaceURI()->getValue(),
);
}


// test unmarshalling


/**
* Test creating a SecurityTokenServiceType object from XML.
*/
public function testUnmarshalling(): void
{
$securityTokenServiceType = SecurityTokenServiceType::fromXML(
self::$xmlRepresentation->documentElement,
);

$this->assertEquals(
'fed:SecurityTokenServiceType',
strval($securityTokenServiceType->getXsiType()),
);

// xsi:type is modelled as the element's own type, not as one of its extendable attributes.
$this->assertEmpty($securityTokenServiceType->getAttributesNS());
$this->assertCount(1, $securityTokenServiceType->getSecurityTokenServiceEndpoint());
$this->assertCount(1, $securityTokenServiceType->getPassiveRequestorEndpoint());
$this->assertEquals(
'SimpleSAMLphp ADFS IdP',
strval($securityTokenServiceType->getServiceDisplayName()),
);
}
}
13 changes: 13 additions & 0 deletions tests/resources/xml/fed/SecurityTokenServiceType.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<md:RoleDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706" xmlns:wsa10="http://www.w3.org/2005/08/addressing" protocolSupportEnumeration="http://docs.oasis-open.org/wsfed/federation/200706" xsi:type="fed:SecurityTokenServiceType" ServiceDisplayName="SimpleSAMLphp ADFS IdP">
<fed:SecurityTokenServiceEndpoint xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706">
<wsa10:EndpointReference xmlns:wsa10="http://www.w3.org/2005/08/addressing">
<wsa10:Address>https://idp.example.org/adfs/services/trust</wsa10:Address>
</wsa10:EndpointReference>
</fed:SecurityTokenServiceEndpoint>
<fed:PassiveRequestorEndpoint xmlns:fed="http://docs.oasis-open.org/wsfed/federation/200706">
<wsa10:EndpointReference xmlns:wsa10="http://www.w3.org/2005/08/addressing">
<wsa10:Address>https://idp.example.org/adfs/ls/</wsa10:Address>
</wsa10:EndpointReference>
</fed:PassiveRequestorEndpoint>
</md:RoleDescriptor>