Passed
Push — master ( e90e44...21bab2 )
by Tim
01:51
created

AbstractAssertionType::setOriginalXML()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\SAML11\XML\saml;
6
7
use DateTimeImmutable;
8
use DOMElement;
9
use SimpleSAML\Assert\Assert;
10
use SimpleSAML\SAML11\Compat\ContainerSingleton;
11
use SimpleSAML\SAML11\Constants as C;
12
use SimpleSAML\SAML11\Exception\ProtocolViolationException;
13
use SimpleSAML\SAML11\Utils\XPath;
14
use SimpleSAML\XML\Exception\InvalidDOMElementException;
15
use SimpleSAML\XML\Exception\MissingElementException;
16
use SimpleSAML\XML\Exception\SchemaViolationException;
17
use SimpleSAML\XML\Exception\TooManyElementsException;
18
use SimpleSAML\XMLSecurity\XML\ds\Signature;
19
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
20
use SimpleSAML\XMLSecurity\XML\SignableElementTrait;
21
use SimpleSAML\XMLSecurity\XML\SignedElementInterface;
22
use SimpleSAML\XMLSecurity\XML\SignedElementTrait;
23
24
use function array_filter;
25
use function array_merge;
26
use function array_pop;
27
use function array_values;
28
use function preg_replace;
29
30
/**
31
 * SAML Assertion Type abstract data type.
32
 *
33
 * @package simplesamlphp/saml11
34
 */
35
abstract class AbstractAssertionType extends AbstractSamlElement implements
36
    SignableElementInterface,
37
    SignedElementInterface
