Completed
Push — feature/ra-optional-vetting ( 23510f...388e27 )
by
unknown
02:30
created

VettingService::verifyGssfId()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
/**
4
 * Copyright 2014 SURFnet bv
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
namespace Surfnet\StepupRa\RaBundle\Service;
20
21
use DateInterval;
22
use RuntimeException;
23
use Surfnet\StepupBundle\Command\SendSmsChallengeCommand;
24
use Surfnet\StepupBundle\Command\VerifyPossessionOfPhoneCommand;
25
use Surfnet\StepupBundle\Service\SecondFactorTypeService;
26
use Surfnet\StepupBundle\Service\SmsSecondFactor\OtpVerification;
27
use Surfnet\StepupBundle\Service\SmsSecondFactorServiceInterface;
28
use Surfnet\StepupBundle\Value\PhoneNumber\InternationalPhoneNumber;
29
use Surfnet\StepupBundle\Value\SecondFactorType;
30
use Surfnet\StepupMiddlewareClientBundle\Identity\Service\SecondFactorService;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Surfnet\StepupRa\RaBundl...ice\SecondFactorService.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
31
use Surfnet\StepupMiddlewareClientBundle\Identity\Command\VetSecondFactorCommand;
32
use Surfnet\StepupRa\RaBundle\Command\CreateU2fSignRequestCommand;
33
use Surfnet\StepupRa\RaBundle\Command\StartVettingProcedureCommand;
34
use Surfnet\StepupRa\RaBundle\Command\VerifyIdentityCommand;
35
use Surfnet\StepupRa\RaBundle\Command\VerifyU2fAuthenticationCommand;
36
use Surfnet\StepupRa\RaBundle\Command\VerifyYubikeyPublicIdCommand;
37
use Surfnet\StepupRa\RaBundle\Exception\DomainException;
38
use Surfnet\StepupRa\RaBundle\Exception\InvalidArgumentException;
39
use Surfnet\StepupRa\RaBundle\Exception\LoaTooLowException;
40
use Surfnet\StepupRa\RaBundle\Exception\UnknownVettingProcedureException;
41
use Surfnet\StepupRa\RaBundle\Repository\VettingProcedureRepository;
42
use Surfnet\StepupRa\RaBundle\Service\Gssf\VerificationResult as GssfVerificationResult;
43
use Surfnet\StepupRa\RaBundle\Service\U2f\AuthenticationVerificationResult;
44
use Surfnet\StepupRa\RaBundle\Service\U2f\SignRequestCreationResult;
45
use Surfnet\StepupRa\RaBundle\Value\DateTime;
46
use Surfnet\StepupRa\RaBundle\VettingProcedure;
47
use Surfnet\StepupU2fBundle\Dto\SignRequest;
48
use Surfnet\StepupU2fBundle\Dto\SignResponse;
49
use Symfony\Component\Translation\TranslatorInterface;
50
51
/**
52
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
53
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
54
 */
