Passed
Push — master ( 30e0bc...991e3c )
by Tim
05:06 queued 02:27
created

AttributeServer::filterAttributeValues()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 13
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\exampleattributeserver\Controller;
6
7
use DateInterval;
8
use Nyholm\Psr7\Factory\Psr17Factory;
9
use SimpleSAML\{Configuration, Error, Logger};
10
use SimpleSAML\HTTP\RunnableResponse;
11
use SimpleSAML\Metadata\MetaDataStorageHandler;
12
use SimpleSAML\SAML2\Binding;
13
use SimpleSAML\SAML2\Binding\HTTPPost;
14
use SimpleSAML\SAML2\Constants as C;
15
use SimpleSAML\SAML2\Utils as SAML2_Utils;
16
use SimpleSAML\SAML2\XML\saml\{
17
    Assertion,
18
    Attribute,
19
    AttributeStatement,
20
    AttributeValue,
21
    Audience,
22
    AudienceRestriction,
23
    Conditions,
24
    Issuer,
25
    Subject,
26
    SubjectConfirmation,
27
    SubjectConfirmationData,
28
};
29
use SimpleSAML\SAML2\XML\samlp\{AttributeQuery, Response, Status, StatusCode};
30
use SimpleSAML\Utils;
31
use SimpleSAML\XML\Utils\Random;
32
use SimpleSAML\XMLSecurity\Alg\Signature\SignatureAlgorithmFactory;
33
use SimpleSAML\XMLSecurity\Key\PrivateKey;
34
use SimpleSAML\XMLSecurity\XML\ds\{KeyInfo, X509Certificate, X509Data};
35
use SimpleSAML\XMLSecurity\XML\SignableElementInterface;
36
use Symfony\Bridge\PsrHttpMessage\Factory\{HttpFoundationFactory, PsrHttpFactory};
37
use Symfony\Component\HttpFoundation\Request;
38
39
use function array_filter;
40
41
/**
42
 * Controller class for the exampleattributeserver module.
43
 *
44
 * This class serves the attribute server available in the module.
45
 *
46
 * @package SimpleSAML\Module\exampleattributeserver
47
 */