38
{
39
    use SignableElementTrait;
40
    use SignedElementTrait;
41
42
43
    /**
44
     * The original signed XML
45
     *
46
     * @var \DOMElement
47
     */
48
    protected DOMElement $xml;
49
50
51
    /**
52
     * Initialize a saml:AssertionType from scratch
53
     *
54
     * @param string $assertionID
55
     * @param string $issuer
56
     * @param \DateTimeImmutable $issueInstant
57
     * @param \SimpleSAML\SAML11\XML\saml\Conditions|null $conditions
58
     * @param \SimpleSAML\SAML11\XML\saml\Advice|null $advice
59
     * @param array<\SimpleSAML\SAML11\XML\saml\AbstractStatementType> $statements
60
     */
61
    final public function __construct(
62
        protected string $assertionID,
63
        protected string $issuer,
64
        protected DateTimeImmutable $issueInstant,
65
        protected ?Conditions $conditions = null,
66
        protected ?Advice $advice = null,
67
        protected array $statements = [],
68
    ) {
69
        Assert::same($issueInstant->getTimeZone()->getName(), 'Z', ProtocolViolationException::class);
70
        Assert::validNCName($assertionID, SchemaViolationException::class);
71
        Assert::minCount($statements, 1, MissingElementException::class);
72
        Assert::maxCount($statements, C::UNBOUNDED_LIMIT);
73
        Assert::allIsInstanceOf($statements, AbstractStatementType::class, SchemaViolationException::class);
74
    }
75
76
77
    /**
78
     * Collect the value of the assertionID-property
79
     *
80
     * Note: the name of this method is not consistent, but it has to be named getId for xml-security to work.
81
     *
82
     * @return string
83
     */
84
    public function getId(): string
85
    {
86
        return $this->assertionID;
87
    }
88
89
90
    /**
91
     * Collect the value of the issuer-property
92
     *
93
     * @return string
94
     */
95
    public function getIssuer(): string
96
    {
97
        return $this->issuer;
98
    }
99
100
101
    /**
102
     * Collect the value of the issueInstant-property
103
     *
104
     * @return \DateTimeImmutable
105
     */
106
    public function getIssueInstant(): DateTimeImmutable
107
    {
108
        return $this->issueInstant;
109
    }
110
111
112
    /**
113
     * Collect the value of the conditions-property
114
     *
115
     * @return \SimpleSAML\SAML11\XML\saml\Conditions|null
116
     */
117
    public function getConditions(): ?Conditions
118
    {
119
        return $this->conditions;
120
    }
121
122
123
    /**
124
     * Collect the value of the advice-property
125
     *
126
     * @return \SimpleSAML\SAML11\XML\saml\Advice|null
127
     */
128
    public function getAdvice(): ?Advice
129
    {
130
        return $this->advice;
131
    }
132
133
134
    /**
135
     * Collect the value of the statements-property
136
     *
137
     * @return array<\SimpleSAML\SAML11\XML\saml\AbstractStatementType>
138
     */
139
    public function getAllStatements(): array
140
    {
141
        return $this->statements;
142
    }
143
144
145
    /**
146
     * @return \SimpleSAML\SAML11\XML\saml\AbstractStatement[]
147
     */
148
    public function getStatements(): array
149
    {
150
        return array_values(array_filter($this->statements, function ($statement) {
151
            return $statement instanceof AbstractStatement;
152
        }));
153
    }
154
155
156
    /**
157
     * @return \SimpleSAML\SAML11\XML\saml\AbstractSubjectStatement[]
158
     */
159
    public function getSubjectStatements(): array
160
    {
161
        return array_values(array_filter($this->statements, function ($statement) {
162
            return $statement instanceof AbstractSubjectStatement;
163
        }));
164
    }
165
166
167
    /**
168
     * @return \SimpleSAML\SAML11\XML\saml\AuthenticationStatement[]
169
     */
170
    public function getAuthenticationStatements(): array
171
    {
172
        return array_values(array_filter($this->statements, function ($statement) {
173
            return $statement instanceof AuthenticationStatement;
174
        }));
175
    }
176
177
178
    /**
179
     * @return \SimpleSAML\SAML11\XML\saml\AuthorizationDecisionStatement[]
180
     */
181
    public function getAuthorizationStatements(): array
182
    {
183
        return array_values(array_filter($this->statements, function ($statement) {
184
            return $statement instanceof AuthorizationDecisionStatement;
185
        }));
186
    }
187
188
189
    /**
190
     * @return \SimpleSAML\SAML11\XML\saml\AttributeStatement[]
191
     */
192
    public function getAttributeStatements(): array
193
    {
194
        return array_values(array_filter($this->statements, function ($statement) {
195
            return $statement instanceof AttributeStatement;
196
        }));
197
    }
198
199
200
    /**
201
     * Set the XML element.
202
     *
203
     * @param \DOMElement $xml
204
     */
205
    private function setOriginalXML(DOMElement $xml): void
206
    {
207
        $this->xml = $xml;
208
    }
209
210
211
    /**
212
     * @return \DOMElement
213
     */
214
    protected function getOriginalXML(): DOMElement
215
    {
216
        return $this->xml ?? $this->toUnsignedXML();
217
    }
218
219
220
    public function getBlacklistedAlgorithms(): ?array
221
    {
222
        $container = ContainerSingleton::getInstance();
223
        return $container->getBlacklistedEncryptionAlgorithms();
224
    }
225
226
227
    /**
228
     * Convert XML into an AssertionType
229
     *
230
     * @param \DOMElement $xml The XML element we should load
231
     * @return static
232
     *
233
     * @throws \SimpleSAML\XML\Exception\InvalidDOMElementException
234
     *   if the qualified name of the supplied element is wrong
235
     */
236
    public static function fromXML(DOMElement $xml): static
237
    {
238
        Assert::same($xml->localName, static::getLocalName(), InvalidDOMElementException::class);
239
        Assert::same($xml->namespaceURI, static::NS, InvalidDOMElementException::class);
240
241
        Assert::same(self::getIntegerAttribute($xml, 'MajorVersion'), 1, 'Unsupported major version: %s');
242
        Assert::same(self::getIntegerAttribute($xml, 'MinorVersion'), 1, 'Unsupported minor version: %s');
243
244
        $assertionID = self::getAttribute($xml, 'AssertionID');
245
        Assert::validNCName($assertionID); // Covers the empty string
246
247
        $issueInstant = self::getAttribute($xml, 'IssueInstant');
248
        // Strip sub-seconds - See paragraph 1.2.2 of SAML core specifications
249
        $issueInstant = preg_replace('/([.][0-9]+Z)$/', 'Z', $issueInstant, 1);
250
251
        Assert::validDateTimeZulu($issueInstant, ProtocolViolationException::class);
252
        $issueInstant = new DateTimeImmutable($issueInstant);
253
254
        $conditions = Conditions::getChildrenOfClass($xml);
255
        Assert::maxCount(
256
            $conditions,
257
            1,
258
            'More than one <saml:Conditions> in <saml:Assertion>.',
259
            TooManyElementsException::class,
260
        );
261
262
        $advice = Advice::getChildrenOfClass($xml);
263
        Assert::maxCount(
264
            $advice,
265
            1,
266
            'More than one <saml:Advice> in <saml:Assertion>.',
267
            TooManyElementsException::class,
268
        );
269
270
        $statements = AbstractStatement::getChildrenOfClass($xml);
271
        $subjectStatement = AbstractSubjectStatement::getChildrenOfClass($xml);
272
        $authnStatement = AuthenticationStatement::getChildrenOfClass($xml);
273
        $authzDecisionStatement = AuthorizationDecisionStatement::getChildrenOfClass($xml);
274
        $attrStatement = AttributeStatement::getChildrenOfClass($xml);
275
276
        $signature = Signature::getChildrenOfClass($xml);
277
        Assert::maxCount($signature, 1, 'Only one <ds:Signature> element is allowed.', TooManyElementsException::class);
278
279
        $assertion = new static(
280
            $assertionID,
281
            self::getAttribute($xml, 'Issuer'),
282
            $issueInstant,
283
            array_pop($conditions),
284
            array_pop($advice),
285
            array_merge($statements, $subjectStatement, $authnStatement, $authzDecisionStatement, $attrStatement),
286
        );
287
288
        if (!empty($signature)) {
289
            $assertion->setSignature($signature[0]);
290
            $assertion->setOriginalXML($xml);
291
        }
292
293
        return $assertion;
294
    }
295
296
297
    /**
298
     * Convert this assertion to an unsigned XML document.
299
     * This method does not sign the resulting XML document.
300
     *
301
     * @return \DOMElement The root element of the DOM tree
302
     */
303
    protected function toUnsignedXML(?DOMElement $parent = null): DOMElement
304
    {
305
        $e = $this->instantiateParentElement($parent);
306
307
        $e->setAttribute('MajorVersion', '1');
308
        $e->setAttribute('MinorVersion', '1');
309
        $e->setAttribute('AssertionID', $this->getId());
310
        $e->setAttribute('Issuer', $this->getIssuer());
311
        $e->setAttribute('IssueInstant', $this->getIssueInstant()->format(C::DATETIME_FORMAT));
312
313
        $this->getConditions()?->toXML($e);
314
        $this->getAdvice()?->toXML($e);
315
316
        foreach ($this->getAllStatements() as $statement) {
317
            $statement->toXML($e);
318
        }
319
320
        return $e;
321
    }
322
323
324
    /**
325
     * Convert this assertion to a signed XML element, if a signer was set.
326
     *
327
     * @param \DOMElement|null $parent The DOM node the assertion should be created in.
328
     *
329
     * @return \DOMElement This assertion.
330
     * @throws \Exception
331
     */
332
    public function toXML(DOMElement $parent = null): DOMElement
333
    {
334
        if ($this->isSigned() === true && $this->signer === null) {
335
            // We already have a signed document and no signer was set to re-sign it
336
            if ($parent === null) {
337
                return $this->getOriginalXML();
338
            }
339
340
            $node = $parent->ownerDocument?->importNode($this->getOriginalXML(), true);
341
            $parent->appendChild($node);
342
            return $parent;
343
        }
344
345
        $e = $this->toUnsignedXML($parent);
346
347
        if ($this->signer !== null) {
348
            $signedXML = $this->doSign($e);
349
350
            // Test for last element, if any
351
            $assertionElements = XPath::xpQuery(
352
                $signedXML,
353
                './saml_assertion/following-sibling::*[position() = last()]',
354
                XPath::getXPath($signedXML),
355
            );
356
            $last = array_pop($assertionElements);
357
358
            if ($last !== null) {
359
                $signedXML->insertBefore($this->signature?->toXML($signedXML), $last->nextSibling);
360
            } else {
361
                $signedXML->appendChild($this->signature?->toXML($signedXML));
362
            }
363
364
            return $signedXML;
365
        }
366
367
        return $e;
368
    }
369
}
370