55
class VettingService
56
{
57
    const REGISTRATION_CODE_EXPIRED_ERROR =
58
        'Surfnet\Stepup\Exception\DomainException: Cannot vet second factor, the registration window is closed.';
59
60
    /**
61
     * @var \Surfnet\StepupBundle\Service\SmsSecondFactorServiceInterface
62
     */
63
    private $smsSecondFactorService;
64
65
    /**
66
     * @var \Surfnet\StepupRa\RaBundle\Service\YubikeySecondFactorService
67
     */
68
    private $yubikeySecondFactorService;
69
70
    /**
71
     * @var \Surfnet\StepupRa\RaBundle\Service\GssfService
72
     */
73
    private $gssfService;
74
75
    /**
76
     * @var \Surfnet\StepupRa\RaBundle\Service\U2fService
77
     */
78
    private $u2fService;
79
80
    /**
81
     * @var \Surfnet\StepupRa\RaBundle\Service\CommandService
82
     */
83
    private $commandService;
84
85
    /**
86
     * @var \Surfnet\StepupRa\RaBundle\Repository\VettingProcedureRepository
87
     */
88
    private $vettingProcedureRepository;
89
90
    /**
91
     * @var \Symfony\Component\Translation\TranslatorInterface
92
     */
93
    private $translator;
94
95
    /**
96
     * @var \Surfnet\StepupRa\RaBundle\Service\IdentityService
97
     */
98
    private $identityService;
99
100
    /**
101
     * @var \Surfnet\StepupBundle\Service\SecondFactorTypeService
102
     */
103
    private $secondFactorTypeService;
104
105
    /**
106
     * @var SecondFactorService
107
     */
108
    private $secondFactorService;
109
110
    public function __construct(
111
        SmsSecondFactorServiceInterface $smsSecondFactorService,
112
        YubikeySecondFactorService $yubikeySecondFactorService,
113
        GssfService $gssfService,
114
        U2fService $u2fService,
115
        CommandService $commandService,
116
        VettingProcedureRepository $vettingProcedureRepository,
117
        TranslatorInterface $translator,
118
        IdentityService $identityService,
119
        SecondFactorTypeService $secondFactorTypeService,
120
        SecondFactorService $secondFactorService
121
    ) {
122
        $this->smsSecondFactorService = $smsSecondFactorService;
123
        $this->yubikeySecondFactorService = $yubikeySecondFactorService;
124
        $this->gssfService = $gssfService;
125
        $this->u2fService = $u2fService;
126
        $this->commandService = $commandService;
127
        $this->vettingProcedureRepository = $vettingProcedureRepository;
128
        $this->translator = $translator;
129
        $this->identityService = $identityService;
130
        $this->secondFactorTypeService = $secondFactorTypeService;
131
        $this->secondFactorService = $secondFactorService;
132
    }
133
134
    /**
135
     * @param StartVettingProcedureCommand $command
136
     * @return bool
137
     */
138
    public function isLoaSufficientToStartProcedure(StartVettingProcedureCommand $command)
139
    {
140
        $secondFactorType = new SecondFactorType($command->secondFactor->type);
141
142
        return $this->secondFactorTypeService->isSatisfiedBy($secondFactorType, $command->authorityLoa);
143
    }
144
145
    /**
146
     * @param StartVettingProcedureCommand $command
147
     * @return bool
148
     */
149
    public function isExpiredRegistrationCode(StartVettingProcedureCommand $command)
150
    {
151
        return DateTime::now()->comesAfter(
152
            new DateTime(
153
                $command->secondFactor->registrationRequestedAt
154
                    ->add(new DateInterval('P14D'))
155
                    ->setTime(23, 59, 59)
156
            )
157
        );
158
    }
159
160
    /**
161
     * @param StartVettingProcedureCommand $command
162
     * @return bool
0 ignored issues
show
Documentation introduced by
Should the return type not be null|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
163
     */
164
    public function isVettedImplicitly(StartVettingProcedureCommand $command)
165
    {
166
        return $this->secondFactorService->getVerifiedSkipVetting($command->secondFactor->id);
167
    }
168
169
    /**
170
     * @param StartVettingProcedureCommand $command
171
     * @return string The procedure ID.
172
     */
173
    public function startProcedure(StartVettingProcedureCommand $command)
174
    {
175
        $this->smsSecondFactorService->clearSmsVerificationState();
176
177
        if (!$this->isLoaSufficientToStartProcedure($command)) {
178
            throw new LoaTooLowException(
179
                sprintf(
180
                    "Registration authority has LoA '%s', which is not enough to allow vetting of a '%s' second factor",
181
                    $command->authorityLoa,
182
                    $command->secondFactor->type
183
                )
184
            );
185
        }
186
187
        $procedure = VettingProcedure::start(
188
            $command->secondFactor->id,
189
            $command->authorityId,
190
            $command->registrationCode,
191
            $command->secondFactor
192
        );
193
194
        $this->vettingProcedureRepository->store($procedure);
195
196
        return $procedure->getId();
197
    }
198
199
    /**
200
     * @param string $procedureId
201
     * @throws UnknownVettingProcedureException
202
     */
203 View Code Duplication
    public function cancelProcedure($procedureId)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
    {
205
        if (!is_string($procedureId)) {
206
            throw InvalidArgumentException::invalidType('string', 'procedureId', $procedureId);
207
        }
208
209
        $procedure = $this->vettingProcedureRepository->retrieve($procedureId);
210
211
        if (!$procedure) {
212
            throw new UnknownVettingProcedureException(
213
                sprintf("No vetting procedure with id '%s' is known.", $procedureId)
214
            );
215
        }
216
217
        $this->vettingProcedureRepository->remove($procedureId);
218
    }
219
220
    /**
221
     * @return int
222
     */
223
    public function getSmsOtpRequestsRemainingCount()
224
    {
225
        return $this->smsSecondFactorService->getOtpRequestsRemainingCount();
226
    }
227
228
    /**
229
     * @return int
230
     */
231
    public function getSmsMaximumOtpRequestsCount()
232
    {
233
        return $this->smsSecondFactorService->getMaximumOtpRequestsCount();
234
    }
235
236
    /**
237
     * @param string $procedureId
238
     * @param SendSmsChallengeCommand $command
239
     * @return bool
240
     * @throws UnknownVettingProcedureException
241
     * @throws RuntimeException
242
     */
243
    public function sendSmsChallenge($procedureId, SendSmsChallengeCommand $command)
244
    {
245
        $procedure = $this->getProcedure($procedureId);
246
247
        $phoneNumber = InternationalPhoneNumber::fromStringFormat(
248
            $procedure->getSecondFactor()->secondFactorIdentifier
249
        );
250
251
        $identity = $this->identityService->findById($procedure->getSecondFactor()->identityId);
252
253
        if (!$identity) {
254
            throw new RuntimeException("Second factor is coupled to an identity that doesn't exist");
255
        }
256
257
        $command->phoneNumber = $phoneNumber;
258
        $command->body        = $this->translator->trans('ra.vetting.sms.challenge_body', [], 'messages', $identity->preferredLocale);
259
        $command->identity    = $procedure->getSecondFactor()->identityId;
260
        $command->institution = $procedure->getSecondFactor()->institution;
261
262
        return $this->smsSecondFactorService->sendChallenge($command);
263
    }
264
265
    /**
266
     * @param string                   $procedureId
267
     * @param VerifyPossessionOfPhoneCommand $command
268
     * @return OtpVerification
269
     * @throws UnknownVettingProcedureException
270
     * @throws DomainException
271
     */
272
    public function verifyPhoneNumber($procedureId, VerifyPossessionOfPhoneCommand $command)
273
    {
274
        $procedure = $this->getProcedure($procedureId);
275
276
        $verification = $this->smsSecondFactorService->verifyPossession($command);
277
278
        if (!$verification->wasSuccessful()) {
279
            return $verification;
280
        }
281
282
        $procedure->verifySecondFactorIdentifier($verification->getPhoneNumber());
283
        $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 274 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
284
285
        return $verification;
286
    }
287
288
    /**
289
     * @param string                       $procedureId
290
     * @param VerifyYubikeyPublicIdCommand $command
291
     * @return YubikeySecondFactor\VerificationResult
292
     */
293
    public function verifyYubikeyPublicId($procedureId, VerifyYubikeyPublicIdCommand $command)
294
    {
295
        $procedure = $this->getProcedure($procedureId);
296
297
        $command->expectedPublicId = $procedure->getSecondFactor()->secondFactorIdentifier;
298
        $command->identityId = $procedure->getSecondFactor()->identityId;
299
        $command->institution = $procedure->getSecondFactor()->institution;
300
301
        $result = $this->yubikeySecondFactorService->verifyYubikeyPublicId($command);
302
303
        if ($result->didPublicIdMatch()) {
304
            $procedure->verifySecondFactorIdentifier($result->getPublicId()->getYubikeyPublicId());
305
306
            $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 295 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
307
        }
308
309
        return $result;
310
    }
311
312
    /**
313
     * @param string $procedureId
314
     */
315
    public function startGssfVerification($procedureId)
316
    {
317
        $procedure = $this->getProcedure($procedureId);
318
319
        $this->gssfService->startVerification($procedure->getSecondFactor()->secondFactorIdentifier, $procedureId);
320
    }
321
322
    /**
323
     * @param string $gssfId
324
     * @return GssfVerificationResult
325
     */
326
    public function verifyGssfId($gssfId)
327
    {
328
        $result = $this->gssfService->verify($gssfId);
329
330
        if (!$result->isSuccess()) {
331
            return $result;
332
        }
333
334
        $procedure = $this->getProcedure($result->getProcedureId());
335
        $procedure->verifySecondFactorIdentifier($gssfId);
336
337
        $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($result->getProcedureId()) on line 334 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
338
339
        return $result;
340
    }
341
342
    /**
343
     * @param string $procedureId
344
     * @return SignRequestCreationResult
345
     */
346
    public function createU2fSignRequest($procedureId)
347
    {
348
        $procedure = $this->getProcedure($procedureId);
349
350
        $command = new CreateU2fSignRequestCommand();
351
        $command->keyHandle = $procedure->getSecondFactor()->secondFactorIdentifier;
352
        $command->identityId = $procedure->getSecondFactor()->identityId;
353
        $command->institution = $procedure->getSecondFactor()->institution;
354
355
        return $this->u2fService->createSignRequest($command);
356
    }
357
358
    /**
359
     * @param string       $procedureId
360
     * @param SignRequest  $signRequest
361
     * @param SignResponse $signResponse
362
     * @return AuthenticationVerificationResult
363
     */
364
    public function verifyU2fAuthentication($procedureId, SignRequest $signRequest, SignResponse $signResponse)
365
    {
366
        $procedure = $this->getProcedure($procedureId);
367
368
        $command = new VerifyU2fAuthenticationCommand();
369
        $command->identityId = $procedure->getSecondFactor()->identityId;
370
        $command->institution = $procedure->getSecondFactor()->institution;
371
        $command->signRequest = $signRequest;
372
        $command->signResponse = $signResponse;
373
374
        $result = $this->u2fService->verifyAuthentication($command);
375
376
        if ($result->wasSuccessful()) {
377
            $procedure->verifySecondFactorIdentifier($signResponse->keyHandle);
378
            $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 366 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
379
        }
380
381
        return $result;
382
    }
383
384
    /**
385
     * @param string $procedureId
386
     * @param VerifyIdentityCommand $command
387
     * @return void
388
     * @throws UnknownVettingProcedureException
389
     * @throws DomainException
390
     */
391
    public function verifyIdentity($procedureId, VerifyIdentityCommand $command)
392
    {
393
        $procedure = $this->getProcedure($procedureId);
394
        $procedure->verifyIdentity($command->documentNumber, $command->identityVerified);
395
396
        $this->vettingProcedureRepository->store($procedure);
0 ignored issues
show
Bug introduced by
It seems like $procedure defined by $this->getProcedure($procedureId) on line 393 can be null; however, Surfnet\StepupRa\RaBundl...dureRepository::store() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
397
    }
398
399
    /**
400
     * @param string $procedureId
401
     * @return \Surfnet\StepupMiddlewareClient\Service\ExecutionResult
402
     * @throws UnknownVettingProcedureException
403
     * @throws DomainException
404
     */
405
    public function vet($procedureId)
406
    {
407
        $procedure = $this->getProcedure($procedureId);
408
        $procedure->vet();
409
410
        $command = new VetSecondFactorCommand();
411
        $command->authorityId = $procedure->getAuthorityId();
412
        $command->identityId = $procedure->getSecondFactor()->identityId;
413
        $command->secondFactorId = $procedure->getSecondFactor()->id;
414
        $command->registrationCode = $procedure->getRegistrationCode();
415
        $command->secondFactorType = $procedure->getSecondFactor()->type;
416
        $command->secondFactorIdentifier = $procedure->getInputSecondFactorIdentifier();
417
        $command->documentNumber = $procedure->getDocumentNumber();
418
        $command->identityVerified = $procedure->isIdentityVerified();
419
420
        $result = $this->commandService->execute($command);
421
422
        if ($result->isSuccessful()) {
423
            $this->vettingProcedureRepository->remove($procedureId);
424
        }
425
426
        return $result;
427
    }
428
429
    /**
430
     * @param string $procedureId
431
     * @return string
432
     * @throws UnknownVettingProcedureException
433
     */
434
    public function getIdentityCommonName($procedureId)
435
    {
436
        return $this->getProcedure($procedureId)->getSecondFactor()->commonName;
437
    }
438
439
    /**
440
     * @param $procedureId
441
     * @return string
442
     * @throws UnknownVettingProcedureException
443
     */
444
    public function getSecondFactorIdentifier($procedureId)
445
    {
446
        return $this->getProcedure($procedureId)->getSecondFactor()->secondFactorIdentifier;
447
    }
448
449
    /**
450
     * @param string $procedureId
451
     * @return null|VettingProcedure
452
     * @throws UnknownVettingProcedureException
453
     */
454 View Code Duplication
    private function getProcedure($procedureId)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
455
    {
456
        if (!is_string($procedureId)) {
457
            throw InvalidArgumentException::invalidType('string', 'procedureId', $procedureId);
458
        }
459
460
        $procedure = $this->vettingProcedureRepository->retrieve($procedureId);
461
462
        if (!$procedure) {
463
            throw new UnknownVettingProcedureException(
464
                sprintf("No vetting procedure with id '%s' is known.", $procedureId)
465
            );
466
        }
467
468
        return $procedure;
469
    }
470
471
    /**
472
     * @param string $procedureId
473
     * @return bool
474
     */
475
    public function hasProcedure($procedureId)
476
    {
477
        if (!is_string($procedureId)) {
478
            throw InvalidArgumentException::invalidType('string', 'procedureId', $procedureId);
479
        }
480
481
        return $this->vettingProcedureRepository->retrieve($procedureId) !== null;
482
    }
483
}
484