48
class AttributeServer
49
{
50
    /** @var \SimpleSAML\Configuration */
51
    protected Configuration $config;
52
53
    /** @var \SimpleSAML\Metadata\MetaDataStorageHandler|null */
54
    protected ?MetaDataStorageHandler $metadataHandler = null;
55
56
57
    /**
58
     * ConfigController constructor.
59
     *
60
     * @param \SimpleSAML\Configuration $config The configuration to use.
61
     */
62
    public function __construct(Configuration $config)
63
    {
64
        $this->config = $config;
65
    }
66
67
68
    /**
69
     * Inject the \SimpleSAML\Metadata\MetaDataStorageHandler dependency.
70
     *
71
     * @param \SimpleSAML\Metadata\MetaDataStorageHandler $handler
72
     */
73
    public function setMetadataStorageHandler(MetaDataStorageHandler $handler): void
74
    {
75
        $this->metadataHandler = $handler;
76
    }
77
78
79
    /**
80
     * @param \Symfony\Component\HttpFoundation\Request $request The current request.
81
     *
82
     * @return \SimpleSAML\HTTP\RunnableResponse
83
     * @throws \SimpleSAML\Error\BadRequest
84
     */
85
    public function main(/** @scrutinizer ignore-unused */ Request $request): RunnableResponse
86
    {
87
        $psr17Factory = new Psr17Factory();
88
        $psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
89
        $psrRequest = $psrHttpFactory->createRequest($request);
90
91
        $binding = Binding::getCurrentBinding($psrRequest);
92
        $message = $binding->receive($psrRequest);
93
        if (!($message instanceof AttributeQuery)) {
94
            throw new Error\BadRequest('Invalid message received to AttributeQuery endpoint.');
95
        }
96
97
        $idpEntityId = $this->metadataHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');
0 ignored issues
show
Bug introduced by
The method getMetaDataCurrentEntityID() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

97
        /** @scrutinizer ignore-call */ 
98
        $idpEntityId = $this->metadataHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
98
99
        $issuer = $message->getIssuer();
100
        if ($issuer === null) {
101
            throw new Error\BadRequest('Missing <saml:Issuer> in <samlp:AttributeQuery>.');
102
        } else {
103
            $spEntityId = $issuer->getContent();
104
            if ($spEntityId === '') {
105
                throw new Error\BadRequest('Empty <saml:Issuer> in <samlp:AttributeQuery>.');
106
            }
107
        }
108
109
        $idpMetadata = $this->metadataHandler->getMetaDataConfig($idpEntityId, 'saml20-idp-hosted');
110
        $spMetadata = $this->metadataHandler->getMetaDataConfig($spEntityId, 'saml20-sp-remote');
111
112
        // The endpoint we should deliver the message to
113
        $endpoint = $spMetadata->getString('testAttributeEndpoint');
114
115
        // The attributes we will return
116
        $attributes = [
117
            new Attribute(
118
                'name',
119
                C::NAMEFORMAT_UNSPECIFIED,
120
                null,
121
                [
122
                    new AttributeValue('value1'),
123
                    new AttributeValue('value2'),
124
                    new AttributeValue('value3'),
125
                ],
126
            ),
127
            new Attribute(
128
                'test',
129
                C::NAMEFORMAT_UNSPECIFIED,
130
                null,
131
                [
132
                    new AttributeValue('test'),
133
                ],
134
            ),
135
        ];
136
137
        // Determine which attributes we will return
138
        // @phpstan-ignore identical.alwaysFalse
139
        if (count($attributes) === 0) {
140
            Logger::debug('No attributes requested - return all attributes.');
141
            $attributeStatement = null;
142
        } else {
143
            $returnAttributes = [];
144
            foreach ($message->getAttributes() as $reqAttr) {
145
                foreach ($attributes as $attr) {
146
                    if (
147
                        $attr->getName() === $reqAttr->getName()
148
                        && $attr->getNameFormat() === $reqAttr->getNameFormat()
149
                    ) {
150
                        // The requested attribute is available
151
                        if ($reqAttr->getAttributeValues() === []) {
152
                            // If no specific values are requested, return all
153
                            $returnAttributes[] = $attr;
154
                        } else {
155
                            $returnValues = $this->filterAttributeValues(
156
                                $reqAttr->getAttributeValues(),
157
                                $attr->getAttributeValues(),
158
                            );
159
160
                            $returnAttributes[] = new Attribute(
161
                                $attr->getName(),
162
                                $attr->getNameFormat(),
163
                                null,
164
                                $returnValues,
165
                                $attr->getAttributesNS(),
166
                            );
167
                        }
168
                    }
169
                }
170
            }
171
172
            $attributeStatement = $returnAttributes ? (new AttributeStatement($returnAttributes)) : null;
173
        }
174
175
        // $returnAttributes contains the attributes we should return. Send them
176
        $clock = SAML2_Utils::getContainer()->getClock();
177
178
        $statements = array_filter([$attributeStatement]);
179
        $assertion = new Assertion(
180
            issuer: new Issuer($idpEntityId),
181
            issueInstant: $clock->now(),
182
            id: (new Random())->generateID(),
183
            subject: new Subject(
184
                identifier: $message->getSubject()->getIdentifier(),
185
                subjectConfirmation: [
186
                    new SubjectConfirmation(
187
                        method: C::CM_BEARER,
188
                        subjectConfirmationData: new SubjectConfirmationData(
189
                            notOnOrAfter: $clock->now()->add(new DateInterval('PT300S')),
190
                            recipient: $endpoint,
191
                            inResponseTo: $message->getId(),
192
                        ),
193
                    ),
194
                ],
195
            ),
196
            conditions: new Conditions(
197
                notBefore: $clock->now(),
198
                notOnOrAfter: $clock->now()->add(new DateInterval('PT300S')),
199
                audienceRestriction: [
200
                    new AudienceRestriction([
201
                        new Audience($spEntityId),
202
                    ]),
203
                ],
204
            ),
205
            statements: $statements,
206
        );
207
208
        self::addSign($idpMetadata, $spMetadata, $assertion);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Module\exampl...ributeServer::addSign() has been deprecated: This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and should be replaced with a call to a future ServiceProvider-class in the saml2-library ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

208
        /** @scrutinizer ignore-deprecated */ self::addSign($idpMetadata, $spMetadata, $assertion);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
209
210
        $response = new Response(
211
            status: new Status(
212
                new StatusCode(C::STATUS_SUCCESS),
213
            ),
214
            issueInstant: $clock->now(),
215
            issuer: $issuer,
216
            id: (new Random())->generateID(),
217
            version: '2.0',
218
            inResponseTo: $message->getId(),
219
            destination: $endpoint,
220
            assertions: [$assertion],
221
        );
222
223
        self::addSign($idpMetadata, $spMetadata, $response);
0 ignored issues
show
Deprecated Code introduced by
The function SimpleSAML\Module\exampl...ributeServer::addSign() has been deprecated: This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and should be replaced with a call to a future ServiceProvider-class in the saml2-library ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

223
        /** @scrutinizer ignore-deprecated */ self::addSign($idpMetadata, $spMetadata, $response);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
224
225
        /** @var \SimpleSAML\SAML2\Binding\HTTPPost $httpPost */
226
        $httpPost = new HTTPPost();
227
        $httpPost->setRelayState($binding->getRelayState());
0 ignored issues
show
Bug introduced by
The method getRelayState() does not exist on SimpleSAML\SAML2\Binding\SOAP. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

227
        $httpPost->setRelayState($binding->/** @scrutinizer ignore-call */ getRelayState());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
228
229
        return new RunnableResponse([$httpPost, 'send'], [$response]);
230
    }
231
232
233
    /**
234
     * @param array<\SimpleSAML\SAML2\XML\saml\AttributeValue> $reqValues
235
     * @param array<\SimpleSAML\SAML2\XML\saml\AttributeValue> $values
236
     *
237
     * @return array<\SimpleSAML\SAML2\XML\saml\AttributeValue>
238
     */
239
    private function filterAttributeValues(array $reqValues, array $values): array
240
    {
241
        $result = [];
242
243
        foreach ($reqValues as $x) {
244
            foreach ($values as $y) {
245
                if ($x->getValue() === $y->getValue()) {
246
                    $result[] = $y;
247
                }
248
            }
249
        }
250
251
        return $result;
252
    }
253
254
255
    /**
256
     * @deprecated This method is a modified version of \SimpleSAML\Module\saml\Message::addSign and
257
     *  should be replaced with a call to a future ServiceProvider-class in the saml2-library
258
     *
259
     * Add signature key and sender certificate to an element (Message or Assertion).
260
     *
261
     * @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
262
     * @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
263
     * @param \SimpleSAML\XMLSecurity\XML\SignableElementInterface $element The element we should add the data to.
264
     */
265
    private static function addSign(
266
        Configuration $srcMetadata,
267
        Configuration $dstMetadata,
268
        SignableElementInterface &$element,
269
    ): void {
270
        $dstPrivateKey = $dstMetadata->getOptionalString('signature.privatekey', null);
271
        $cryptoUtils = new Utils\Crypto();
272
273
        if ($dstPrivateKey !== null) {
274
            /** @var string[] $keyArray */
275
            $keyArray = $cryptoUtils->loadPrivateKey($dstMetadata, true, 'signature.');
276
            $certArray = $cryptoUtils->loadPublicKey($dstMetadata, false, 'signature.');
277
        } else {
278
            /** @var string[] $keyArray */
279
            $keyArray = $cryptoUtils->loadPrivateKey($srcMetadata, true);
280
            $certArray = $cryptoUtils->loadPublicKey($srcMetadata, false);
281
        }
282
283
        $algo = $dstMetadata->getOptionalString('signature.algorithm', null);
284
        if ($algo === null) {
285
            $algo = $srcMetadata->getOptionalString('signature.algorithm', C::SIG_RSA_SHA256);
286
        }
287
288
        $privateKey = PrivateKey::fromFile($keyArray['PEM'], $keyArray['password']);
289
290
        $keyInfo = null;
291
        if ($certArray !== null) {
292
            $keyInfo = new KeyInfo([
293
                new X509Data(
294
                    [
295
                        new X509Certificate($certArray['PEM']),
296
                    ],
297
                ),
298
            ]);
299
        }
300
301
        $signer = (new SignatureAlgorithmFactory())->getAlgorithm(
302
            $algo,
0 ignored issues
show
Bug introduced by
It seems like $algo can also be of type null; however, parameter $algId of SimpleSAML\XMLSecurity\A...Factory::getAlgorithm() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

302
            /** @scrutinizer ignore-type */ $algo,
Loading history...
303
            $privateKey,
304
        );
305
306
        $element->sign($signer, C::C14N_EXCLUSIVE_WITHOUT_COMMENTS, $keyInfo);
307
    }
308
}
309