From 97d6f68c386c3aed0b3a2e12410f182e9b8c05d8 Mon Sep 17 00:00:00 2001 From: Chris Wynne Date: Wed, 27 Nov 2024 15:00:44 +0000 Subject: [PATCH 1/6] PYIC-7076: Add check-reverification-identity lambda We need to ensure that if a user starts a reverification journey that they have some form of existing identity to re-verify. This adds a new lamdba that will check if they've got a P2, P1 or operational identity. It's not concerned about CIs, expired VCs, account interventions or pending identities. --- api-tests/features/mfa-reset-journey.feature | 138 +++---- deploy/journeyEngineStepFunction.asl.json | 21 ++ deploy/template.yaml | 97 +++++ .../build.gradle | 46 +++ .../CheckReverificationIdentityHandler.java | 197 ++++++++++ .../main/resources/IpvLambdaJsonLayout.json | 88 +++++ .../src/main/resources/log4j2.json | 22 ++ ...heckReverificationIdentityHandlerTest.java | 343 ++++++++++++++++++ .../journey-maps/reverification.yaml | 16 +- .../core/library/journeys/JourneyUris.java | 3 + .../persistence/item/IpvSessionItem.java | 6 + .../ipv/core/library/fixtures/VcFixtures.java | 7 + local-running/build.gradle | 1 + .../handlers/JourneyEngineHandler.java | 8 + settings.gradle | 1 + 15 files changed, 928 insertions(+), 66 deletions(-) create mode 100644 lambdas/check-reverification-identity/build.gradle create mode 100644 lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java create mode 100644 lambdas/check-reverification-identity/src/main/resources/IpvLambdaJsonLayout.json create mode 100644 lambdas/check-reverification-identity/src/main/resources/log4j2.json create mode 100644 lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java diff --git a/api-tests/features/mfa-reset-journey.feature b/api-tests/features/mfa-reset-journey.feature index 1d7d3fb670..58a9f7c272 100644 --- a/api-tests/features/mfa-reset-journey.feature +++ b/api-tests/features/mfa-reset-journey.feature @@ -1,75 +1,83 @@ @Build Feature: MFA reset journey - Background: There is an existing user and they start an MFA reset journey - Given the subject already has the following credentials - | CRI | scenario | - | dcmaw | kenneth-driving-permit-valid | - | address | kenneth-current | - | fraud | kenneth-score-2 | + Rule: User has an existing identity + Background: There is an existing user and they start an MFA reset journey + Given the subject already has the following credentials + | CRI | scenario | + | dcmaw | kenneth-driving-permit-valid | + | address | kenneth-current | + | fraud | kenneth-score-2 | - # Start MFA reset journey - When I start a new 'reverification' journey - Then I get a 'page-ipv-identity-document-start' page response + # Start MFA reset journey + When I start a new 'reverification' journey + Then I get a 'page-ipv-identity-document-start' page response - Scenario: Successful MFA reset journey - When I submit an 'appTriage' event - Then I get a 'dcmaw' CRI response - When I submit 'kenneth-driving-permit-valid' details to the CRI stub - Then I get a 'page-dcmaw-success' page response - When I submit a 'next' event - Then I get an OAuth response - When I use the OAuth response to get my MFA reset result - Then I get a successful MFA reset result + Scenario: Successful MFA reset journey + When I submit an 'appTriage' event + Then I get a 'dcmaw' CRI response + When I submit 'kenneth-driving-permit-valid' details to the CRI stub + Then I get a 'page-dcmaw-success' page response + When I submit a 'next' event + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get a successful MFA reset result - Scenario: Failed MFA reset journey with breaching CI - user can still reuse existing identity - When I submit an 'appTriage' event - Then I get a 'dcmaw' CRI response - When I submit 'kenneth-passport-with-breaching-ci' details to the CRI stub - Then I get a 'pyi-no-match' page response - When I submit a 'next' event - Then I get an OAuth response - When I use the OAuth response to get my MFA reset result - Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed' + Scenario: Failed MFA reset journey with breaching CI - user can still reuse existing identity + When I submit an 'appTriage' event + Then I get a 'dcmaw' CRI response + When I submit 'kenneth-passport-with-breaching-ci' details to the CRI stub + Then I get a 'pyi-no-match' page response + When I submit a 'next' event + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed' - # New journey with same user id - When I start a new 'medium-confidence' journey - Then I get a 'page-ipv-reuse' page response + # New journey with same user id + When I start a new 'medium-confidence' journey + Then I get a 'page-ipv-reuse' page response - Scenario: Failed MFA reset journey - DCMAW error - When I submit an 'appTriage' event - Then I get a 'dcmaw' CRI response - When I call the CRI stub and get an 'access-denied' OAuth error - When I submit a 'next' event - Then I get an OAuth response - When I use the OAuth response to get my MFA reset result - Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete' + Scenario: Failed MFA reset journey - DCMAW error + When I submit an 'appTriage' event + Then I get a 'dcmaw' CRI response + When I call the CRI stub and get an 'access-denied' OAuth error + When I submit a 'next' event + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete' - Scenario: Failed MFA reset journey - no photo id - When I submit an 'end' event - Then I get a 'pyi-another-way' page response - When I submit an 'next' event - Then I get an OAuth response - When I use the OAuth response to get my MFA reset result - Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete' + Scenario: Failed MFA reset journey - no photo id + When I submit an 'end' event + Then I get a 'pyi-another-way' page response + When I submit an 'next' event + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get an unsuccessful MFA reset result with failure code 'identity_check_incomplete' - Scenario: Failed MFA reset journey - failed verification score - When I submit an 'appTriage' event - Then I get a 'dcmaw' CRI response - When I submit 'kenneth-passport-verification-zero' details to the CRI stub - Then I get a 'pyi-no-match' page response - When I submit a 'next' event - Then I get an OAuth response - When I use the OAuth response to get my MFA reset result - Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed' + Scenario: Failed MFA reset journey - failed verification score + When I submit an 'appTriage' event + Then I get a 'dcmaw' CRI response + When I submit 'kenneth-passport-verification-zero' details to the CRI stub + Then I get a 'pyi-no-match' page response + When I submit a 'next' event + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get an unsuccessful MFA reset result with failure code 'identity_check_failed' - Scenario: Failed MFA reset journey - non-matching identity - When I submit an 'appTriage' event - Then I get a 'dcmaw' CRI response - When I submit 'alice-passport-valid' details to the CRI stub - Then I get a 'page-dcmaw-success' page response - When I submit a 'next' event - Then I get a 'pyi-no-match' page response - When I submit a 'next' event - Then I get an OAuth response - When I use the OAuth response to get my MFA reset result - Then I get an unsuccessful MFA reset result with failure code 'identity_did_not_match' + Scenario: Failed MFA reset journey - non-matching identity + When I submit an 'appTriage' event + Then I get a 'dcmaw' CRI response + When I submit 'alice-passport-valid' details to the CRI stub + Then I get a 'page-dcmaw-success' page response + When I submit a 'next' event + Then I get a 'pyi-no-match' page response + When I submit a 'next' event + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get an unsuccessful MFA reset result with failure code 'identity_did_not_match' + + Rule: The user has no existing identity + Scenario: Attempted MFA reset journey + When I start a new 'reverification' journey + Then I get an OAuth response + When I use the OAuth response to get my MFA reset result + Then I get an unsuccessful MFA reset result with failure code 'no_identity_available' diff --git a/deploy/journeyEngineStepFunction.asl.json b/deploy/journeyEngineStepFunction.asl.json index f29b440702..5e93a8e2d2 100644 --- a/deploy/journeyEngineStepFunction.asl.json +++ b/deploy/journeyEngineStepFunction.asl.json @@ -77,6 +77,11 @@ "Variable": "$.journey", "StringMatches": "/journey/call-dcmaw-async-cri", "Next": "CallDcmawAsyncCriLambda" + }, + { + "Variable": "$.journey", + "StringMatches": "/journey/check-reverification-identity", + "Next": "CheckReverificationIdentityLambda" } ], "Default": "Success" @@ -278,6 +283,22 @@ "MaxDelaySeconds": 4 }] }, + "CheckReverificationIdentityLambda": { + "Type": "Task", + "Resource": "${CheckReverificationIdentityFunctionArn}", + "Parameters": { + "ipvSessionId.$": "$$.Execution.Input.ipvSessionId", + "featureSet.$": "$$.Execution.Input.featureSet" + }, + "Next": "ProcessNextJourney", + "Retry": [{ + "ErrorEquals": ["Lambda.SnapStartNotReadyException"], + "IntervalSeconds": 1, + "BackoffRate": 2, + "MaxAttempts": 5, + "MaxDelaySeconds": 4 + }] + }, "ProcessNextJourney": { "Type": "Choice", "Choices": [ diff --git a/deploy/template.yaml b/deploy/template.yaml index 658f4fbc7d..1b474048d1 100644 --- a/deploy/template.yaml +++ b/deploy/template.yaml @@ -1510,6 +1510,7 @@ Resources: StoreIdentityLambdaArn: !Ref StoreIdentityFunction.Version ResetSessionIdentityFunctionArn: !Ref ResetSessionIdentityFunction.Version CheckCoiFunctionArn: !Ref CheckCoiFunction.Version + CheckReverificationIdentityFunctionArn: !Ref CheckReverificationIdentityFunction.Version AutoPublishAlias: live DeploymentPreference: Type: !Ref StepFunctionDeploymentPreference @@ -1530,6 +1531,7 @@ Resources: - !Ref StoreIdentityFunctionErrorCanaryAlarm - !Ref ResetSessionIdentityFunctionErrorCanaryAlarm - !Ref CheckCoiFunctionErrorCanaryAlarm + - !Ref CheckReverificationIdentityFunctionErrorCanaryAlarm - !Ref AWS::NoValue StateMachineVersionArn: !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${JourneyEngineStepFunction}:live" Events: @@ -1575,6 +1577,8 @@ Resources: FunctionName: !Ref ResetSessionIdentityFunction - LambdaInvokePolicy: FunctionName: !Ref CheckCoiFunction + - LambdaInvokePolicy: + FunctionName: !Ref CheckReverificationIdentityFunction - Statement: - Sid: CloudWatchLogsAccess Effect: Allow @@ -2603,6 +2607,73 @@ Resources: FilterPattern: "" LogGroupName: !Ref CheckCoiFunctionLogGroup + CheckReverificationIdentityFunction: + Type: AWS::Serverless::Function + DependsOn: + - CheckReverificationIdentityFunctionLogGroup + Properties: + # checkov:skip=CKV_AWS_115: We do not have enough data to allocate the concurrent execution allowance per function. + # checkov:skip=CKV_AWS_116: Lambdas invoked via API Gateway do not support Dead Letter Queues. + # checkov:skip=CKV_AWS_117: Lambdas will migrate to our own VPC in future work. + FunctionName: !Sub "check-reverification-identity-${Environment}" + Handler: uk.gov.di.ipv.core.checkreverificationidentity.CheckReverificationIdentityHandler::handleRequest + PackageType: Zip + CodeUri: ../lambdas/check-reverification-identity + Tracing: Active + Environment: + # checkov:skip=CKV_AWS_173: These environment variables do not require encryption. + Variables: + ENVIRONMENT: !Sub "${Environment}" + POWERTOOLS_SERVICE_NAME: !Sub check-reverification-identity-${Environment} + IPV_SESSIONS_TABLE_NAME: !Ref SessionsTable + CLIENT_OAUTH_SESSIONS_TABLE_NAME: !Ref ClientOAuthSessionsTable + VpcConfig: + SubnetIds: + - Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdA + - Fn::ImportValue: !Sub ${VpcStackName}-ProtectedSubnetIdB + SecurityGroupIds: + - !GetAtt LambdaSecurityGroup.GroupId + Policies: + - VPCAccessPolicy: { } + - Statement: + - Sid: EnforceStayinSpecificVpc + Effect: Allow + Action: + - 'lambda:CreateFunction' + - 'lambda:UpdateFunctionConfiguration' + Resource: + - "*" + Condition: + StringEquals: + "lambda:VpcIds": + - Fn::ImportValue: !Sub ${VpcStackName}-VpcId + - KMSDecryptPolicy: + KeyId: !Ref DynamoDBKmsKey + - SSMParameterReadPolicy: + ParameterName: !Sub ${Environment}/core/* + - DynamoDBCrudPolicy: + TableName: !Ref SessionsTable + - DynamoDBReadPolicy: + TableName: !Ref ClientOAuthSessionsTable + - AWSSecretsManagerGetSecretValuePolicy: + SecretArn: !Sub arn:aws:secretsmanager:eu-west-2:*:secret:/${Environment}/core/evcs/apiKey-* + AutoPublishAlias: live + + CheckReverificationIdentityFunctionLogGroup: + Type: AWS::Logs::LogGroup + Properties: + RetentionInDays: 30 + LogGroupName: !Sub "/aws/lambda/check-reverification-identity-${Environment}" + KmsKeyId: !GetAtt LoggingKmsKey.Arn + + CheckReverificationIdentityFunctionLogGroupSubscriptionFilter: + Type: AWS::Logs::SubscriptionFilter + Condition: IsSubscriptionEnviroment + Properties: + DestinationArn: "arn:aws:logs:eu-west-2:885513274347:destination:csls_cw_logs_destination_prodpython" + FilterPattern: "" + LogGroupName: !Ref CheckReverificationIdentityFunctionLogGroup + UserIssuedCredentialsV2Table: Type: AWS::DynamoDB::Table Properties: @@ -3720,6 +3791,32 @@ Resources: ComparisonOperator: GreaterThanOrEqualToThreshold TreatMissingData: notBreaching + CheckReverificationIdentityFunctionErrorCanaryAlarm: + Type: AWS::CloudWatch::Alarm + Condition: UseCanaryDeploymentAlarms + Properties: + ActionsEnabled: true + AlarmActions: + - !ImportValue alarm-alerts-topic + AlarmDescription: !Sub "Error returned from the CheckReverificationIdentityFunction" + AlarmName: !Sub ${AWS::StackName}-CheckReverificationIdentityFunction-ErrorCanary + MetricName: Errors + Dimensions: + - Name: Resource + Value: !Sub "check-reverification-identity-${Environment}:live" + - Name: FunctionName + Value: !Ref CheckReverificationIdentityFunction + - Name: ExecutedVersion + Value: !GetAtt CheckReverificationIdentityFunction.Version.Version + Namespace: AWS/Lambda + Statistic: Sum + Unit: Count + Period: 60 + EvaluationPeriods: 3 + Threshold: 1 + ComparisonOperator: GreaterThanOrEqualToThreshold + TreatMissingData: notBreaching + Outputs: IPVCorePrivateAPIGatewayID: Description: Core Back Private API Gateway ID diff --git a/lambdas/check-reverification-identity/build.gradle b/lambdas/check-reverification-identity/build.gradle new file mode 100644 index 0000000000..cdf761586e --- /dev/null +++ b/lambdas/check-reverification-identity/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'java' + id 'idea' + id 'jacoco' + alias libs.plugins.postCompileWeaving +} + +dependencies { + implementation libs.bundles.awsLambda, + project(":libs:common-services"), + project(":libs:evcs-service"), + project(":libs:gpg45-evaluator"), + project(":libs:user-identity-service"), + project(":libs:verifiable-credentials") + + aspect libs.powertoolsLogging, + libs.powertoolsTracing, + libs.aspectj + + testImplementation libs.junitJupiter, + libs.mockitoJunit, + project(path: ":libs:test-helpers"), + project(path: ":libs:common-services", configuration: "tests") + + + testRuntimeOnly(libs.junitPlatform) +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +test { + // Configures environment variable to avoid initialization of AWS X-Ray segments for each tests + environment "LAMBDA_TASK_ROOT", "handler" + useJUnitPlatform () + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required.set(true) + } +} diff --git a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java new file mode 100644 index 0000000000..665183d95e --- /dev/null +++ b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java @@ -0,0 +1,197 @@ +package uk.gov.di.ipv.core.checkreverificationidentity; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import software.amazon.lambda.powertools.logging.Logging; +import software.amazon.lambda.powertools.tracing.Tracing; +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.domain.JourneyErrorResponse; +import uk.gov.di.ipv.core.library.domain.JourneyRequest; +import uk.gov.di.ipv.core.library.domain.JourneyResponse; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; +import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; +import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; +import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; +import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +import uk.gov.di.ipv.core.library.helpers.LogHelper; +import uk.gov.di.ipv.core.library.helpers.RequestHelper; +import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.EvcsService; +import uk.gov.di.ipv.core.library.service.IpvSessionService; +import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; + +import java.text.ParseException; +import java.util.List; +import java.util.Map; + +import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_NOT_FOUND; +import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_SERVER_ERROR; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_SUCCESSFUL_VC_STORE_ITEMS; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; +import static uk.gov.di.ipv.core.library.domain.ProfileType.GPG45; +import static uk.gov.di.ipv.core.library.domain.ProfileType.OPERATIONAL_HMRC; +import static uk.gov.di.ipv.core.library.domain.ReverificationFailureCode.NO_IDENTITY_AVAILABLE; +import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; +import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; +import static uk.gov.di.ipv.core.library.enums.Vot.SUPPORTED_VOTS_BY_DESCENDING_STRENGTH; +import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_PROFILE; +import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; +import static uk.gov.di.ipv.core.library.helpers.RequestHelper.getIpvSessionId; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ERROR_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_FOUND; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_NOT_FOUND_PATH; + +public class CheckReverificationIdentityHandler + implements RequestHandler> { + private static final Logger LOGGER = LogManager.getLogger(); + private static final Map NOT_FOUND_RESPONSE = + new JourneyResponse(JOURNEY_NOT_FOUND_PATH).toObjectMap(); + private static final Map FOUND_RESPONSE = + new JourneyResponse(JOURNEY_FOUND).toObjectMap(); + private final ConfigService configService; + private final IpvSessionService ipvSessionService; + private final ClientOAuthSessionDetailsService clientSessionService; + private final EvcsService evcsService; + private final UserIdentityService userIdentityService; + private final Gpg45ProfileEvaluator gpg45ProfileEvaluator; + + public CheckReverificationIdentityHandler( + ConfigService configService, + IpvSessionService ipvSessionService, + ClientOAuthSessionDetailsService clientOAuthSessionDetailsService, + EvcsService evcsService, + UserIdentityService userIdentityService, + Gpg45ProfileEvaluator gpg45ProfileEvaluator) { + this.configService = configService; + this.ipvSessionService = ipvSessionService; + this.clientSessionService = clientOAuthSessionDetailsService; + this.evcsService = evcsService; + this.userIdentityService = userIdentityService; + this.gpg45ProfileEvaluator = gpg45ProfileEvaluator; + } + + @ExcludeFromGeneratedCoverageReport + public CheckReverificationIdentityHandler() { + this(ConfigService.create()); + } + + @ExcludeFromGeneratedCoverageReport + public CheckReverificationIdentityHandler(ConfigService configService) { + this.configService = ConfigService.create(); + this.ipvSessionService = new IpvSessionService(configService); + this.clientSessionService = new ClientOAuthSessionDetailsService(configService); + this.evcsService = new EvcsService(configService); + this.userIdentityService = new UserIdentityService(configService); + this.gpg45ProfileEvaluator = new Gpg45ProfileEvaluator(); + } + + @Tracing + @Logging(clearState = true) + @Override + public Map handleRequest(JourneyRequest request, Context context) { + LogHelper.attachComponentId(configService); + configService.setFeatureSet(RequestHelper.getFeatureSet(request)); + + try { + var ipvSessionId = getIpvSessionId(request); + LogHelper.attachIpvSessionIdToLogs(ipvSessionId); + var ipvSession = ipvSessionService.getIpvSession(ipvSessionId); + LogHelper.attachClientSessionIdToLogs(ipvSession.getClientOAuthSessionId()); + var clientOAuthSession = + clientSessionService.getClientOAuthSession( + ipvSession.getClientOAuthSessionId()); + LogHelper.attachClientIdToLogs(clientOAuthSession.getClientId()); + LogHelper.attachGovukSigninJourneyIdToLogs( + clientOAuthSession.getGovukSigninJourneyId()); + + var vcs = + evcsService.getVerifiableCredentials( + clientOAuthSession.getUserId(), + clientOAuthSession.getEvcsAccessToken(), + CURRENT); + + if (!vcsContainIdentity(vcs)) { + ipvSession.setFailureCode(NO_IDENTITY_AVAILABLE); + ipvSessionService.updateIpvSession(ipvSession); + return NOT_FOUND_RESPONSE; + } + + return FOUND_RESPONSE; + + } catch (HttpResponseExceptionWithErrorBody | EvcsServiceException e) { + LOGGER.error(LogHelper.buildErrorMessage(e.getErrorResponse())); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, e.getResponseCode(), e.getErrorResponse()) + .toObjectMap(); + } catch (IpvSessionNotFoundException e) { + LOGGER.error(LogHelper.buildErrorMessage("IPV session not found", e)); + return new JourneyErrorResponse(JOURNEY_ERROR_PATH, SC_NOT_FOUND, IPV_SESSION_NOT_FOUND) + .toObjectMap(); + } catch (CredentialParseException e) { + LOGGER.error(LogHelper.buildErrorMessage("Failed to parse credentials", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, + SC_SERVER_ERROR, + FAILED_TO_PARSE_SUCCESSFUL_VC_STORE_ITEMS) + .toObjectMap(); + } catch (ParseException e) { + LOGGER.error(LogHelper.buildErrorMessage("Failed to get VOT from operational VC", e)); + return new JourneyErrorResponse( + JOURNEY_ERROR_PATH, SC_SERVER_ERROR, FAILED_TO_PARSE_ISSUED_CREDENTIALS) + .toObjectMap(); + } catch (Exception e) { + LOGGER.error(LogHelper.buildErrorMessage("Unhandled lambda exception", e)); + throw e; + } + } + + private boolean vcsContainIdentity(List vcs) + throws ParseException, HttpResponseExceptionWithErrorBody { + var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcs, GPG45); + var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); + var gpg45VcsCorrelated = userIdentityService.areVcsCorrelated(gpg45Vcs); + var operationalVcs = VcHelper.filterVCBasedOnProfileType(vcs, OPERATIONAL_HMRC); + + for (var vot : SUPPORTED_VOTS_BY_DESCENDING_STRENGTH) { + if (GPG45.equals(vot.getProfileType()) && gpg45VcsCorrelated) { + var matchedProfile = + gpg45ProfileEvaluator.getFirstMatchingProfile( + gpg45Scores, vot.getSupportedGpg45Profiles()); + if (matchedProfile.isPresent()) { + LOGGER.info( + LogHelper.buildLogMessage("Identity for reverification found") + .with(LOG_VOT.getFieldName(), vot) + .with(LOG_PROFILE.getFieldName(), matchedProfile.get())); + return true; + } + } + if (OPERATIONAL_HMRC.equals(vot.getProfileType()) + && vcsContainOperationalVot(operationalVcs, vot)) { + LOGGER.info( + LogHelper.buildLogMessage("Identity for reverification found") + .with(LOG_VOT.getFieldName(), vot)); + return true; + } + } + LOGGER.info(LogHelper.buildLogMessage("No identity for reverification found")); + return false; + } + + private boolean vcsContainOperationalVot(List vcs, Vot vot) + throws ParseException { + for (var vc : vcs) { + var credentialVot = vc.getClaimsSet().getStringClaim(VOT_CLAIM_NAME); + if (vot.name().equals(credentialVot)) { + return true; + } + } + return false; + } +} diff --git a/lambdas/check-reverification-identity/src/main/resources/IpvLambdaJsonLayout.json b/lambdas/check-reverification-identity/src/main/resources/IpvLambdaJsonLayout.json new file mode 100644 index 0000000000..fb70c97dfe --- /dev/null +++ b/lambdas/check-reverification-identity/src/main/resources/IpvLambdaJsonLayout.json @@ -0,0 +1,88 @@ +{ + "timestamp": { + "$resolver": "timestamp" + }, + "instant": { + "epochSecond": { + "$resolver": "timestamp", + "epoch": { + "unit": "secs", + "rounded": true + } + }, + "nanoOfSecond": { + "$resolver": "timestamp", + "epoch": { + "unit": "secs.nanos" + } + } + }, + "thread": { + "$resolver": "thread", + "field": "name" + }, + "level": { + "$resolver": "level", + "field": "name" + }, + "loggerName": { + "$resolver": "logger", + "field": "name" + }, + "message": { + "$resolver": "message" + }, + "thrown": { + "message": { + "$resolver": "exception", + "field": "message" + }, + "name": { + "$resolver": "exception", + "field": "className" + }, + "extendedStackTrace": { + "$resolver": "exception", + "field": "stackTrace" + } + }, + "contextStack": { + "$resolver": "ndc" + }, + "endOfBatch": { + "$resolver": "endOfBatch" + }, + "loggerFqcn": { + "$resolver": "logger", + "field": "fqcn" + }, + "threadId": { + "$resolver": "thread", + "field": "id" + }, + "threadPriority": { + "$resolver": "thread", + "field": "priority" + }, + "source": { + "class": { + "$resolver": "source", + "field": "className" + }, + "method": { + "$resolver": "source", + "field": "methodName" + }, + "file": { + "$resolver": "source", + "field": "fileName" + }, + "line": { + "$resolver": "source", + "field": "lineNumber" + } + }, + "": { + "$resolver": "powertools" + } +} diff --git a/lambdas/check-reverification-identity/src/main/resources/log4j2.json b/lambdas/check-reverification-identity/src/main/resources/log4j2.json new file mode 100644 index 0000000000..7055612fe6 --- /dev/null +++ b/lambdas/check-reverification-identity/src/main/resources/log4j2.json @@ -0,0 +1,22 @@ +{ + "Configuration": { + "status": "warn", + "appenders": { + "Console": { + "name": "JsonAppender", + "target": "SYSTEM_OUT", + "JsonTemplateLayout": { + "eventTemplateUri": "classpath:IpvLambdaJsonLayout.json" + } + } + }, + "Loggers": { + "Root": { + "level": "info", + "AppenderRef": { + "ref": "JsonAppender" + } + } + } + } +} diff --git a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java new file mode 100644 index 0000000000..1bfa1233ca --- /dev/null +++ b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java @@ -0,0 +1,343 @@ +package uk.gov.di.ipv.core.checkreverificationidentity; + +import com.amazonaws.services.lambda.runtime.Context; +import com.nimbusds.jwt.JWTClaimsSet; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.di.ipv.core.library.domain.JourneyRequest; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.exception.EvcsServiceException; +import uk.gov.di.ipv.core.library.exceptions.ClientOauthSessionNotFoundException; +import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; +import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; +import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; +import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; +import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; +import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService; +import uk.gov.di.ipv.core.library.service.ConfigService; +import uk.gov.di.ipv.core.library.service.EvcsService; +import uk.gov.di.ipv.core.library.service.IpvSessionService; +import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; + +import java.util.List; + +import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_NOT_FOUND; +import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_SERVER_ERROR; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static uk.gov.di.ipv.core.library.domain.Cri.HMRC_MIGRATION; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.CLIENT_OAUTH_SESSION_NOT_FOUND; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_NAME_CORRELATION; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_SUCCESSFUL_VC_STORE_ITEMS; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.RECEIVED_NON_200_RESPONSE_STATUS_CODE; +import static uk.gov.di.ipv.core.library.domain.ReverificationFailureCode.NO_IDENTITY_AVAILABLE; +import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; +import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1B_DCMAW_VC; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.VC_ADDRESS; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.l1AEvidenceVc; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcExperianFraudScoreTwo; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL200; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL250; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcVerificationM1a; + +@ExtendWith(MockitoExtension.class) +class CheckReverificationIdentityHandlerTest { + private static final String TEST_IPV_SESSION_ID = "test-ipv-session-id"; + private static final String TEST_CLIENT_SESSION_ID = "test-client-session-id"; + private static final String TEST_USER_ID = "test-user-id"; + private static final String TEST_EVCS_ACCESS_TOKEN = "test-evcs-access-token"; + private static final JourneyRequest REQUEST = + JourneyRequest.builder().ipvSessionId(TEST_IPV_SESSION_ID).build(); + private static VerifiableCredential pcl200vc; + private static VerifiableCredential pcl250vc; + private static VerifiableCredential m1BFraudVc; + private static VerifiableCredential l1AEvidenceVc; + private static VerifiableCredential m1AVerificationVc; + private IpvSessionItem ipvSession; + private ClientOAuthSessionItem clientSession; + + @Mock private VerifiableCredential mockVc; + @Mock private Context mockContext; + @Mock private ConfigService mockConfigService; + @Mock private IpvSessionService mockIpvSessionService; + @Mock private ClientOAuthSessionDetailsService mockClientSessionService; + @Mock private EvcsService mockEvcsService; + @Mock private UserIdentityService mockUserIdentityService; + @Spy private Gpg45ProfileEvaluator mockGpg45Evaluator; + @InjectMocks private CheckReverificationIdentityHandler checkReverificationIdentityHandler; + + @BeforeAll + public static void beforeAll() throws Exception { + pcl200vc = vcHmrcMigrationPCL200(); + pcl250vc = vcHmrcMigrationPCL250(); + m1BFraudVc = vcExperianFraudScoreTwo(); + l1AEvidenceVc = l1AEvidenceVc(); + m1AVerificationVc = vcVerificationM1a(); + } + + @BeforeEach + public void beforeEach() { + ipvSession = + spy( + IpvSessionItem.builder() + .ipvSessionId(TEST_IPV_SESSION_ID) + .clientOAuthSessionId(TEST_CLIENT_SESSION_ID) + .build()); + clientSession = + ClientOAuthSessionItem.builder() + .clientOAuthSessionId(TEST_CLIENT_SESSION_ID) + .userId(TEST_USER_ID) + .evcsAccessToken(TEST_EVCS_ACCESS_TOKEN) + .build(); + } + + @Nested + class SuccessfulInvocations { + @BeforeEach + public void beforeEachFound() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); + when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) + .thenReturn(clientSession); + } + + @Test + void shouldReturnJourneyFoundIfUserHasP2Identity() throws Exception { + var p2Vcs = List.of(M1B_DCMAW_VC, VC_ADDRESS, m1BFraudVc, pcl250vc); + when(mockUserIdentityService.areVcsCorrelated( + List.of(M1B_DCMAW_VC, VC_ADDRESS, m1BFraudVc))) + .thenReturn(true); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(p2Vcs); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/found", response.get("journey")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyFoundIfUserHasP1Identity() throws Exception { + var p1Vcs = List.of(l1AEvidenceVc, VC_ADDRESS, m1BFraudVc, m1AVerificationVc); + when(mockUserIdentityService.areVcsCorrelated(p1Vcs)).thenReturn(true); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(p1Vcs); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/found", response.get("journey")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyFoundIfUserHasPcl250Identity() throws Exception { + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(List.of(pcl250vc)); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/found", response.get("journey")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyFoundIfUserHasPcl200Identity() throws Exception { + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(List.of(pcl200vc)); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/found", response.get("journey")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyNotFoundWhenNoVcs() throws Exception { + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(List.of()); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/not-found", response.get("journey")); + + var inorder = inOrder(ipvSession, mockIpvSessionService); + inorder.verify(ipvSession).setFailureCode(NO_IDENTITY_AVAILABLE); + inorder.verify(mockIpvSessionService).updateIpvSession(ipvSession); + inorder.verifyNoMoreInteractions(); + } + + @Test + void shouldReturnJourneyNotFoundIfVcsDontCorrelate() throws Exception { + var p2Vcs = List.of(M1B_DCMAW_VC, VC_ADDRESS, m1BFraudVc); + when(mockUserIdentityService.areVcsCorrelated(p2Vcs)).thenReturn(false); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(p2Vcs); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/not-found", response.get("journey")); + + var inorder = inOrder(ipvSession, mockIpvSessionService); + inorder.verify(ipvSession).setFailureCode(NO_IDENTITY_AVAILABLE); + inorder.verify(mockIpvSessionService).updateIpvSession(ipvSession); + inorder.verifyNoMoreInteractions(); + } + } + + @Nested + class ErrorInvocations { + @Test + void shouldReturnJourneyErrorIfIpvSessionNotFound() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenThrow(new IpvSessionNotFoundException("Beep")); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/error", response.get("journey")); + assertEquals(SC_NOT_FOUND, response.get("statusCode")); + assertEquals(IPV_SESSION_NOT_FOUND.getMessage(), response.get("message")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyErrorIfClientSessionIdNotFound() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); + when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) + .thenThrow(new ClientOauthSessionNotFoundException()); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/error", response.get("journey")); + assertEquals(SC_SERVER_ERROR, response.get("statusCode")); + assertEquals(CLIENT_OAUTH_SESSION_NOT_FOUND.getMessage(), response.get("message")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyErrorIfErrorFetchingVcs() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); + when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) + .thenReturn(clientSession); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenThrow( + new EvcsServiceException( + SC_SERVER_ERROR, RECEIVED_NON_200_RESPONSE_STATUS_CODE)); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/error", response.get("journey")); + assertEquals(SC_SERVER_ERROR, response.get("statusCode")); + assertEquals( + RECEIVED_NON_200_RESPONSE_STATUS_CODE.getMessage(), response.get("message")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyErrorIfFailureToParseFetchedVcs() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); + when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) + .thenReturn(clientSession); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenThrow(new CredentialParseException("Baa")); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/error", response.get("journey")); + assertEquals(SC_SERVER_ERROR, response.get("statusCode")); + assertEquals( + FAILED_TO_PARSE_SUCCESSFUL_VC_STORE_ITEMS.getMessage(), + response.get("message")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyErrorIfFailureToDoCorrelationCheck() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); + when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) + .thenReturn(clientSession); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(List.of(m1BFraudVc)); + when(mockUserIdentityService.areVcsCorrelated(List.of(m1BFraudVc))) + .thenThrow( + new HttpResponseExceptionWithErrorBody( + SC_SERVER_ERROR, FAILED_NAME_CORRELATION)); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/error", response.get("journey")); + assertEquals(SC_SERVER_ERROR, response.get("statusCode")); + assertEquals(FAILED_NAME_CORRELATION.getMessage(), response.get("message")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldReturnJourneyErrorIfFailureToParseVotFromOperationalVc() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); + when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) + .thenReturn(clientSession); + when(mockEvcsService.getVerifiableCredentials( + TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) + .thenReturn(List.of(mockVc)); + when(mockVc.getCri()).thenReturn(HMRC_MIGRATION); + when(mockVc.getClaimsSet()) + .thenReturn(new JWTClaimsSet.Builder().claim(VOT_CLAIM_NAME, 101).build()); + + var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); + + assertEquals("/journey/error", response.get("journey")); + assertEquals(SC_SERVER_ERROR, response.get("statusCode")); + assertEquals(FAILED_TO_PARSE_ISSUED_CREDENTIALS.getMessage(), response.get("message")); + verify(ipvSession, never()).setFailureCode(any()); + } + + @Test + void shouldLogRuntimeExceptionsAndRethrow() throws Exception { + when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)) + .thenThrow(new RuntimeException("😓")); + + var logCollector = + LogCollector.getLogCollectorFor(CheckReverificationIdentityHandler.class); + + var thrown = + assertThrows( + RuntimeException.class, + () -> + checkReverificationIdentityHandler.handleRequest( + REQUEST, mockContext)); + + assertEquals("😓", thrown.getMessage()); + + var logMessage = logCollector.getLogMessages().get(0); + assertTrue(logMessage.contains("Unhandled lambda exception")); + assertTrue(logMessage.contains("😓")); + verify(ipvSession, never()).setFailureCode(any()); + } + } +} diff --git a/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reverification.yaml b/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reverification.yaml index b977fe8faf..be8d617594 100644 --- a/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reverification.yaml +++ b/lambdas/process-journey-event/src/main/resources/statemachine/journey-maps/reverification.yaml @@ -13,7 +13,7 @@ states: targetState: ERROR_NO_TICF checkFeatureFlag: mfaResetEnabled: - targetState: IDENTITY_START_PAGE + targetState: CHECK_REVERIFICATION_IDENTITY checkIfDisabled: dcmaw: targetJourney: TECHNICAL_ERROR @@ -78,6 +78,20 @@ states: targetState: ERROR_NO_TICF # Journey states + + CHECK_REVERIFICATION_IDENTITY: + response: + type: process + lambda: check-reverification-identity + events: + found: + targetState: IDENTITY_START_PAGE + not-found: + targetState: CRI_TICF + error: + targetJourney: TECHNICAL_ERROR + targetState: ERROR + IDENTITY_START_PAGE: response: type: page diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java index 7868d5aeb6..d21c786b16 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/journeys/JourneyUris.java @@ -19,6 +19,8 @@ private JourneyUris() { public static final String JOURNEY_CHECK_EXISTING_IDENTITY_PATH = "/journey/check-existing-identity"; public static final String JOURNEY_CHECK_GPG45_SCORE_PATH = "/journey/check-gpg45-score"; + public static final String JOURNEY_CHECK_REVERIFICATION_IDENTITY_PATH = + "/journey/check-reverification-identity"; public static final String JOURNEY_COI_CHECK_FAILED_PATH = "/journey/coi-check-failed"; public static final String JOURNEY_COI_CHECK_PASSED_PATH = "/journey/coi-check-passed"; public static final String JOURNEY_DL_AUTH_SOURCE_CHECK_PATH = "/journey/dl-auth-source-check"; @@ -32,6 +34,7 @@ private JourneyUris() { public static final String JOURNEY_F2F_FAIL_PATH = "/journey/f2f-fail"; public static final String JOURNEY_FAIL_WITH_CI_PATH = "/journey/fail-with-ci"; public static final String JOURNEY_FAIL_WITH_NO_CI_PATH = "/journey/fail-with-no-ci"; + public static final String JOURNEY_FOUND = "/journey/found"; public static final String JOURNEY_IDENTITY_STORED_PATH = "/journey/identity-stored"; public static final String JOURNEY_IN_MIGRATION_REUSE_PATH = "/journey/in-migration-reuse"; public static final String JOURNEY_INVALID_REQUEST_PATH = "/journey/invalid-request"; diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java index 5b930abff3..3f722680a8 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java @@ -1,6 +1,9 @@ package uk.gov.di.ipv.core.library.persistence.item; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; +import lombok.NoArgsConstructor; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbPartitionKey; import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbSecondaryPartitionKey; @@ -20,6 +23,9 @@ @DynamoDbBean @ExcludeFromGeneratedCoverageReport @Data +@Builder +@AllArgsConstructor +@NoArgsConstructor public class IpvSessionItem implements PersistenceItem { private String ipvSessionId; private String clientOAuthSessionId; diff --git a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/VcFixtures.java b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/VcFixtures.java index 7a03a5352a..70b9ee5a83 100644 --- a/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/VcFixtures.java +++ b/libs/common-services/src/test/java/uk/gov/di/ipv/core/library/fixtures/VcFixtures.java @@ -1122,6 +1122,13 @@ static VerifiableCredential vcF2fBankAccount() { Instant.ofEpochSecond(1652953080)); } + static VerifiableCredential l1AEvidenceVc() { + var evidence = TestVc.TestEvidence.builder().strengthScore(2).validityScore(2).build(); + + return generateVerifiableCredential( + TEST_SUBJECT, NINO, TestVc.builder().evidence(List.of(evidence)).build()); + } + static VerifiableCredential vcHmrcMigrationPCL200() throws Exception { TestVc.TestCredentialSubject credentialSubject = TestVc.TestCredentialSubject.builder() diff --git a/local-running/build.gradle b/local-running/build.gradle index 5d19619e75..e393d2548c 100644 --- a/local-running/build.gradle +++ b/local-running/build.gradle @@ -23,6 +23,7 @@ dependencies { project(":lambdas:check-coi"), project(":lambdas:check-existing-identity"), project(":lambdas:check-gpg45-score"), + project(":lambdas:check-reverification-identity"), project(":lambdas:evaluate-gpg45-scores"), project(":lambdas:initialise-ipv-session"), project(":lambdas:issue-client-access-token"), diff --git a/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java b/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java index d3a107ccfd..4a7c5a1d63 100644 --- a/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java +++ b/local-running/src/main/java/uk/gov/di/ipv/coreback/handlers/JourneyEngineHandler.java @@ -8,6 +8,7 @@ import uk.gov.di.ipv.core.checkcoi.CheckCoiHandler; import uk.gov.di.ipv.core.checkexistingidentity.CheckExistingIdentityHandler; import uk.gov.di.ipv.core.checkgpg45score.CheckGpg45ScoreHandler; +import uk.gov.di.ipv.core.checkreverificationidentity.CheckReverificationIdentityHandler; import uk.gov.di.ipv.core.evaluategpg45scores.EvaluateGpg45ScoresHandler; import uk.gov.di.ipv.core.library.domain.CriJourneyRequest; import uk.gov.di.ipv.core.library.domain.JourneyRequest; @@ -28,6 +29,7 @@ import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_CHECK_COI_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_CHECK_EXISTING_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_CHECK_GPG45_SCORE_PATH; +import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_CHECK_REVERIFICATION_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_EVALUATE_GPG45_SCORES_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_RESET_SESSION_IDENTITY_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_STORE_IDENTITY_PATH; @@ -55,6 +57,7 @@ public class JourneyEngineHandler { private final CallDcmawAsyncCriHandler callDcmawAsyncHandler; private final StoreIdentityHandler storeIdentityHandler; private final CheckCoiHandler checkCoiHandler; + private final CheckReverificationIdentityHandler checkReverificationIdentityHandler; public JourneyEngineHandler() throws IOException { this.configService = new YamlConfigService(); @@ -69,6 +72,8 @@ public JourneyEngineHandler() throws IOException { this.callDcmawAsyncHandler = new CallDcmawAsyncCriHandler(configService); this.storeIdentityHandler = new StoreIdentityHandler(configService); this.checkCoiHandler = new CheckCoiHandler(configService); + this.checkReverificationIdentityHandler = + new CheckReverificationIdentityHandler(configService); } public void journeyEngine(Context ctx) { @@ -125,6 +130,9 @@ private Map processJourneyStep( buildProcessRequest(ctx, processJourneyEventOutput), EMPTY_CONTEXT); case JOURNEY_CHECK_COI_PATH -> checkCoiHandler.handleRequest( buildProcessRequest(ctx, processJourneyEventOutput), EMPTY_CONTEXT); + case JOURNEY_CHECK_REVERIFICATION_IDENTITY_PATH -> checkReverificationIdentityHandler + .handleRequest( + buildProcessRequest(ctx, processJourneyEventOutput), EMPTY_CONTEXT); default -> { if (journeyStep.matches("/journey/cri/build-oauth-request/.*")) { yield buildCriOauthRequestHandler.handleRequest( diff --git a/settings.gradle b/settings.gradle index e8a56b8f1b..1b7d8b7170 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,7 @@ include "lambdas", "lambdas:check-coi", "lambdas:check-existing-identity", "lambdas:check-gpg45-score", + "lambdas:check-reverification-identity", "lambdas:evaluate-gpg45-scores", "lambdas:initialise-ipv-session", "lambdas:issue-client-access-token", From e4af7f4ce0f246d85c6907a1ef379b646d28cf49 Mon Sep 17 00:00:00 2001 From: Chris Wynne Date: Fri, 29 Nov 2024 12:02:04 +0000 Subject: [PATCH 2/6] PYIC-7076: Refactor vot matching to separate class This moves the logic for matching a VOT from a given set of VC to a shared class that is now used by the check-existing-identity handler as well as the check-reverification-identity class. --- .../CheckExistingIdentityHandler.java | 172 +++------- .../CheckExistingIdentityHandlerTest.java | 294 ++++++------------ .../CheckReverificationIdentityHandler.java | 56 ++-- ...heckReverificationIdentityHandlerTest.java | 96 ++++-- .../ipv/core/library/gpg45/Gpg45Scores.java | 2 +- libs/user-identity-service/build.gradle | 6 +- .../core/library/service/VotAndProfile.java | 8 + .../ipv/core/library/service/VotMatcher.java | 111 +++++++ .../core/library/service/VotMatcherTest.java | 164 ++++++++++ 9 files changed, 512 insertions(+), 397 deletions(-) create mode 100644 libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java create mode 100644 libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java create mode 100644 libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java diff --git a/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java b/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java index 327d0087e4..be3f959ea8 100644 --- a/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java +++ b/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java @@ -5,7 +5,6 @@ import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.StringMapMessage; import software.amazon.lambda.powertools.logging.Logging; import software.amazon.lambda.powertools.tracing.Tracing; import uk.gov.di.ipv.core.checkexistingidentity.exceptions.MitigationRouteException; @@ -24,7 +23,6 @@ import uk.gov.di.ipv.core.library.domain.JourneyResponse; import uk.gov.di.ipv.core.library.domain.VerifiableCredential; import uk.gov.di.ipv.core.library.enums.EvcsVCState; -import uk.gov.di.ipv.core.library.enums.OperationalProfile; import uk.gov.di.ipv.core.library.enums.Vot; import uk.gov.di.ipv.core.library.exception.EvcsServiceException; import uk.gov.di.ipv.core.library.exceptions.ConfigException; @@ -51,13 +49,13 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotMatcher; import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; import uk.gov.di.model.ContraIndicator; import java.text.ParseException; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; @@ -75,12 +73,8 @@ import static uk.gov.di.ipv.core.library.domain.Cri.HMRC_MIGRATION; import static uk.gov.di.ipv.core.library.domain.ProfileType.GPG45; import static uk.gov.di.ipv.core.library.domain.ProfileType.OPERATIONAL_HMRC; -import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.PENDING_RETURN; -import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_GPG45_PROFILE; -import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_MESSAGE_DESCRIPTION; -import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; import static uk.gov.di.ipv.core.library.helpers.RequestHelper.getIpAddress; import static uk.gov.di.ipv.core.library.helpers.RequestHelper.getIpvSessionId; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ENHANCED_VERIFICATION_F2F_FAIL_PATH; @@ -97,6 +91,7 @@ import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_REPROVE_IDENTITY_GPG45_MEDIUM_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_REUSE_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_REUSE_WITH_STORE_PATH; +import static uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper.filterVCBasedOnProfileType; /** Check Existing Identity response Lambda */ public class CheckExistingIdentityHandler @@ -140,6 +135,7 @@ public class CheckExistingIdentityHandler private final SessionCredentialsService sessionCredentialsService; private final EvcsService evcsService; private final EvcsMigrationService evcsMigrationService; + private final VotMatcher votMatcher; @SuppressWarnings({ "unused", @@ -158,7 +154,8 @@ public CheckExistingIdentityHandler( VerifiableCredentialService verifiableCredentialService, SessionCredentialsService sessionCredentialsService, EvcsService evcsService, - EvcsMigrationService evcsMigrationService) { + EvcsMigrationService evcsMigrationService, + VotMatcher votMatcher) { this.configService = configService; this.userIdentityService = userIdentityService; this.ipvSessionService = ipvSessionService; @@ -172,6 +169,7 @@ public CheckExistingIdentityHandler( this.sessionCredentialsService = sessionCredentialsService; this.evcsService = evcsService; this.evcsMigrationService = evcsMigrationService; + this.votMatcher = votMatcher; VcHelper.setConfigService(this.configService); } @@ -196,6 +194,8 @@ public CheckExistingIdentityHandler(ConfigService configService) { this.sessionCredentialsService = new SessionCredentialsService(configService); this.evcsService = new EvcsService(configService); this.evcsMigrationService = new EvcsMigrationService(configService); + this.votMatcher = + new VotMatcher(userIdentityService, gpg45ProfileEvaluator, cimitUtilityService); VcHelper.setConfigService(this.configService); } @@ -603,30 +603,47 @@ private Optional checkForProfileMatch( boolean areGpg45VcsCorrelated, List contraIndicators) throws ParseException, VerifiableCredentialException, EvcsServiceException { + + var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcBundle.credentials(), GPG45); + var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); + var operationalVcs = + VcHelper.filterVCBasedOnProfileType(vcBundle.credentials(), OPERATIONAL_HMRC); + // Check for attained vot from requested vots - var strongestAttainedVotFromVtr = - getStrongestAttainedVotForVtr( + var strongestAttainedVotAndProfileFromVtr = + votMatcher.matchFirstVot( clientOAuthSessionItem .getParsedVtr() .getRequestedVotsByStrengthDescending(), - vcBundle.credentials, - auditEventUser, - deviceInformation, + gpg45Vcs, + gpg45Scores, areGpg45VcsCorrelated, + operationalVcs, contraIndicators); - // vot achieved for vtr - if (strongestAttainedVotFromVtr.isPresent()) { - return Optional.of( - buildReuseResponse( - strongestAttainedVotFromVtr.get(), - ipvSessionItem, - vcBundle, - auditEventUser, - deviceInformation)); + if (strongestAttainedVotAndProfileFromVtr.isEmpty()) { + return Optional.empty(); + } + + var attainedVotAndProfile = strongestAttainedVotAndProfileFromVtr.get(); + + if (GPG45.equals(attainedVotAndProfile.vot().getProfileType())) { + sendProfileMatchedAuditEvent( + attainedVotAndProfile.gpg45Profile(), + gpg45Scores, + gpg45Vcs, + auditEventUser, + deviceInformation); } - return Optional.empty(); + // vot achieved for vtr + return Optional.of( + buildReuseResponse( + attainedVotAndProfile.vot(), + ipvSessionItem, + vcBundle, + auditEventUser, + deviceInformation)); } private JourneyResponse buildF2FNoMatchResponse( @@ -718,7 +735,7 @@ && allFraudVcsAreExpired(vcBundle.credentials)) { boolean isCurrentlyMigrating = ipvSessionItem.isInheritedIdentityReceivedThisSession(); sessionCredentialsService.persistCredentials( - VcHelper.filterVCBasedOnProfileType(vcBundle.credentials, OPERATIONAL_HMRC), + filterVCBasedOnProfileType(vcBundle.credentials, OPERATIONAL_HMRC), auditEventUser.getSessionId(), isCurrentlyMigrating); @@ -728,8 +745,7 @@ && allFraudVcsAreExpired(vcBundle.credentials)) { } sessionCredentialsService.persistCredentials( - VcHelper.filterVCBasedOnProfileType( - vcBundle.credentials, attainedVot.getProfileType()), + filterVCBasedOnProfileType(vcBundle.credentials, attainedVot.getProfileType()), auditEventUser.getSessionId(), false); @@ -807,110 +823,6 @@ private JourneyResponse buildErrorResponse(ErrorResponse errorResponse, Exceptio JOURNEY_ERROR_PATH, HttpStatus.SC_INTERNAL_SERVER_ERROR, errorResponse); } - private Optional getStrongestAttainedVotForVtr( - List requestedVotsByStrength, - List vcs, - AuditEventUser auditEventUser, - String deviceInformation, - boolean areGpg45VcsCorrelated, - List contraIndicators) - throws ParseException { - - for (Vot requestedVot : requestedVotsByStrength) { - boolean requestedVotAttained = false; - if (requestedVot.getProfileType().equals(GPG45)) { - if (areGpg45VcsCorrelated) { - requestedVotAttained = - achievedWithGpg45Profile( - requestedVot, - VcHelper.filterVCBasedOnProfileType(vcs, GPG45), - auditEventUser, - deviceInformation, - contraIndicators); - } - } else { - requestedVotAttained = hasOperationalProfileVc(requestedVot, vcs, contraIndicators); - } - - if (requestedVotAttained) { - return Optional.of(requestedVot); - } - } - return Optional.empty(); - } - - private boolean achievedWithGpg45Profile( - Vot requestedVot, - List vcs, - AuditEventUser auditEventUser, - String deviceInformation, - List contraIndicators) - throws ParseException { - Gpg45Scores gpg45Scores = gpg45ProfileEvaluator.buildScore(vcs); - Optional matchedGpg45Profile = - !userIdentityService.checkRequiresAdditionalEvidence(vcs) - ? gpg45ProfileEvaluator.getFirstMatchingProfile( - gpg45Scores, requestedVot.getSupportedGpg45Profiles()) - : Optional.empty(); - - var isBreaching = - cimitUtilityService.isBreachingCiThreshold(contraIndicators, requestedVot); - - // Successful match - if (matchedGpg45Profile.isPresent() && !isBreaching) { - var gpg45Credentials = new ArrayList(); - for (var vc : vcs) { - if (!VcHelper.isOperationalProfileVc(vc)) { - gpg45Credentials.add(vc); - } - } - LOGGER.info( - LogHelper.buildLogMessage("GPG45 profile has been met.") - .with( - LOG_GPG45_PROFILE.getFieldName(), - matchedGpg45Profile.get().getLabel())); - sendProfileMatchedAuditEvent( - matchedGpg45Profile.get(), - gpg45Scores, - gpg45Credentials, - auditEventUser, - deviceInformation); - - return true; - } - return false; - } - - private boolean hasOperationalProfileVc( - Vot requestedVot, - List vcs, - List contraIndicators) - throws ParseException { - for (var vc : vcs) { - String credentialVot = vc.getClaimsSet().getStringClaim(VOT_CLAIM_NAME); - Optional matchedOperationalProfile = - requestedVot.getSupportedOperationalProfiles().stream() - .map(OperationalProfile::name) - .filter(profileName -> profileName.equals(credentialVot)) - .findFirst(); - - var isBreaching = - cimitUtilityService.isBreachingCiThreshold(contraIndicators, requestedVot); - - // Successful match - if (matchedOperationalProfile.isPresent() && !isBreaching) { - LOGGER.info( - new StringMapMessage() - .with( - LOG_MESSAGE_DESCRIPTION.getFieldName(), - "Operational profile matched") - .with(LOG_VOT.getFieldName(), requestedVot)); - return true; - } - } - return false; - } - private void sendProfileMatchedAuditEvent( Gpg45Profile gpg45Profile, Gpg45Scores gpg45Scores, diff --git a/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java b/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java index aa81bedb91..59a8e78859 100644 --- a/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java +++ b/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java @@ -38,14 +38,10 @@ import uk.gov.di.ipv.core.library.domain.VerifiableCredential; import uk.gov.di.ipv.core.library.enums.EvcsVCState; import uk.gov.di.ipv.core.library.enums.Vot; -import uk.gov.di.ipv.core.library.exception.EvcsServiceException; import uk.gov.di.ipv.core.library.exceptions.ConfigException; -import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; -import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.UnrecognisedCiException; import uk.gov.di.ipv.core.library.exceptions.VerifiableCredentialException; import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; -import uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile; import uk.gov.di.ipv.core.library.helpers.SecureTokenHelper; import uk.gov.di.ipv.core.library.helpers.TestVc; import uk.gov.di.ipv.core.library.journeys.JourneyUris; @@ -62,6 +58,8 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotAndProfile; +import uk.gov.di.ipv.core.library.service.VotMatcher; import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; @@ -107,6 +105,8 @@ import static uk.gov.di.ipv.core.library.enums.EvcsVCState.PENDING_RETURN; import static uk.gov.di.ipv.core.library.enums.Vot.P1; import static uk.gov.di.ipv.core.library.enums.Vot.P2; +import static uk.gov.di.ipv.core.library.enums.Vot.PCL200; +import static uk.gov.di.ipv.core.library.enums.Vot.PCL250; import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.EC_PRIVATE_KEY_JWK; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.DCMAW_EVIDENCE_VRI_CHECK; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.EXPIRED_M1A_EXPERIAN_FRAUD_VC; @@ -121,6 +121,8 @@ import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcF2fM1a; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL200; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcVerificationM1a; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.M1A; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.M1B; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ENHANCED_VERIFICATION_F2F_FAIL_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ENHANCED_VERIFICATION_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_F2F_FAIL_PATH; @@ -195,6 +197,7 @@ class CheckExistingIdentityHandlerTest { @Mock private EvcsService mockEvcsService; @Mock private SessionCredentialsService mockSessionCredentialService; @Mock private EvcsMigrationService mockEvcsMigrationService; + @Mock private VotMatcher mockVotMatcher; @InjectMocks private CheckExistingIdentityHandler checkExistingIdentityHandler; @Spy private IpvSessionItem ipvSessionItem; @@ -205,7 +208,7 @@ class CheckExistingIdentityHandlerTest { @BeforeAll static void setUp() throws Exception { jwtSigner = createJwtSigner(); - pcl200Vc = createOperationalProfileVc(Vot.PCL200); + pcl200Vc = createOperationalProfileVc(PCL200); pcl250Vc = createOperationalProfileVc(Vot.PCL250); } @@ -345,9 +348,14 @@ void shouldReturnJourneyReuseResponseIfScoresSatisfyM1AGpg45Profile_alsoStoreVcs when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, hmrcMigrationVC)); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1A)); + when(mockVotMatcher.matchFirstVot( + List.of(P2), + List.of(gpg45Vc), + null, + true, + List.of(hmrcMigrationVC), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -384,8 +392,7 @@ void shouldReturnJourneyReuseResponseIfScoresSatisfyM1AGpg45Profile_alsoStoreVcs @Test void shouldReturnJourneyReuseUpdateResponseIfVcIsF2fAndHasPendingReturnInEvcs() - throws CredentialParseException, EvcsServiceException, - HttpResponseExceptionWithErrorBody, VerifiableCredentialException { + throws Exception { when(configService.enabled(EVCS_WRITE_ENABLED)).thenReturn(true); when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(true); var vcs = new ArrayList<>(List.of(gpg45Vc, vcF2fM1a())); @@ -395,9 +402,8 @@ void shouldReturnJourneyReuseUpdateResponseIfVcIsF2fAndHasPendingReturnInEvcs() .thenReturn(Map.of(PENDING_RETURN, vcs)); when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1A)); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, null, true, List.of(), List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -434,8 +440,7 @@ void shouldIncludeCurrentInheritedIdentityInVcBundleWhenPendingReturn() throws E @Test void shouldReturnJourneyReuseStoreResponseIfVcIsF2fAndHasPartiallyMigratedVcs() - throws CredentialParseException, EvcsServiceException, - HttpResponseExceptionWithErrorBody, VerifiableCredentialException { + throws Exception { when(configService.enabled(EVCS_WRITE_ENABLED)).thenReturn(true); when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(true); var f2fVc1 = vcF2fM1a(); @@ -450,9 +455,14 @@ void shouldReturnJourneyReuseStoreResponseIfVcIsF2fAndHasPartiallyMigratedVcs() .thenReturn(Map.of(PENDING_RETURN, new ArrayList<>(List.of(f2fVc1, f2fVc2)))); when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1A)); + when(mockVotMatcher.matchFirstVot( + List.of(P2), + List.of(f2fVc1, f2fVc2, f2fVc3), + null, + true, + List.of(), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -467,9 +477,9 @@ void shouldReturnJourneyReuseStoreResponseIfVcIsF2fAndHasPartiallyMigratedVcs() @Test void shouldReturnJourneyReuseResponseIfScoresSatisfyM1BGpg45Profile() throws Exception { - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); + when(mockVotMatcher.matchFirstVot( + List.of(P2), List.of(), null, true, List.of(), List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -500,7 +510,16 @@ void shouldReturnJourneyOpProfileReuseResponseIfPCL200RequestedAndMetWhenNotInMi throws Exception { when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, pcl200Vc)); - clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name(), Vot.PCL200.name())); + when(mockVotMatcher.matchFirstVot( + List.of(P2, PCL250, PCL200), + List.of(gpg45Vc), + null, + false, + List.of(pcl200Vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL200, null))); + + clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name(), PCL200.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(false); JourneyResponse journeyResponse = @@ -514,11 +533,11 @@ void shouldReturnJourneyOpProfileReuseResponseIfPCL200RequestedAndMetWhenNotInMi .persistCredentials(List.of(pcl200Vc), ipvSessionItem.getIpvSessionId(), false); InOrder inOrder = inOrder(ipvSessionItem, ipvSessionService); - inOrder.verify(ipvSessionItem).setVot(Vot.PCL200); + inOrder.verify(ipvSessionItem).setVot(PCL200); inOrder.verify(ipvSessionService).updateIpvSession(ipvSessionItem); inOrder.verify(ipvSessionItem, never()).setVot(any()); - assertEquals(Vot.PCL200, ipvSessionItem.getVot()); - assertEquals(Vot.PCL200, ipvSessionItem.getTargetVot()); + assertEquals(PCL200, ipvSessionItem.getVot()); + assertEquals(PCL200, ipvSessionItem.getTargetVot()); } @Test // User returning after migration @@ -526,6 +545,15 @@ void shouldReturnJourneyOpProfileReuseResponseIfPCL250RequestedAndMetWhenNotInMi throws Exception { when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, pcl250Vc)); + when(mockVotMatcher.matchFirstVot( + List.of(P2, PCL250), + List.of(gpg45Vc), + null, + false, + List.of(pcl250Vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); + clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(false); @@ -548,10 +576,17 @@ void shouldReturnJourneyOpProfileReuseResponseIfPCL250RequestedAndMetWhenNotInMi } @Test // User returning after migration - void shouldReturnJourneyOpProfileReuseResponseIfOpProfileAndPendingF2F() - throws CredentialParseException { + void shouldReturnJourneyOpProfileReuseResponseIfOpProfileAndPendingF2F() throws Exception { when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); when(mockVerifiableCredentialService.getVcs(any())).thenReturn(List.of(pcl250Vc)); + when(mockVotMatcher.matchFirstVot( + List.of(P2, PCL250), + List.of(), + null, + false, + List.of(pcl250Vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(false); @@ -575,8 +610,17 @@ void shouldReturnJourneyOpProfileReuseResponseIfOpProfileAndPendingF2F() void shouldReturnJourneyInMigrationReuseResponseIfPCL200RequestedAndMet() throws Exception { when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, pcl200Vc)); + when(mockVotMatcher.matchFirstVot( + List.of(P2, PCL250, PCL200), + List.of(gpg45Vc), + null, + false, + List.of(pcl200Vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL200, null))); + ipvSessionItem.setInheritedIdentityReceivedThisSession(true); - clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name(), Vot.PCL200.name())); + clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name(), PCL200.name())); JourneyResponse journeyResponse = toResponseClass( @@ -589,17 +633,26 @@ void shouldReturnJourneyInMigrationReuseResponseIfPCL200RequestedAndMet() throws .persistCredentials(List.of(pcl200Vc), ipvSessionItem.getIpvSessionId(), true); InOrder inOrder = inOrder(ipvSessionItem, ipvSessionService); - inOrder.verify(ipvSessionItem).setVot(Vot.PCL200); + inOrder.verify(ipvSessionItem).setVot(PCL200); inOrder.verify(ipvSessionService).updateIpvSession(ipvSessionItem); inOrder.verify(ipvSessionItem, never()).setVot(any()); - assertEquals(Vot.PCL200, ipvSessionItem.getVot()); - assertEquals(Vot.PCL200, ipvSessionItem.getTargetVot()); + assertEquals(PCL200, ipvSessionItem.getVot()); + assertEquals(PCL200, ipvSessionItem.getTargetVot()); } @Test // User in process of migration void shouldReturnJourneyInMigrationReuseResponseIfPCL250RequestedAndMet() throws Exception { when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, pcl250Vc)); + when(mockVotMatcher.matchFirstVot( + List.of(P2, PCL250), + List.of(gpg45Vc), + null, + true, + List.of(pcl250Vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); + when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(true); @@ -623,89 +676,12 @@ void shouldReturnJourneyInMigrationReuseResponseIfPCL250RequestedAndMet() throws assertEquals(Vot.PCL250, ipvSessionItem.getTargetVot()); } - @Test - void shouldMatchStrongestVotRegardlessOfVtrOrder() throws Exception { - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); - when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); - - clientOAuthSessionItem.setVtr(List.of(Vot.PCL250.name(), Vot.PCL200.name(), P2.name())); - - JourneyResponse journeyResponse = - toResponseClass( - checkExistingIdentityHandler.handleRequest(event, context), - JourneyResponse.class); - - assertEquals(JOURNEY_REUSE, journeyResponse); - - verify(auditService, times(2)).sendAuditEvent(auditEventArgumentCaptor.capture()); - assertEquals( - AuditEventTypes.IPV_IDENTITY_REUSE_COMPLETE, - auditEventArgumentCaptor.getValue().getEventName()); - verify(clientOAuthSessionDetailsService, times(1)).getClientOAuthSession(any()); - - verify(ipvSessionService, times(3)).updateIpvSession(ipvSessionItem); - - InOrder inOrder = inOrder(ipvSessionItem, ipvSessionService); - inOrder.verify(ipvSessionItem).setTargetVot(P2); - inOrder.verify(ipvSessionItem).setVot(P2); - inOrder.verify(ipvSessionService).updateIpvSession(ipvSessionItem); - inOrder.verify(ipvSessionItem, never()).setVot(any()); - assertEquals(P2, ipvSessionItem.getVot()); - assertEquals(Vot.PCL200, ipvSessionItem.getTargetVot()); - } - - @Test - void shouldMatchWeakerVotIfStrongerVotHasBreachingCi() throws Exception { - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P1.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.L1A)); - when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); - when(configService.enabled(EVCS_WRITE_ENABLED)).thenReturn(false); - when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(false); - when(configService.enabled(P1_JOURNEYS_ENABLED)).thenReturn(true); - - when(cimitService.getContraIndicators( - TEST_USER_ID, TEST_JOURNEY_ID, TEST_CLIENT_SOURCE_IP)) - .thenReturn(List.of()); - when(cimitUtilityService.isBreachingCiThreshold(any(), eq(Vot.P2))).thenReturn(true); - when(cimitUtilityService.isBreachingCiThreshold(any(), eq(Vot.P1))).thenReturn(false); - - clientOAuthSessionItem.setVtr(List.of(Vot.P1.name(), P2.name())); - - JourneyResponse journeyResponse = - toResponseClass( - checkExistingIdentityHandler.handleRequest(event, context), - JourneyResponse.class); - - assertEquals(JOURNEY_REUSE, journeyResponse); - - verify(auditService, times(2)).sendAuditEvent(auditEventArgumentCaptor.capture()); - assertEquals( - AuditEventTypes.IPV_IDENTITY_REUSE_COMPLETE, - auditEventArgumentCaptor.getValue().getEventName()); - verify(clientOAuthSessionDetailsService, times(1)).getClientOAuthSession(any()); - - verify(ipvSessionService, times(3)).updateIpvSession(ipvSessionItem); - - InOrder inOrder = inOrder(ipvSessionItem, ipvSessionService); - inOrder.verify(ipvSessionItem).setVot(P1); - inOrder.verify(ipvSessionService).updateIpvSession(ipvSessionItem); - inOrder.verify(ipvSessionItem, never()).setVot(any()); - assertEquals(P1, ipvSessionItem.getVot()); - assertEquals(P1, ipvSessionItem.getTargetVot()); - } - @Test void shouldReturnErrorResponseIfVcCanNotBeStoredInSessionCredentialTable() throws Exception { - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1A)); + when(mockVotMatcher.matchFirstVot( + List.of(P2), List.of(), null, true, List.of(), List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); doThrow( new VerifiableCredentialException( @@ -741,7 +717,7 @@ void shouldReturnNoMatchForF2FCompleteAndVCsDoNotCorrelate() throws Exception { when(criResponseService.getFaceToFaceRequest(TEST_USER_ID)).thenReturn(criResponseItem); when(userIdentityService.areVcsCorrelated(any())).thenReturn(false); - clientOAuthSessionItem.setVtr(List.of(Vot.PCL250.name(), Vot.PCL200.name(), P2.name())); + clientOAuthSessionItem.setVtr(List.of(Vot.PCL250.name(), PCL200.name(), P2.name())); JourneyResponse journeyResponse = toResponseClass( @@ -769,7 +745,7 @@ void shouldNoMatchStrongestVotAndAlsoVCsDoNotCorrelate() throws Exception { when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(List.of(vcF2fM1a())); when(userIdentityService.areVcsCorrelated(any())).thenReturn(false); - clientOAuthSessionItem.setVtr(List.of(Vot.PCL250.name(), Vot.PCL200.name(), P2.name())); + clientOAuthSessionItem.setVtr(List.of(Vot.PCL250.name(), PCL200.name(), P2.name())); JourneyResponse journeyResponse = toResponseClass( @@ -818,9 +794,6 @@ void shouldReturnJourneyIpvGpg45MediumResponseIfScoresDoNotSatisfyM1AGpg45Profil when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(VCS_FROM_STORE); when(criResponseService.getFaceToFaceRequest(TEST_USER_ID)).thenReturn(null); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.empty()); when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); @@ -1145,70 +1118,6 @@ void shouldReturn500IfUnrecognisedCiReceived() throws Exception { assertEquals(ErrorResponse.UNRECOGNISED_CI_CODE.getMessage(), response.getMessage()); } - @Test - void shouldReturnJourneyReuseResponseIfCheckRequiresAdditionalEvidenceResponseFalse() - throws Exception { - when(ipvSessionService.getIpvSessionWithRetry(TEST_SESSION_ID)).thenReturn(ipvSessionItem); - when(criResponseService.getFaceToFaceRequest(TEST_USER_ID)).thenReturn(null); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); - when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(clientOAuthSessionItem); - when(userIdentityService.checkRequiresAdditionalEvidence(any())).thenReturn(false); - when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); - JourneyResponse journeyResponse = - toResponseClass( - checkExistingIdentityHandler.handleRequest(event, context), - JourneyResponse.class); - - assertEquals(JOURNEY_REUSE, journeyResponse); - - verify(auditService, times(2)).sendAuditEvent(auditEventArgumentCaptor.capture()); - assertEquals( - AuditEventTypes.IPV_GPG45_PROFILE_MATCHED, - auditEventArgumentCaptor.getAllValues().get(0).getEventName()); - assertEquals( - AuditEventTypes.IPV_IDENTITY_REUSE_COMPLETE, - auditEventArgumentCaptor.getAllValues().get(1).getEventName()); - verify(clientOAuthSessionDetailsService, times(1)).getClientOAuthSession(any()); - - verify(ipvSessionService, times(3)).updateIpvSession(ipvSessionItem); - - InOrder inOrder = inOrder(ipvSessionItem, ipvSessionService); - inOrder.verify(ipvSessionItem).setVot(P2); - inOrder.verify(ipvSessionService).updateIpvSession(ipvSessionItem); - inOrder.verify(ipvSessionItem, never()).setVot(any()); - assertEquals(P2, ipvSessionItem.getVot()); - verify(mockEvcsMigrationService, times(0)).migrateExistingIdentity(any(), any()); - assertEquals(P2, ipvSessionItem.getTargetVot()); - } - - @Test - void shouldReturnJourneyIpvGpg45MediumResponseIfCheckRequiresAdditionalEvidenceResponseTrue() - throws Exception { - when(ipvSessionService.getIpvSessionWithRetry(TEST_SESSION_ID)).thenReturn(ipvSessionItem); - when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(VCS_FROM_STORE); - when(criResponseService.getFaceToFaceRequest(TEST_USER_ID)).thenReturn(null); - when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(clientOAuthSessionItem); - when(userIdentityService.checkRequiresAdditionalEvidence(any())).thenReturn(true); - when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); - - JourneyResponse journeyResponse = - toResponseClass( - checkExistingIdentityHandler.handleRequest(event, context), - JourneyResponse.class); - - assertEquals(JOURNEY_IPV_GPG45_MEDIUM, journeyResponse); - - verify(clientOAuthSessionDetailsService, times(1)).getClientOAuthSession(any()); - - verify(ipvSessionItem, never()).setVot(any()); - assertNull(ipvSessionItem.getVot()); - assertEquals(P2, ipvSessionItem.getTargetVot()); - } - @Test void shouldReturnJourneyFailedWithCiIfTrueCiMitigationJourneyStepPresentAndNoMitigationJourneyStep() @@ -1418,12 +1327,10 @@ void shouldReturnJourneyRepeatFraudCheckResponseIfExpiredFraudAndFlagIsTrue() th M1B_DCMAW_VC); when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(vcs); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, null, true, List.of(), List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); - when(userIdentityService.checkRequiresAdditionalEvidence(any())).thenReturn(false); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); when(configService.enabled(EVCS_WRITE_ENABLED)).thenReturn(false); when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(false); @@ -1461,13 +1368,10 @@ void shouldReturnJourneyRepeatFraudCheckResponseIfExpiredFraudAndFlagIsTrue() th vcVerificationM1a(), M1B_DCMAW_VC); when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(vcs); - - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, null, true, List.of(), List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); - when(userIdentityService.checkRequiresAdditionalEvidence(any())).thenReturn(false); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); when(configService.enabled(RESET_IDENTITY)).thenReturn(false); when(configService.enabled(REPEAT_FRAUD_CHECK)).thenReturn(true); @@ -1502,12 +1406,12 @@ void shouldNotReturnJourneyRepeatFraudCheckResponseIfNotExpiredFraudAndFlagIsTru when(ipvSessionService.getIpvSessionWithRetry(TEST_SESSION_ID)).thenReturn(ipvSessionItem); when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(VCS_FROM_STORE); - when(gpg45ProfileEvaluator.getFirstMatchingProfile( - any(), eq(P2.getSupportedGpg45Profiles()))) - .thenReturn(Optional.of(Gpg45Profile.M1B)); + when(mockVotMatcher.matchFirstVot( + List.of(P2), VCS_FROM_STORE, null, true, List.of(), List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); + when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); - when(userIdentityService.checkRequiresAdditionalEvidence(any())).thenReturn(false); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); when(configService.enabled(EVCS_WRITE_ENABLED)).thenReturn(false); when(configService.enabled(EVCS_READ_ENABLED)).thenReturn(false); @@ -1559,10 +1463,10 @@ void shouldLogRuntimeExceptionsAndRethrow() throws Exception { private static Stream votAndVtrCombinationsThatShouldStartIpvJourney() { return Stream.of( Arguments.of(List.of("P2"), Optional.empty()), - Arguments.of(List.of("P2"), Optional.of(Vot.PCL200)), + Arguments.of(List.of("P2"), Optional.of(PCL200)), Arguments.of(List.of("P2"), Optional.of(Vot.PCL250)), Arguments.of(List.of("P2", "PCL250"), Optional.empty()), - Arguments.of(List.of("P2", "PCL250"), Optional.of(Vot.PCL200)), + Arguments.of(List.of("P2", "PCL250"), Optional.of(PCL200)), Arguments.of(List.of("P2", "PCL250", "PCL200"), Optional.empty())); } diff --git a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java index 665183d95e..484f659287 100644 --- a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java +++ b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java @@ -11,7 +11,6 @@ import uk.gov.di.ipv.core.library.domain.JourneyRequest; import uk.gov.di.ipv.core.library.domain.JourneyResponse; import uk.gov.di.ipv.core.library.domain.VerifiableCredential; -import uk.gov.di.ipv.core.library.enums.Vot; import uk.gov.di.ipv.core.library.exception.EvcsServiceException; import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; @@ -24,6 +23,7 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotMatcher; import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; import java.text.ParseException; @@ -38,11 +38,8 @@ import static uk.gov.di.ipv.core.library.domain.ProfileType.GPG45; import static uk.gov.di.ipv.core.library.domain.ProfileType.OPERATIONAL_HMRC; import static uk.gov.di.ipv.core.library.domain.ReverificationFailureCode.NO_IDENTITY_AVAILABLE; -import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; import static uk.gov.di.ipv.core.library.enums.Vot.SUPPORTED_VOTS_BY_DESCENDING_STRENGTH; -import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_PROFILE; -import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; import static uk.gov.di.ipv.core.library.helpers.RequestHelper.getIpvSessionId; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ERROR_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_FOUND; @@ -61,6 +58,7 @@ public class CheckReverificationIdentityHandler private final EvcsService evcsService; private final UserIdentityService userIdentityService; private final Gpg45ProfileEvaluator gpg45ProfileEvaluator; + private final VotMatcher votMatcher; public CheckReverificationIdentityHandler( ConfigService configService, @@ -68,13 +66,15 @@ public CheckReverificationIdentityHandler( ClientOAuthSessionDetailsService clientOAuthSessionDetailsService, EvcsService evcsService, UserIdentityService userIdentityService, - Gpg45ProfileEvaluator gpg45ProfileEvaluator) { + Gpg45ProfileEvaluator gpg45ProfileEvaluator, + VotMatcher votMatcher) { this.configService = configService; this.ipvSessionService = ipvSessionService; this.clientSessionService = clientOAuthSessionDetailsService; this.evcsService = evcsService; this.userIdentityService = userIdentityService; this.gpg45ProfileEvaluator = gpg45ProfileEvaluator; + this.votMatcher = votMatcher; } @ExcludeFromGeneratedCoverageReport @@ -90,6 +90,7 @@ public CheckReverificationIdentityHandler(ConfigService configService) { this.evcsService = new EvcsService(configService); this.userIdentityService = new UserIdentityService(configService); this.gpg45ProfileEvaluator = new Gpg45ProfileEvaluator(); + this.votMatcher = new VotMatcher(userIdentityService, gpg45ProfileEvaluator); } @Tracing @@ -159,39 +160,20 @@ private boolean vcsContainIdentity(List vcs) var gpg45VcsCorrelated = userIdentityService.areVcsCorrelated(gpg45Vcs); var operationalVcs = VcHelper.filterVCBasedOnProfileType(vcs, OPERATIONAL_HMRC); - for (var vot : SUPPORTED_VOTS_BY_DESCENDING_STRENGTH) { - if (GPG45.equals(vot.getProfileType()) && gpg45VcsCorrelated) { - var matchedProfile = - gpg45ProfileEvaluator.getFirstMatchingProfile( - gpg45Scores, vot.getSupportedGpg45Profiles()); - if (matchedProfile.isPresent()) { - LOGGER.info( - LogHelper.buildLogMessage("Identity for reverification found") - .with(LOG_VOT.getFieldName(), vot) - .with(LOG_PROFILE.getFieldName(), matchedProfile.get())); - return true; - } - } - if (OPERATIONAL_HMRC.equals(vot.getProfileType()) - && vcsContainOperationalVot(operationalVcs, vot)) { - LOGGER.info( - LogHelper.buildLogMessage("Identity for reverification found") - .with(LOG_VOT.getFieldName(), vot)); - return true; - } + var matchedVot = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + gpg45Vcs, + gpg45Scores, + gpg45VcsCorrelated, + operationalVcs, + List.of()); + + if (matchedVot.isEmpty()) { + LOGGER.info(LogHelper.buildLogMessage("No identity for reverification found")); + return false; } - LOGGER.info(LogHelper.buildLogMessage("No identity for reverification found")); - return false; - } - private boolean vcsContainOperationalVot(List vcs, Vot vot) - throws ParseException { - for (var vc : vcs) { - var credentialVot = vc.getClaimsSet().getStringClaim(VOT_CLAIM_NAME); - if (vot.name().equals(credentialVot)) { - return true; - } - } - return false; + return true; } } diff --git a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java index 1bfa1233ca..5f5cced54f 100644 --- a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java +++ b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java @@ -1,7 +1,6 @@ package uk.gov.di.ipv.core.checkreverificationidentity; import com.amazonaws.services.lambda.runtime.Context; -import com.nimbusds.jwt.JWTClaimsSet; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -19,6 +18,7 @@ import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; import uk.gov.di.ipv.core.library.service.ClientOAuthSessionDetailsService; @@ -26,9 +26,13 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotAndProfile; +import uk.gov.di.ipv.core.library.service.VotMatcher; import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; +import java.text.ParseException; import java.util.List; +import java.util.Optional; import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_NOT_FOUND; import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_SERVER_ERROR; @@ -41,7 +45,6 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static uk.gov.di.ipv.core.library.domain.Cri.HMRC_MIGRATION; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.CLIENT_OAUTH_SESSION_NOT_FOUND; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_NAME_CORRELATION; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS; @@ -49,8 +52,12 @@ import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.RECEIVED_NON_200_RESPONSE_STATUS_CODE; import static uk.gov.di.ipv.core.library.domain.ReverificationFailureCode.NO_IDENTITY_AVAILABLE; -import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; +import static uk.gov.di.ipv.core.library.enums.Vot.P1; +import static uk.gov.di.ipv.core.library.enums.Vot.P2; +import static uk.gov.di.ipv.core.library.enums.Vot.PCL200; +import static uk.gov.di.ipv.core.library.enums.Vot.PCL250; +import static uk.gov.di.ipv.core.library.enums.Vot.SUPPORTED_VOTS_BY_DESCENDING_STRENGTH; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1B_DCMAW_VC; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.VC_ADDRESS; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.l1AEvidenceVc; @@ -58,6 +65,8 @@ import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL200; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL250; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcVerificationM1a; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.L1A; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.M1A; @ExtendWith(MockitoExtension.class) class CheckReverificationIdentityHandlerTest { @@ -75,7 +84,6 @@ class CheckReverificationIdentityHandlerTest { private IpvSessionItem ipvSession; private ClientOAuthSessionItem clientSession; - @Mock private VerifiableCredential mockVc; @Mock private Context mockContext; @Mock private ConfigService mockConfigService; @Mock private IpvSessionService mockIpvSessionService; @@ -83,6 +91,7 @@ class CheckReverificationIdentityHandlerTest { @Mock private EvcsService mockEvcsService; @Mock private UserIdentityService mockUserIdentityService; @Spy private Gpg45ProfileEvaluator mockGpg45Evaluator; + @Mock private VotMatcher mockVotMatcher; @InjectMocks private CheckReverificationIdentityHandler checkReverificationIdentityHandler; @BeforeAll @@ -128,6 +137,14 @@ void shouldReturnJourneyFoundIfUserHasP2Identity() throws Exception { when(mockEvcsService.getVerifiableCredentials( TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) .thenReturn(p2Vcs); + when(mockVotMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + List.of(M1B_DCMAW_VC, VC_ADDRESS, m1BFraudVc), + new Gpg45Scores(3, 2, 1, 2, 2), + true, + List.of(pcl250vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -142,6 +159,14 @@ void shouldReturnJourneyFoundIfUserHasP1Identity() throws Exception { when(mockEvcsService.getVerifiableCredentials( TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) .thenReturn(p1Vcs); + when(mockVotMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + p1Vcs, + new Gpg45Scores(2, 2, 0, 2, 2), + true, + List.of(), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(P1, L1A))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -154,6 +179,14 @@ void shouldReturnJourneyFoundIfUserHasPcl250Identity() throws Exception { when(mockEvcsService.getVerifiableCredentials( TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) .thenReturn(List.of(pcl250vc)); + when(mockVotMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + List.of(), + Gpg45Scores.builder().build(), + false, + List.of(pcl250vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -166,6 +199,14 @@ void shouldReturnJourneyFoundIfUserHasPcl200Identity() throws Exception { when(mockEvcsService.getVerifiableCredentials( TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) .thenReturn(List.of(pcl200vc)); + when(mockVotMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + List.of(), + Gpg45Scores.builder().build(), + false, + List.of(pcl200vc), + List.of())) + .thenReturn(Optional.of(new VotAndProfile(PCL200, null))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -174,28 +215,15 @@ void shouldReturnJourneyFoundIfUserHasPcl200Identity() throws Exception { } @Test - void shouldReturnJourneyNotFoundWhenNoVcs() throws Exception { - when(mockEvcsService.getVerifiableCredentials( - TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) - .thenReturn(List.of()); - - var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); - - assertEquals("/journey/not-found", response.get("journey")); - - var inorder = inOrder(ipvSession, mockIpvSessionService); - inorder.verify(ipvSession).setFailureCode(NO_IDENTITY_AVAILABLE); - inorder.verify(mockIpvSessionService).updateIpvSession(ipvSession); - inorder.verifyNoMoreInteractions(); - } - - @Test - void shouldReturnJourneyNotFoundIfVcsDontCorrelate() throws Exception { - var p2Vcs = List.of(M1B_DCMAW_VC, VC_ADDRESS, m1BFraudVc); - when(mockUserIdentityService.areVcsCorrelated(p2Vcs)).thenReturn(false); - when(mockEvcsService.getVerifiableCredentials( - TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) - .thenReturn(p2Vcs); + void shouldReturnJourneyNotFoundWhenNoVotMatched() throws Exception { + when(mockVotMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + List.of(), + Gpg45Scores.builder().build(), + false, + List.of(), + List.of())) + .thenReturn(Optional.empty()); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -298,16 +326,18 @@ void shouldReturnJourneyErrorIfFailureToDoCorrelationCheck() throws Exception { } @Test - void shouldReturnJourneyErrorIfFailureToParseVotFromOperationalVc() throws Exception { + void shouldReturnJourneyErrorIfErrorMatchingVot() throws Exception { when(mockIpvSessionService.getIpvSession(TEST_IPV_SESSION_ID)).thenReturn(ipvSession); when(mockClientSessionService.getClientOAuthSession(TEST_CLIENT_SESSION_ID)) .thenReturn(clientSession); - when(mockEvcsService.getVerifiableCredentials( - TEST_USER_ID, TEST_EVCS_ACCESS_TOKEN, CURRENT)) - .thenReturn(List.of(mockVc)); - when(mockVc.getCri()).thenReturn(HMRC_MIGRATION); - when(mockVc.getClaimsSet()) - .thenReturn(new JWTClaimsSet.Builder().claim(VOT_CLAIM_NAME, 101).build()); + when(mockVotMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + List.of(), + Gpg45Scores.builder().build(), + false, + List.of(), + List.of())) + .thenThrow(new ParseException("😬", 0)); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/gpg45/Gpg45Scores.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/gpg45/Gpg45Scores.java index 3bd7605f50..e33634ed91 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/gpg45/Gpg45Scores.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/gpg45/Gpg45Scores.java @@ -149,7 +149,7 @@ public int hashCode() { return Objects.hash(evidences, activity, fraud, verification); } - static class Builder { + public static class Builder { private List evidences = new ArrayList<>(); private int activity; diff --git a/libs/user-identity-service/build.gradle b/libs/user-identity-service/build.gradle index 7a388e04f7..b0d2a25516 100644 --- a/libs/user-identity-service/build.gradle +++ b/libs/user-identity-service/build.gradle @@ -11,7 +11,11 @@ dependencies { libs.powertoolsLogging, libs.powertoolsParameters, project(":libs:common-services"), - project(":libs:verifiable-credentials") + project(":libs:verifiable-credentials"), + project(":libs:gpg45-evaluator") + + compileOnly libs.lombok + annotationProcessor libs.lombok testImplementation libs.junitJupiter, libs.mockitoJunit, diff --git a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java new file mode 100644 index 0000000000..d54835b409 --- /dev/null +++ b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java @@ -0,0 +1,8 @@ +package uk.gov.di.ipv.core.library.service; + +import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; +import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile; + +@ExcludeFromGeneratedCoverageReport +public record VotAndProfile(Vot vot, Gpg45Profile gpg45Profile) {} diff --git a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java new file mode 100644 index 0000000000..6adfe68f2b --- /dev/null +++ b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java @@ -0,0 +1,111 @@ +package uk.gov.di.ipv.core.library.service; + +import lombok.AllArgsConstructor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; +import uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile; +import uk.gov.di.ipv.core.library.helpers.LogHelper; +import uk.gov.di.model.ContraIndicator; + +import java.text.ParseException; +import java.util.List; +import java.util.Optional; + +import static uk.gov.di.ipv.core.library.domain.ProfileType.GPG45; +import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; +import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_GPG45_PROFILE; +import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; + +@AllArgsConstructor +public class VotMatcher { + private static final Logger LOGGER = LogManager.getLogger(); + + private final UserIdentityService userIdentityService; + private final Gpg45ProfileEvaluator gpg45ProfileEvaluator; + private final CimitUtilityService cimitUtilityService; + + public VotMatcher( + UserIdentityService userIdentityService, Gpg45ProfileEvaluator gpg45ProfileEvaluator) { + this(userIdentityService, gpg45ProfileEvaluator, null); + } + + public Optional matchFirstVot( + List vots, + List gpg45Vcs, + Gpg45Scores gpg45Scores, + boolean areGpg45VcsCorrelated, + List operationalVcs, + List contraIndicators) + throws ParseException { + + for (Vot vot : vots) { + if (vot.getProfileType().equals(GPG45) && areGpg45VcsCorrelated) { + var matchedGpg45Profile = + achievedWithGpg45Profile(vot, gpg45Vcs, gpg45Scores, contraIndicators); + if (matchedGpg45Profile.isPresent()) { + return Optional.of(new VotAndProfile(vot, matchedGpg45Profile.get())); + } + } + if (hasOperationalProfileVc(vot, operationalVcs, contraIndicators)) { + return Optional.of(new VotAndProfile(vot, null)); + } + } + return Optional.empty(); + } + + private Optional achievedWithGpg45Profile( + Vot requestedVot, + List gpg45Vcs, + Gpg45Scores gpg45Scores, + List contraIndicators) { + Optional matchedGpg45Profile = + !userIdentityService.checkRequiresAdditionalEvidence(gpg45Vcs) + ? gpg45ProfileEvaluator.getFirstMatchingProfile( + gpg45Scores, requestedVot.getSupportedGpg45Profiles()) + : Optional.empty(); + + if (matchedGpg45Profile.isEmpty() || isBreaching(contraIndicators, requestedVot)) { + return Optional.empty(); + } + + // Successful match + LOGGER.info( + LogHelper.buildLogMessage("GPG45 profile has been met.") + .with(LOG_VOT.getFieldName(), requestedVot) + .with( + LOG_GPG45_PROFILE.getFieldName(), + matchedGpg45Profile.get().getLabel())); + return matchedGpg45Profile; + } + + private boolean hasOperationalProfileVc( + Vot requestedVot, + List vcs, + List contraIndicators) + throws ParseException { + for (var vc : vcs) { + var vcContainsVot = + requestedVot.name().equals(vc.getClaimsSet().getStringClaim(VOT_CLAIM_NAME)); + + if (!vcContainsVot || isBreaching(contraIndicators, requestedVot)) { + continue; + } + + // Successful match + LOGGER.info( + LogHelper.buildLogMessage("Operational profile matched") + .with(LOG_VOT.getFieldName(), requestedVot)); + return true; + } + return false; + } + + private boolean isBreaching(List contraIndicators, Vot vot) { + return !contraIndicators.isEmpty() + && cimitUtilityService.isBreachingCiThreshold(contraIndicators, vot); + } +} diff --git a/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java b/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java new file mode 100644 index 0000000000..54ff7fd691 --- /dev/null +++ b/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java @@ -0,0 +1,164 @@ +package uk.gov.di.ipv.core.library.service; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import uk.gov.di.ipv.core.library.domain.VerifiableCredential; +import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; +import uk.gov.di.model.ContraIndicator; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static uk.gov.di.ipv.core.library.enums.Vot.P1; +import static uk.gov.di.ipv.core.library.enums.Vot.P2; +import static uk.gov.di.ipv.core.library.enums.Vot.PCL200; +import static uk.gov.di.ipv.core.library.enums.Vot.PCL250; +import static uk.gov.di.ipv.core.library.enums.Vot.SUPPORTED_VOTS_BY_DESCENDING_STRENGTH; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcExperianFraudScoreTwo; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL200; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcHmrcMigrationPCL250; +import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.vcVerificationM1a; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.L1A; +import static uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile.M1A; + +@ExtendWith(MockitoExtension.class) +public class VotMatcherTest { + private static final Gpg45Scores GPG_45_SCORES = Gpg45Scores.builder().build(); + private static VerifiableCredential pcl200vc; + private static VerifiableCredential pcl250vc; + private static List gpg45Vcs; + + @Mock private UserIdentityService mockUseridentityService; + @Mock private Gpg45ProfileEvaluator mockGpg45ProfileEvaluator; + @Mock private CimitUtilityService mockCimitUtilityService; + @InjectMocks private VotMatcher votMatcher; + + @BeforeAll + public static void beforeAll() throws Exception { + gpg45Vcs = List.of(vcExperianFraudScoreTwo(), vcVerificationM1a()); + pcl200vc = vcHmrcMigrationPCL200(); + pcl250vc = vcHmrcMigrationPCL250(); + } + + @Test + void shouldReturnFirstMatchedGpg45VotAndProfile() throws Exception { + when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(false); + when(mockGpg45ProfileEvaluator.getFirstMatchingProfile( + GPG_45_SCORES, P2.getSupportedGpg45Profiles())) + .thenReturn(Optional.empty()); + when(mockGpg45ProfileEvaluator.getFirstMatchingProfile( + GPG_45_SCORES, P1.getSupportedGpg45Profiles())) + .thenReturn(Optional.of(L1A)); + + var votAndProfile = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + gpg45Vcs, + GPG_45_SCORES, + true, + List.of(), + List.of()); + + assertEquals(Optional.of(new VotAndProfile(P1, L1A)), votAndProfile); + } + + @Test + void shouldReturnFirstMatchedOperationalVot() throws Exception { + var operationalVcs = List.of(pcl200vc); + + when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(false); + + var votAndProfile = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + gpg45Vcs, + GPG_45_SCORES, + true, + operationalVcs, + List.of()); + + assertEquals(Optional.of(new VotAndProfile(PCL200, null)), votAndProfile); + } + + @Test + void shouldReturnEmptyOptionalIfNoVotMatched() throws Exception { + var votAndProfile = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + List.of(), + GPG_45_SCORES, + true, + List.of(), + List.of()); + + assertEquals(Optional.empty(), votAndProfile); + } + + @Test + void shouldMatchWeakerGpg45VotIfStrongerVotHasBreachingCi() throws Exception { + var contraIndicators = List.of(new ContraIndicator()); + + when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(false); + when(mockGpg45ProfileEvaluator.getFirstMatchingProfile( + GPG_45_SCORES, P2.getSupportedGpg45Profiles())) + .thenReturn(Optional.of(M1A)); + when(mockGpg45ProfileEvaluator.getFirstMatchingProfile( + GPG_45_SCORES, P1.getSupportedGpg45Profiles())) + .thenReturn(Optional.of(L1A)); + when(mockCimitUtilityService.isBreachingCiThreshold(contraIndicators, P2)).thenReturn(true); + + var votAndProfile = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + gpg45Vcs, + GPG_45_SCORES, + true, + List.of(), + contraIndicators); + + assertEquals(Optional.of(new VotAndProfile(P1, L1A)), votAndProfile); + } + + @Test + void shouldMatchWeakerOperationalVotIfStrongerVotHasBreachingCi() throws Exception { + var operationalVcs = List.of(pcl250vc, pcl200vc); + var contraIndicators = List.of(new ContraIndicator()); + + when(mockCimitUtilityService.isBreachingCiThreshold(contraIndicators, PCL250)) + .thenReturn(true); + + var votAndProfile = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + gpg45Vcs, + GPG_45_SCORES, + true, + operationalVcs, + contraIndicators); + + assertEquals(Optional.of(new VotAndProfile(PCL200, null)), votAndProfile); + } + + @Test + void shouldNotMatchGpg45VotIfRequiresAdditionalEvidence() throws Exception { + when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(true); + + var votAndProfile = + votMatcher.matchFirstVot( + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, + gpg45Vcs, + GPG_45_SCORES, + true, + List.of(), + List.of()); + + assertEquals(Optional.empty(), votAndProfile); + } +} From 00eb9ece1143931263bac1a200866ccb460ed23b Mon Sep 17 00:00:00 2001 From: Chris Wynne Date: Fri, 29 Nov 2024 15:56:29 +0000 Subject: [PATCH 3/6] PYIC-7076: Set target VOT in check-reverification-identity Rather than hardcoding reverification journeys to P2, we can determine it from the identity to be reverified. --- .../CheckReverificationIdentityHandler.java | 15 +++++++++---- ...heckReverificationIdentityHandlerTest.java | 21 +++++++++++++++++++ .../library/service/IpvSessionService.java | 15 ++++--------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java index 484f659287..66bc657c8c 100644 --- a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java +++ b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java @@ -23,12 +23,14 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; +import uk.gov.di.ipv.core.library.service.VotAndProfile; import uk.gov.di.ipv.core.library.service.VotMatcher; import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; import java.text.ParseException; import java.util.List; import java.util.Map; +import java.util.Optional; import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_NOT_FOUND; import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_SERVER_ERROR; @@ -118,12 +120,18 @@ public Map handleRequest(JourneyRequest request, Context context clientOAuthSession.getEvcsAccessToken(), CURRENT); - if (!vcsContainIdentity(vcs)) { + var reverificationIdentityVot = getReverificationIdentityVot(vcs); + if (reverificationIdentityVot.isEmpty()) { ipvSession.setFailureCode(NO_IDENTITY_AVAILABLE); ipvSessionService.updateIpvSession(ipvSession); return NOT_FOUND_RESPONSE; } + if (GPG45.equals(reverificationIdentityVot.get().vot().getProfileType())) { + ipvSession.setTargetVot(reverificationIdentityVot.get().vot()); + ipvSessionService.updateIpvSession(ipvSession); + } + return FOUND_RESPONSE; } catch (HttpResponseExceptionWithErrorBody | EvcsServiceException e) { @@ -153,7 +161,7 @@ public Map handleRequest(JourneyRequest request, Context context } } - private boolean vcsContainIdentity(List vcs) + private Optional getReverificationIdentityVot(List vcs) throws ParseException, HttpResponseExceptionWithErrorBody { var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcs, GPG45); var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); @@ -171,9 +179,8 @@ private boolean vcsContainIdentity(List vcs) if (matchedVot.isEmpty()) { LOGGER.info(LogHelper.buildLogMessage("No identity for reverification found")); - return false; } - return true; + return matchedVot; } } diff --git a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java index 5f5cced54f..bdd2f4437a 100644 --- a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java +++ b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java @@ -149,7 +149,12 @@ void shouldReturnJourneyFoundIfUserHasP2Identity() throws Exception { var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); assertEquals("/journey/found", response.get("journey")); + verify(ipvSession, never()).setFailureCode(any()); + var inorder = inOrder(ipvSession, mockIpvSessionService); + inorder.verify(ipvSession).setTargetVot(P2); + inorder.verify(mockIpvSessionService).updateIpvSession(ipvSession); + inorder.verifyNoMoreInteractions(); } @Test @@ -171,7 +176,12 @@ void shouldReturnJourneyFoundIfUserHasP1Identity() throws Exception { var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); assertEquals("/journey/found", response.get("journey")); + verify(ipvSession, never()).setFailureCode(any()); + var inorder = inOrder(ipvSession, mockIpvSessionService); + inorder.verify(ipvSession).setTargetVot(P1); + inorder.verify(mockIpvSessionService).updateIpvSession(ipvSession); + inorder.verifyNoMoreInteractions(); } @Test @@ -192,6 +202,7 @@ void shouldReturnJourneyFoundIfUserHasPcl250Identity() throws Exception { assertEquals("/journey/found", response.get("journey")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -212,6 +223,7 @@ void shouldReturnJourneyFoundIfUserHasPcl200Identity() throws Exception { assertEquals("/journey/found", response.get("journey")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -229,6 +241,8 @@ void shouldReturnJourneyNotFoundWhenNoVotMatched() throws Exception { assertEquals("/journey/not-found", response.get("journey")); + verify(ipvSession, never()).setTargetVot(any()); + var inorder = inOrder(ipvSession, mockIpvSessionService); inorder.verify(ipvSession).setFailureCode(NO_IDENTITY_AVAILABLE); inorder.verify(mockIpvSessionService).updateIpvSession(ipvSession); @@ -249,6 +263,7 @@ void shouldReturnJourneyErrorIfIpvSessionNotFound() throws Exception { assertEquals(SC_NOT_FOUND, response.get("statusCode")); assertEquals(IPV_SESSION_NOT_FOUND.getMessage(), response.get("message")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -263,6 +278,7 @@ void shouldReturnJourneyErrorIfClientSessionIdNotFound() throws Exception { assertEquals(SC_SERVER_ERROR, response.get("statusCode")); assertEquals(CLIENT_OAUTH_SESSION_NOT_FOUND.getMessage(), response.get("message")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -283,6 +299,7 @@ void shouldReturnJourneyErrorIfErrorFetchingVcs() throws Exception { assertEquals( RECEIVED_NON_200_RESPONSE_STATUS_CODE.getMessage(), response.get("message")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -302,6 +319,7 @@ void shouldReturnJourneyErrorIfFailureToParseFetchedVcs() throws Exception { FAILED_TO_PARSE_SUCCESSFUL_VC_STORE_ITEMS.getMessage(), response.get("message")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -323,6 +341,7 @@ void shouldReturnJourneyErrorIfFailureToDoCorrelationCheck() throws Exception { assertEquals(SC_SERVER_ERROR, response.get("statusCode")); assertEquals(FAILED_NAME_CORRELATION.getMessage(), response.get("message")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -345,6 +364,7 @@ void shouldReturnJourneyErrorIfErrorMatchingVot() throws Exception { assertEquals(SC_SERVER_ERROR, response.get("statusCode")); assertEquals(FAILED_TO_PARSE_ISSUED_CREDENTIALS.getMessage(), response.get("message")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } @Test @@ -368,6 +388,7 @@ void shouldLogRuntimeExceptionsAndRethrow() throws Exception { assertTrue(logMessage.contains("Unhandled lambda exception")); assertTrue(logMessage.contains("😓")); verify(ipvSession, never()).setFailureCode(any()); + verify(ipvSession, never()).setTargetVot(any()); } } } diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/IpvSessionService.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/IpvSessionService.java index fad2502586..7c3f26b810 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/IpvSessionService.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/service/IpvSessionService.java @@ -125,17 +125,10 @@ public IpvSessionItem generateIpvSession( ipvSessionItem.setVot(Vot.P0); if (errorObject == null) { - if (isReverification) { - ipvSessionItem.pushState(new JourneyState(REVERIFICATION, START_STATE)); - // PYIC-7076 - // Currently reverifcation journeys don't check the user's existing profile so we - // have to hard code this to P2 here. Eventually this can be set in the first step - // of the reverification journey. - ipvSessionItem.setTargetVot(Vot.P2); - } else { - ipvSessionItem.pushState(new JourneyState(INITIAL_JOURNEY_SELECTION, START_STATE)); - // For non-reverification journeys targetVot is set in CheckExistingIdentity - } + ipvSessionItem.pushState( + new JourneyState( + isReverification ? REVERIFICATION : INITIAL_JOURNEY_SELECTION, + START_STATE)); } else { ipvSessionItem.pushState(new JourneyState(TECHNICAL_ERROR, ERROR_STATE)); ipvSessionItem.setErrorCode(errorObject.getCode()); From 82844376baf965f990957f9de79e712485aacad8 Mon Sep 17 00:00:00 2001 From: Chris Wynne Date: Thu, 5 Dec 2024 13:55:37 +0000 Subject: [PATCH 4/6] PYIC-7076: Don't set targetVot for reverification journeys It's not used in the journey as we don't actually do a gpg45 check, and it makes no sense if the user has an operational profile. This means that it's now possible for that value to be null in a users session, so we handle that possibility where we read it. --- .../BuildCriOauthRequestHandler.java | 29 +++++++++++----- .../BuildCriOauthRequestHandlerTest.java | 33 +++++++++++++++++++ .../BuildUserIdentityHandler.java | 10 +++++- .../BuildUserIdentityHandlerTest.java | 21 ++++++++++++ .../pact/BuildUserIdentityHandlerTest.java | 1 + .../CheckReverificationIdentityHandler.java | 14 ++------ ...heckReverificationIdentityHandlerTest.java | 19 ----------- .../service/CriCheckingService.java | 12 +++++-- .../service/CriCheckingServiceTest.java | 24 ++++++++++++++ .../core/library/domain/ErrorResponse.java | 3 +- .../ipv/core/library/service/VotMatcher.java | 3 +- 11 files changed, 125 insertions(+), 44 deletions(-) diff --git a/lambdas/build-cri-oauth-request/src/main/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandler.java b/lambdas/build-cri-oauth-request/src/main/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandler.java index fb26a32e7c..7ebd49cb63 100644 --- a/lambdas/build-cri-oauth-request/src/main/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandler.java +++ b/lambdas/build-cri-oauth-request/src/main/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandler.java @@ -58,6 +58,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.OptionalInt; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -72,6 +73,7 @@ import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_EVIDENCE_REQUESTED; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_ISSUED_CREDENTIALS; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT; import static uk.gov.di.ipv.core.library.domain.EvidenceRequest.SCORING_POLICY_GPG45; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_LAMBDA_RESULT; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_REDIRECT_URI; @@ -180,8 +182,6 @@ public Map handleRequest(CriJourneyRequest input, Context contex String govukSigninJourneyId = clientOAuthSessionItem.getGovukSigninJourneyId(); - Vot targetVot = ipvSessionItem.getTargetVot(); - LogHelper.attachGovukSigninJourneyIdToLogs(govukSigninJourneyId); String oauthState = SecureTokenHelper.getInstance().generate(); @@ -194,8 +194,7 @@ public Map handleRequest(CriJourneyRequest input, Context contex govukSigninJourneyId, cri, criContext, - criEvidenceRequest, - targetVot); + criEvidenceRequest); CriResponse criResponse = getCriResponse(criConfig, jweObject, cri, language); @@ -314,8 +313,7 @@ private JWEObject signEncryptJar( String govukSigninJourneyId, Cri cri, String context, - EvidenceRequest evidenceRequest, - Vot requestedVot) + EvidenceRequest evidenceRequest) throws HttpResponseExceptionWithErrorBody, ParseException, JOSEException, VerifiableCredentialException { @@ -327,9 +325,24 @@ private JWEObject signEncryptJar( ipvSessionItem.getEmailAddress(), vcs, getAllowedSharedClaimAttrs(cri)); if (cri.equals(F2F)) { - evidenceRequest = getEvidenceRequestForF2F(vcs, requestedVot); + evidenceRequest = + getEvidenceRequestForF2F( + vcs, + Optional.ofNullable(ipvSessionItem.getTargetVot()) + .orElseThrow( + () -> + new HttpResponseExceptionWithErrorBody( + SC_INTERNAL_SERVER_ERROR, + MISSING_TARGET_VOT))); } else if (cri.isKbvCri()) { - evidenceRequest = getEvidenceRequestForKbvCri(ipvSessionItem.getTargetVot()); + evidenceRequest = + getEvidenceRequestForKbvCri( + Optional.ofNullable(ipvSessionItem.getTargetVot()) + .orElseThrow( + () -> + new HttpResponseExceptionWithErrorBody( + SC_INTERNAL_SERVER_ERROR, + MISSING_TARGET_VOT))); } SignedJWT signedJWT = AuthorizationRequestHelper.createSignedJWT( diff --git a/lambdas/build-cri-oauth-request/src/test/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandlerTest.java b/lambdas/build-cri-oauth-request/src/test/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandlerTest.java index cf32606b4c..fe6320edee 100644 --- a/lambdas/build-cri-oauth-request/src/test/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandlerTest.java +++ b/lambdas/build-cri-oauth-request/src/test/java/uk/gov/di/ipv/core/buildcrioauthrequest/BuildCriOauthRequestHandlerTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; @@ -35,6 +36,7 @@ import uk.gov.di.ipv.core.buildcrioauthrequest.helpers.SharedClaimsHelper; import uk.gov.di.ipv.core.library.auditing.AuditEvent; import uk.gov.di.ipv.core.library.auditing.AuditEventTypes; +import uk.gov.di.ipv.core.library.domain.Cri; import uk.gov.di.ipv.core.library.domain.CriJourneyRequest; import uk.gov.di.ipv.core.library.domain.ErrorResponse; import uk.gov.di.ipv.core.library.domain.EvidenceRequest; @@ -74,6 +76,7 @@ import java.util.Optional; import java.util.stream.Stream; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -96,6 +99,7 @@ import static uk.gov.di.ipv.core.library.domain.Cri.F2F; import static uk.gov.di.ipv.core.library.domain.Cri.HMRC_KBV; import static uk.gov.di.ipv.core.library.domain.Cri.PASSPORT; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT; import static uk.gov.di.ipv.core.library.enums.Vot.P1; import static uk.gov.di.ipv.core.library.enums.Vot.P2; import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.CREDENTIAL_ATTRIBUTES_1; @@ -916,6 +920,35 @@ void shouldSetEvidenceRequestForF2FWithMinStrengthScoreForP1() throws Exception verify(mockIpvSessionService, times(1)).updateIpvSession(any()); } + @ParameterizedTest + @EnumSource(names = {"F2F", "DWP_KBV", "EXPERIAN_KBV", "HMRC_KBV"}) + void shouldReturnJourneyErrorResponseIfTargetVotIsMissing(Cri cri) throws Exception { + // Arrange + ipvSessionItem.setTargetVot(null); + + when(mockIpvSessionService.getIpvSession(SESSION_ID)).thenReturn(ipvSessionItem); + when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) + .thenReturn(clientOAuthSessionItem); + + CriJourneyRequest input = + CriJourneyRequest.builder() + .ipvSessionId(SESSION_ID) + .ipAddress(TEST_IP_ADDRESS) + .language(TEST_LANGUAGE) + .journey(String.format(JOURNEY_BASE_URL, cri.getId())) + .build(); + + // Act + var responseJson = handleRequest(input, context); + + // Assert + JourneyErrorResponse response = + OBJECT_MAPPER.readValue(responseJson, JourneyErrorResponse.class); + + assertEquals(MISSING_TARGET_VOT.getMessage(), response.getMessage()); + assertEquals(SC_INTERNAL_SERVER_ERROR, response.getStatusCode()); + } + @Test void shouldSetEvidenceRequestForKbvCriForP2() throws Exception { // Arrange diff --git a/lambdas/build-user-identity/src/main/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandler.java b/lambdas/build-user-identity/src/main/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandler.java index 91cbee2186..69ac1e8662 100644 --- a/lambdas/build-user-identity/src/main/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandler.java +++ b/lambdas/build-user-identity/src/main/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandler.java @@ -45,10 +45,13 @@ import java.net.URI; import java.util.List; import java.util.Map; +import java.util.Optional; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; import static software.amazon.awssdk.utils.CollectionUtils.isNullOrEmpty; import static uk.gov.di.ipv.core.library.config.ConfigurationVariable.CREDENTIAL_ISSUER_ENABLED; import static uk.gov.di.ipv.core.library.domain.Cri.TICF; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT; import static uk.gov.di.ipv.core.library.domain.ScopeConstants.OPENID; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_LAMBDA_RESULT; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; @@ -120,7 +123,12 @@ public APIGatewayProxyResponseEvent handleRequest( var vcs = sessionCredentialsService.getCredentials(ipvSessionId, userId); - var targetVot = ipvSessionItem.getTargetVot(); + var targetVot = + Optional.ofNullable(ipvSessionItem.getTargetVot()) + .orElseThrow( + () -> + new HttpResponseExceptionWithErrorBody( + SC_INTERNAL_SERVER_ERROR, MISSING_TARGET_VOT)); var achievedVot = ipvSessionItem.getVot(); var thresholdVot = ipvSessionItem.getThresholdVot(); var userIdentity = diff --git a/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java b/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java index e254c82229..ed36a4ce15 100644 --- a/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java +++ b/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java @@ -90,6 +90,7 @@ import static uk.gov.di.ipv.core.library.domain.Cri.CIMIT; import static uk.gov.di.ipv.core.library.domain.Cri.TICF; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_GET_CREDENTIAL; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT; import static uk.gov.di.ipv.core.library.domain.IpvJourneyTypes.INITIAL_JOURNEY_SELECTION; import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.ADDRESS_JSON_1; import static uk.gov.di.ipv.core.library.fixtures.TestFixtures.DRIVING_PERMIT_JSON_1; @@ -336,6 +337,26 @@ void shouldReturnCredentialsWithP1OnSuccessfulUserInfoRequestForP1() throws Exce .deleteSessionCredentials(TEST_IPV_SESSION_ID); } + @Test + void shouldReturnJourneyErrorResponseIfTargetVotIsNull() throws Exception { + // Arrange + ipvSessionItem.setTargetVot(null); + when(mockIpvSessionService.getIpvSessionByAccessToken(TEST_ACCESS_TOKEN)) + .thenReturn(ipvSessionItem); + when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) + .thenReturn(clientOAuthSessionItem); + + // Act + var response = buildUserIdentityHandler.handleRequest(testEvent, mockContext); + + // Assert + Map responseBody = + OBJECT_MAPPER.readValue(response.getBody(), new TypeReference<>() {}); + assertEquals(500, response.getStatusCode()); + assertEquals(MISSING_TARGET_VOT.getCode(), Integer.valueOf(responseBody.get("error"))); + assertEquals(MISSING_TARGET_VOT.getMessage(), responseBody.get("error_description")); + } + @Test void shouldReturnCredentialsWithCimitVCOnSuccessfulUserInfoRequestWhenDeleteSessionCredentialsError() diff --git a/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/pact/BuildUserIdentityHandlerTest.java b/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/pact/BuildUserIdentityHandlerTest.java index 6e320b6d13..ea0edbff45 100644 --- a/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/pact/BuildUserIdentityHandlerTest.java +++ b/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/pact/BuildUserIdentityHandlerTest.java @@ -165,6 +165,7 @@ public void setAccessToken() { ipvSession.setIpvSessionId(IPV_SESSION_ID); ipvSession.setClientOAuthSessionId("dummyClientOAuthSessionId"); ipvSession.setVot(Vot.P2); + ipvSession.setTargetVot(Vot.P2); ipvSession.setAccessTokenMetadata(new AccessTokenMetadata()); var oAuthSession = new ClientOAuthSessionItem(); diff --git a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java index 66bc657c8c..79be1e3671 100644 --- a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java +++ b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java @@ -23,14 +23,12 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; -import uk.gov.di.ipv.core.library.service.VotAndProfile; import uk.gov.di.ipv.core.library.service.VotMatcher; import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; import java.text.ParseException; import java.util.List; import java.util.Map; -import java.util.Optional; import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_NOT_FOUND; import static com.nimbusds.oauth2.sdk.http.HTTPResponse.SC_SERVER_ERROR; @@ -120,18 +118,12 @@ public Map handleRequest(JourneyRequest request, Context context clientOAuthSession.getEvcsAccessToken(), CURRENT); - var reverificationIdentityVot = getReverificationIdentityVot(vcs); - if (reverificationIdentityVot.isEmpty()) { + if (!hasReverificationIdentity(vcs)) { ipvSession.setFailureCode(NO_IDENTITY_AVAILABLE); ipvSessionService.updateIpvSession(ipvSession); return NOT_FOUND_RESPONSE; } - if (GPG45.equals(reverificationIdentityVot.get().vot().getProfileType())) { - ipvSession.setTargetVot(reverificationIdentityVot.get().vot()); - ipvSessionService.updateIpvSession(ipvSession); - } - return FOUND_RESPONSE; } catch (HttpResponseExceptionWithErrorBody | EvcsServiceException e) { @@ -161,7 +153,7 @@ public Map handleRequest(JourneyRequest request, Context context } } - private Optional getReverificationIdentityVot(List vcs) + private boolean hasReverificationIdentity(List vcs) throws ParseException, HttpResponseExceptionWithErrorBody { var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcs, GPG45); var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); @@ -181,6 +173,6 @@ private Optional getReverificationIdentityVot(List + new HttpResponseExceptionWithErrorBody( + SC_INTERNAL_SERVER_ERROR, + MISSING_TARGET_VOT))); if (journeyResponse.isPresent()) { return journeyResponse.get(); } diff --git a/lambdas/process-cri-callback/src/test/java/uk/gov/di/ipv/core/processcricallback/service/CriCheckingServiceTest.java b/lambdas/process-cri-callback/src/test/java/uk/gov/di/ipv/core/processcricallback/service/CriCheckingServiceTest.java index cfa4c0a521..515e56026f 100644 --- a/lambdas/process-cri-callback/src/test/java/uk/gov/di/ipv/core/processcricallback/service/CriCheckingServiceTest.java +++ b/lambdas/process-cri-callback/src/test/java/uk/gov/di/ipv/core/processcricallback/service/CriCheckingServiceTest.java @@ -20,6 +20,7 @@ import uk.gov.di.ipv.core.library.domain.ScopeConstants; import uk.gov.di.ipv.core.library.dto.CriCallbackRequest; import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.VerifiableCredentialException; import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; import uk.gov.di.ipv.core.library.persistence.item.CriOAuthSessionItem; @@ -50,6 +51,7 @@ import static org.mockito.Mockito.when; import static uk.gov.di.ipv.core.library.config.CoreFeatureFlag.DL_AUTH_SOURCE_CHECK; import static uk.gov.di.ipv.core.library.domain.Cri.F2F; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.DCMAW_PASSPORT_VC; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1A_ADDRESS_VC; import static uk.gov.di.ipv.core.library.fixtures.VcFixtures.M1B_DCMAW_VC; @@ -519,6 +521,28 @@ void checkVcResponseShouldReturnNextWhenAllChecksPassForLowerConfidenceVot() thr assertEquals(new JourneyResponse(JOURNEY_NEXT_PATH), result); } + @Test + void checkVcResponseShouldThrowIfTargetVotIsNull() { + // Arrange + var ipvSessionItem = buildValidIpvSessionItem(); + ipvSessionItem.setTargetVot(null); + + // Act + var exception = + assertThrows( + HttpResponseExceptionWithErrorBody.class, + () -> + criCheckingService.checkVcResponse( + List.of(), + "1.1.1.1", + buildValidClientOAuthSessionItem(), + ipvSessionItem, + List.of(M1B_DCMAW_VC))); + + // Assert + assertEquals(MISSING_TARGET_VOT, exception.getErrorResponse()); + } + @Test void checkVcResponseShouldReturnFailWithCiWhenUserBreachesCiThreshold() throws Exception { // Arrange for CI threshold breach diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java index b9438bf9d8..832c8eace3 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/domain/ErrorResponse.java @@ -112,7 +112,8 @@ public enum ErrorResponse { INVALID_PROCESS_MOBILE_APP_JOURNEY_TYPE(1098, "Invalid process mobile app journey type"), FAILED_TO_PARSE_MOBILE_APP_CALLBACK_REQUEST_BODY( 1099, "Failed to parse mobile app callback request body"), - CRI_RESPONSE_ITEM_NOT_FOUND(1100, "CRI response item cannot be found"); + CRI_RESPONSE_ITEM_NOT_FOUND(1100, "CRI response item cannot be found"), + MISSING_TARGET_VOT(1101, "Target VOT missing from session"); private static final String ERROR = "error"; private static final String ERROR_DESCRIPTION = "error_description"; diff --git a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java index 6adfe68f2b..8959f2cc47 100644 --- a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java +++ b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java @@ -49,8 +49,7 @@ public Optional matchFirstVot( if (matchedGpg45Profile.isPresent()) { return Optional.of(new VotAndProfile(vot, matchedGpg45Profile.get())); } - } - if (hasOperationalProfileVc(vot, operationalVcs, contraIndicators)) { + } else if (hasOperationalProfileVc(vot, operationalVcs, contraIndicators)) { return Optional.of(new VotAndProfile(vot, null)); } } From fa3e61cff166df6f111e1aeed065ecd5e8d9d611 Mon Sep 17 00:00:00 2001 From: Chris Wynne Date: Fri, 6 Dec 2024 10:17:45 +0000 Subject: [PATCH 5/6] PYIC-7076: Refactor Vot matcher Clean up the signature of the method a bit. --- .../CheckExistingIdentityHandler.java | 36 ++---- .../CheckExistingIdentityHandlerTest.java | 116 +++++++----------- .../CheckReverificationIdentityHandler.java | 20 +-- ...heckReverificationIdentityHandlerTest.java | 56 ++++----- .../ipv/core/library/service/VotMatcher.java | 21 ++-- ...AndProfile.java => VotMatchingResult.java} | 3 +- .../core/library/service/VotMatcherTest.java | 76 ++++-------- 7 files changed, 129 insertions(+), 199 deletions(-) rename libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/{VotAndProfile.java => VotMatchingResult.java} (64%) diff --git a/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java b/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java index be3f959ea8..2909d6e0a6 100644 --- a/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java +++ b/lambdas/check-existing-identity/src/main/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandler.java @@ -126,7 +126,6 @@ public class CheckExistingIdentityHandler private final UserIdentityService userIdentityService; private final CriResponseService criResponseService; private final IpvSessionService ipvSessionService; - private final Gpg45ProfileEvaluator gpg45ProfileEvaluator; private final AuditService auditService; private final ClientOAuthSessionDetailsService clientOAuthSessionDetailsService; private final CimitService cimitService; @@ -145,7 +144,6 @@ public CheckExistingIdentityHandler( ConfigService configService, UserIdentityService userIdentityService, IpvSessionService ipvSessionService, - Gpg45ProfileEvaluator gpg45ProfileEvaluator, AuditService auditService, ClientOAuthSessionDetailsService clientOAuthSessionDetailsService, CriResponseService criResponseService, @@ -159,7 +157,6 @@ public CheckExistingIdentityHandler( this.configService = configService; this.userIdentityService = userIdentityService; this.ipvSessionService = ipvSessionService; - this.gpg45ProfileEvaluator = gpg45ProfileEvaluator; this.auditService = auditService; this.clientOAuthSessionDetailsService = clientOAuthSessionDetailsService; this.criResponseService = criResponseService; @@ -184,7 +181,6 @@ public CheckExistingIdentityHandler(ConfigService configService) { this.configService = ConfigService.create(); this.userIdentityService = new UserIdentityService(configService); this.ipvSessionService = new IpvSessionService(configService); - this.gpg45ProfileEvaluator = new Gpg45ProfileEvaluator(); this.auditService = AuditService.create(configService); this.clientOAuthSessionDetailsService = new ClientOAuthSessionDetailsService(configService); this.criResponseService = new CriResponseService(configService); @@ -195,7 +191,8 @@ public CheckExistingIdentityHandler(ConfigService configService) { this.evcsService = new EvcsService(configService); this.evcsMigrationService = new EvcsMigrationService(configService); this.votMatcher = - new VotMatcher(userIdentityService, gpg45ProfileEvaluator, cimitUtilityService); + new VotMatcher( + userIdentityService, new Gpg45ProfileEvaluator(), cimitUtilityService); VcHelper.setConfigService(this.configService); } @@ -604,34 +601,27 @@ private Optional checkForProfileMatch( List contraIndicators) throws ParseException, VerifiableCredentialException, EvcsServiceException { - var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcBundle.credentials(), GPG45); - var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); - var operationalVcs = - VcHelper.filterVCBasedOnProfileType(vcBundle.credentials(), OPERATIONAL_HMRC); - // Check for attained vot from requested vots - var strongestAttainedVotAndProfileFromVtr = + var maybeVotMatchingResult = votMatcher.matchFirstVot( clientOAuthSessionItem .getParsedVtr() .getRequestedVotsByStrengthDescending(), - gpg45Vcs, - gpg45Scores, - areGpg45VcsCorrelated, - operationalVcs, - contraIndicators); + vcBundle.credentials(), + contraIndicators, + areGpg45VcsCorrelated); - if (strongestAttainedVotAndProfileFromVtr.isEmpty()) { + if (maybeVotMatchingResult.isEmpty()) { return Optional.empty(); } - var attainedVotAndProfile = strongestAttainedVotAndProfileFromVtr.get(); + var votMatchingResult = maybeVotMatchingResult.get(); - if (GPG45.equals(attainedVotAndProfile.vot().getProfileType())) { + if (GPG45.equals(votMatchingResult.vot().getProfileType())) { sendProfileMatchedAuditEvent( - attainedVotAndProfile.gpg45Profile(), - gpg45Scores, - gpg45Vcs, + votMatchingResult.gpg45Profile(), + votMatchingResult.gpg45Scores(), + VcHelper.filterVCBasedOnProfileType(vcBundle.credentials(), GPG45), auditEventUser, deviceInformation); } @@ -639,7 +629,7 @@ private Optional checkForProfileMatch( // vot achieved for vtr return Optional.of( buildReuseResponse( - attainedVotAndProfile.vot(), + votMatchingResult.vot(), ipvSessionItem, vcBundle, auditEventUser, diff --git a/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java b/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java index 59a8e78859..e9c32e7fd8 100644 --- a/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java +++ b/lambdas/check-existing-identity/src/test/java/uk/gov/di/ipv/core/checkexistingidentity/CheckExistingIdentityHandlerTest.java @@ -41,7 +41,7 @@ import uk.gov.di.ipv.core.library.exceptions.ConfigException; import uk.gov.di.ipv.core.library.exceptions.UnrecognisedCiException; import uk.gov.di.ipv.core.library.exceptions.VerifiableCredentialException; -import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; +import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; import uk.gov.di.ipv.core.library.helpers.SecureTokenHelper; import uk.gov.di.ipv.core.library.helpers.TestVc; import uk.gov.di.ipv.core.library.journeys.JourneyUris; @@ -58,8 +58,8 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; -import uk.gov.di.ipv.core.library.service.VotAndProfile; import uk.gov.di.ipv.core.library.service.VotMatcher; +import uk.gov.di.ipv.core.library.service.VotMatchingResult; import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; import uk.gov.di.ipv.core.library.verifiablecredential.service.VerifiableCredentialService; @@ -187,7 +187,6 @@ class CheckExistingIdentityHandlerTest { @Mock private UserIdentityService userIdentityService; @Mock private CriResponseService criResponseService; @Mock private IpvSessionService ipvSessionService; - @Mock private Gpg45ProfileEvaluator gpg45ProfileEvaluator; @Mock private ConfigService configService; @Mock private AuditService auditService; @Mock private ClientOAuthSessionDetailsService clientOAuthSessionDetailsService; @@ -346,16 +345,12 @@ void shouldReturnJourneyReuseResponseIfScoresSatisfyM1AGpg45Profile_alsoStoreVcs Mockito.lenient().when(configService.enabled(EVCS_WRITE_ENABLED)).thenReturn(true); VerifiableCredential hmrcMigrationVC = vcHmrcMigrationPCL200(); - when(mockVerifiableCredentialService.getVcs(any())) - .thenReturn(List.of(gpg45Vc, hmrcMigrationVC)); - when(mockVotMatcher.matchFirstVot( - List.of(P2), - List.of(gpg45Vc), - null, - true, - List.of(hmrcMigrationVC), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); + var vcs = List.of(gpg45Vc, hmrcMigrationVC); + when(mockVerifiableCredentialService.getVcs(any())).thenReturn(vcs); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, List.of(), true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P2, M1A, Gpg45Scores.builder().build()))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -402,8 +397,10 @@ void shouldReturnJourneyReuseUpdateResponseIfVcIsF2fAndHasPendingReturnInEvcs() .thenReturn(Map.of(PENDING_RETURN, vcs)); when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); - when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, null, true, List.of(), List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, List.of(), true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P2, M1A, Gpg45Scores.builder().build()))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -456,13 +453,10 @@ void shouldReturnJourneyReuseStoreResponseIfVcIsF2fAndHasPartiallyMigratedVcs() when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); when(mockVotMatcher.matchFirstVot( - List.of(P2), - List.of(f2fVc1, f2fVc2, f2fVc3), - null, - true, - List.of(), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); + List.of(P2), List.of(f2fVc1, f2fVc2, f2fVc3), List.of(), true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P2, M1A, Gpg45Scores.builder().build()))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -477,9 +471,10 @@ void shouldReturnJourneyReuseStoreResponseIfVcIsF2fAndHasPartiallyMigratedVcs() @Test void shouldReturnJourneyReuseResponseIfScoresSatisfyM1BGpg45Profile() throws Exception { - when(mockVotMatcher.matchFirstVot( - List.of(P2), List.of(), null, true, List.of(), List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); + when(mockVotMatcher.matchFirstVot(List.of(P2), List.of(), List.of(), true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P2, M1B, Gpg45Scores.builder().build()))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); JourneyResponse journeyResponse = @@ -512,12 +507,10 @@ void shouldReturnJourneyOpProfileReuseResponseIfPCL200RequestedAndMetWhenNotInMi .thenReturn(List.of(gpg45Vc, pcl200Vc)); when(mockVotMatcher.matchFirstVot( List.of(P2, PCL250, PCL200), - List.of(gpg45Vc), - null, - false, - List.of(pcl200Vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL200, null))); + List.of(gpg45Vc, pcl200Vc), + List.of(), + false)) + .thenReturn(Optional.of(new VotMatchingResult(PCL200, null, null))); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name(), PCL200.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(false); @@ -546,13 +539,8 @@ void shouldReturnJourneyOpProfileReuseResponseIfPCL250RequestedAndMetWhenNotInMi when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, pcl250Vc)); when(mockVotMatcher.matchFirstVot( - List.of(P2, PCL250), - List.of(gpg45Vc), - null, - false, - List.of(pcl250Vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); + List.of(P2, PCL250), List.of(gpg45Vc, pcl250Vc), List.of(), false)) + .thenReturn(Optional.of(new VotMatchingResult(PCL250, null, null))); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(false); @@ -580,13 +568,8 @@ void shouldReturnJourneyOpProfileReuseResponseIfOpProfileAndPendingF2F() throws when(criResponseService.getFaceToFaceRequest(any())).thenReturn(new CriResponseItem()); when(mockVerifiableCredentialService.getVcs(any())).thenReturn(List.of(pcl250Vc)); when(mockVotMatcher.matchFirstVot( - List.of(P2, PCL250), - List.of(), - null, - false, - List.of(pcl250Vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); + List.of(P2, PCL250), List.of(pcl250Vc), List.of(), false)) + .thenReturn(Optional.of(new VotMatchingResult(PCL250, null, null))); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name())); ipvSessionItem.setInheritedIdentityReceivedThisSession(false); @@ -612,12 +595,10 @@ void shouldReturnJourneyInMigrationReuseResponseIfPCL200RequestedAndMet() throws .thenReturn(List.of(gpg45Vc, pcl200Vc)); when(mockVotMatcher.matchFirstVot( List.of(P2, PCL250, PCL200), - List.of(gpg45Vc), - null, - false, - List.of(pcl200Vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL200, null))); + List.of(gpg45Vc, pcl200Vc), + List.of(), + false)) + .thenReturn(Optional.of(new VotMatchingResult(PCL200, null, null))); ipvSessionItem.setInheritedIdentityReceivedThisSession(true); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name(), PCL200.name())); @@ -645,13 +626,8 @@ void shouldReturnJourneyInMigrationReuseResponseIfPCL250RequestedAndMet() throws when(mockVerifiableCredentialService.getVcs(any())) .thenReturn(List.of(gpg45Vc, pcl250Vc)); when(mockVotMatcher.matchFirstVot( - List.of(P2, PCL250), - List.of(gpg45Vc), - null, - true, - List.of(pcl250Vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); + List.of(P2, PCL250), List.of(gpg45Vc, pcl250Vc), List.of(), true)) + .thenReturn(Optional.of(new VotMatchingResult(PCL250, null, null))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); clientOAuthSessionItem.setVtr(List.of(P2.name(), Vot.PCL250.name())); @@ -666,7 +642,6 @@ void shouldReturnJourneyInMigrationReuseResponseIfPCL250RequestedAndMet() throws verify(mockSessionCredentialService) .persistCredentials(List.of(pcl250Vc), ipvSessionItem.getIpvSessionId(), true); - verify(gpg45ProfileEvaluator).buildScore(List.of(gpg45Vc)); InOrder inOrder = inOrder(ipvSessionItem, ipvSessionService); inOrder.verify(ipvSessionItem).setVot(Vot.PCL250); @@ -679,9 +654,10 @@ void shouldReturnJourneyInMigrationReuseResponseIfPCL250RequestedAndMet() throws @Test void shouldReturnErrorResponseIfVcCanNotBeStoredInSessionCredentialTable() throws Exception { - when(mockVotMatcher.matchFirstVot( - List.of(P2), List.of(), null, true, List.of(), List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); + when(mockVotMatcher.matchFirstVot(List.of(P2), List.of(), List.of(), true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P2, M1A, Gpg45Scores.builder().build()))); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); doThrow( new VerifiableCredentialException( @@ -1327,8 +1303,9 @@ void shouldReturnJourneyRepeatFraudCheckResponseIfExpiredFraudAndFlagIsTrue() th M1B_DCMAW_VC); when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(vcs); - when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, null, true, List.of(), List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, List.of(), true)) + .thenReturn( + Optional.of(new VotMatchingResult(P2, M1B, Gpg45Scores.builder().build()))); when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); @@ -1368,8 +1345,9 @@ void shouldReturnJourneyRepeatFraudCheckResponseIfExpiredFraudAndFlagIsTrue() th vcVerificationM1a(), M1B_DCMAW_VC); when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(vcs); - when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, null, true, List.of(), List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); + when(mockVotMatcher.matchFirstVot(List.of(P2), vcs, List.of(), true)) + .thenReturn( + Optional.of(new VotMatchingResult(P2, M1B, Gpg45Scores.builder().build()))); when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); when(userIdentityService.areVcsCorrelated(any())).thenReturn(true); @@ -1406,9 +1384,9 @@ void shouldNotReturnJourneyRepeatFraudCheckResponseIfNotExpiredFraudAndFlagIsTru when(ipvSessionService.getIpvSessionWithRetry(TEST_SESSION_ID)).thenReturn(ipvSessionItem); when(mockVerifiableCredentialService.getVcs(TEST_USER_ID)).thenReturn(VCS_FROM_STORE); - when(mockVotMatcher.matchFirstVot( - List.of(P2), VCS_FROM_STORE, null, true, List.of(), List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1B))); + when(mockVotMatcher.matchFirstVot(List.of(P2), VCS_FROM_STORE, List.of(), true)) + .thenReturn( + Optional.of(new VotMatchingResult(P2, M1B, Gpg45Scores.builder().build()))); when(clientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(clientOAuthSessionItem); diff --git a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java index 79be1e3671..a14fb63526 100644 --- a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java +++ b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java @@ -36,7 +36,6 @@ import static uk.gov.di.ipv.core.library.domain.ErrorResponse.FAILED_TO_PARSE_SUCCESSFUL_VC_STORE_ITEMS; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.IPV_SESSION_NOT_FOUND; import static uk.gov.di.ipv.core.library.domain.ProfileType.GPG45; -import static uk.gov.di.ipv.core.library.domain.ProfileType.OPERATIONAL_HMRC; import static uk.gov.di.ipv.core.library.domain.ReverificationFailureCode.NO_IDENTITY_AVAILABLE; import static uk.gov.di.ipv.core.library.enums.EvcsVCState.CURRENT; import static uk.gov.di.ipv.core.library.enums.Vot.SUPPORTED_VOTS_BY_DESCENDING_STRENGTH; @@ -57,7 +56,6 @@ public class CheckReverificationIdentityHandler private final ClientOAuthSessionDetailsService clientSessionService; private final EvcsService evcsService; private final UserIdentityService userIdentityService; - private final Gpg45ProfileEvaluator gpg45ProfileEvaluator; private final VotMatcher votMatcher; public CheckReverificationIdentityHandler( @@ -73,7 +71,6 @@ public CheckReverificationIdentityHandler( this.clientSessionService = clientOAuthSessionDetailsService; this.evcsService = evcsService; this.userIdentityService = userIdentityService; - this.gpg45ProfileEvaluator = gpg45ProfileEvaluator; this.votMatcher = votMatcher; } @@ -89,8 +86,7 @@ public CheckReverificationIdentityHandler(ConfigService configService) { this.clientSessionService = new ClientOAuthSessionDetailsService(configService); this.evcsService = new EvcsService(configService); this.userIdentityService = new UserIdentityService(configService); - this.gpg45ProfileEvaluator = new Gpg45ProfileEvaluator(); - this.votMatcher = new VotMatcher(userIdentityService, gpg45ProfileEvaluator); + this.votMatcher = new VotMatcher(userIdentityService, new Gpg45ProfileEvaluator()); } @Tracing @@ -155,19 +151,13 @@ public Map handleRequest(JourneyRequest request, Context context private boolean hasReverificationIdentity(List vcs) throws ParseException, HttpResponseExceptionWithErrorBody { - var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcs, GPG45); - var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); - var gpg45VcsCorrelated = userIdentityService.areVcsCorrelated(gpg45Vcs); - var operationalVcs = VcHelper.filterVCBasedOnProfileType(vcs, OPERATIONAL_HMRC); - var matchedVot = votMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - gpg45Vcs, - gpg45Scores, - gpg45VcsCorrelated, - operationalVcs, - List.of()); + vcs, + List.of(), + userIdentityService.areVcsCorrelated( + VcHelper.filterVCBasedOnProfileType(vcs, GPG45))); if (matchedVot.isEmpty()) { LOGGER.info(LogHelper.buildLogMessage("No identity for reverification found")); diff --git a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java index 5684d18948..4e92f4c4b6 100644 --- a/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java +++ b/lambdas/check-reverification-identity/src/test/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandlerTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.di.ipv.core.library.domain.JourneyRequest; import uk.gov.di.ipv.core.library.domain.VerifiableCredential; @@ -17,7 +16,6 @@ import uk.gov.di.ipv.core.library.exceptions.CredentialParseException; import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; -import uk.gov.di.ipv.core.library.gpg45.Gpg45ProfileEvaluator; import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; @@ -26,8 +24,8 @@ import uk.gov.di.ipv.core.library.service.EvcsService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.service.UserIdentityService; -import uk.gov.di.ipv.core.library.service.VotAndProfile; import uk.gov.di.ipv.core.library.service.VotMatcher; +import uk.gov.di.ipv.core.library.service.VotMatchingResult; import uk.gov.di.ipv.core.library.testhelpers.unit.LogCollector; import java.text.ParseException; @@ -70,6 +68,7 @@ @ExtendWith(MockitoExtension.class) class CheckReverificationIdentityHandlerTest { + private static final List EMPTY_CONTRA_INDICATORS = List.of(); private static final String TEST_IPV_SESSION_ID = "test-ipv-session-id"; private static final String TEST_CLIENT_SESSION_ID = "test-client-session-id"; private static final String TEST_USER_ID = "test-user-id"; @@ -90,7 +89,6 @@ class CheckReverificationIdentityHandlerTest { @Mock private ClientOAuthSessionDetailsService mockClientSessionService; @Mock private EvcsService mockEvcsService; @Mock private UserIdentityService mockUserIdentityService; - @Spy private Gpg45ProfileEvaluator mockGpg45Evaluator; @Mock private VotMatcher mockVotMatcher; @InjectMocks private CheckReverificationIdentityHandler checkReverificationIdentityHandler; @@ -139,12 +137,12 @@ void shouldReturnJourneyFoundIfUserHasP2Identity() throws Exception { .thenReturn(p2Vcs); when(mockVotMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - List.of(M1B_DCMAW_VC, VC_ADDRESS, m1BFraudVc), - new Gpg45Scores(3, 2, 1, 2, 2), - true, - List.of(pcl250vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(P2, M1A))); + p2Vcs, + EMPTY_CONTRA_INDICATORS, + true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P2, M1A, Gpg45Scores.builder().build()))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -163,11 +161,11 @@ void shouldReturnJourneyFoundIfUserHasP1Identity() throws Exception { when(mockVotMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, p1Vcs, - new Gpg45Scores(2, 2, 0, 2, 2), - true, - List.of(), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(P1, L1A))); + EMPTY_CONTRA_INDICATORS, + true)) + .thenReturn( + Optional.of( + new VotMatchingResult(P1, L1A, Gpg45Scores.builder().build()))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -183,12 +181,10 @@ void shouldReturnJourneyFoundIfUserHasPcl250Identity() throws Exception { .thenReturn(List.of(pcl250vc)); when(mockVotMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - List.of(), - Gpg45Scores.builder().build(), - false, List.of(pcl250vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL250, null))); + EMPTY_CONTRA_INDICATORS, + false)) + .thenReturn(Optional.of(new VotMatchingResult(PCL250, null, null))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -203,12 +199,10 @@ void shouldReturnJourneyFoundIfUserHasPcl200Identity() throws Exception { .thenReturn(List.of(pcl200vc)); when(mockVotMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - List.of(), - Gpg45Scores.builder().build(), - false, List.of(pcl200vc), - List.of())) - .thenReturn(Optional.of(new VotAndProfile(PCL200, null))); + EMPTY_CONTRA_INDICATORS, + false)) + .thenReturn(Optional.of(new VotMatchingResult(PCL200, null, null))); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -221,10 +215,8 @@ void shouldReturnJourneyNotFoundWhenNoVotMatched() throws Exception { when(mockVotMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, List.of(), - Gpg45Scores.builder().build(), - false, - List.of(), - List.of())) + EMPTY_CONTRA_INDICATORS, + false)) .thenReturn(Optional.empty()); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); @@ -335,10 +327,8 @@ void shouldReturnJourneyErrorIfErrorMatchingVot() throws Exception { when(mockVotMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, List.of(), - Gpg45Scores.builder().build(), - false, - List.of(), - List.of())) + EMPTY_CONTRA_INDICATORS, + false)) .thenThrow(new ParseException("😬", 0)); var response = checkReverificationIdentityHandler.handleRequest(REQUEST, mockContext); diff --git a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java index 8959f2cc47..b19f3daa41 100644 --- a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java +++ b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatcher.java @@ -9,6 +9,7 @@ import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; import uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile; import uk.gov.di.ipv.core.library.helpers.LogHelper; +import uk.gov.di.ipv.core.library.verifiablecredential.helpers.VcHelper; import uk.gov.di.model.ContraIndicator; import java.text.ParseException; @@ -16,6 +17,7 @@ import java.util.Optional; import static uk.gov.di.ipv.core.library.domain.ProfileType.GPG45; +import static uk.gov.di.ipv.core.library.domain.ProfileType.OPERATIONAL_HMRC; import static uk.gov.di.ipv.core.library.domain.VocabConstants.VOT_CLAIM_NAME; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_GPG45_PROFILE; import static uk.gov.di.ipv.core.library.helpers.LogHelper.LogField.LOG_VOT; @@ -33,24 +35,27 @@ public VotMatcher( this(userIdentityService, gpg45ProfileEvaluator, null); } - public Optional matchFirstVot( + public Optional matchFirstVot( List vots, - List gpg45Vcs, - Gpg45Scores gpg45Scores, - boolean areGpg45VcsCorrelated, - List operationalVcs, - List contraIndicators) + List vcs, + List contraIndicators, + boolean areGpg45VcsCorrelated) throws ParseException { + var gpg45Vcs = VcHelper.filterVCBasedOnProfileType(vcs, GPG45); + var gpg45Scores = gpg45ProfileEvaluator.buildScore(gpg45Vcs); + var operationalVcs = VcHelper.filterVCBasedOnProfileType(vcs, OPERATIONAL_HMRC); + for (Vot vot : vots) { if (vot.getProfileType().equals(GPG45) && areGpg45VcsCorrelated) { var matchedGpg45Profile = achievedWithGpg45Profile(vot, gpg45Vcs, gpg45Scores, contraIndicators); if (matchedGpg45Profile.isPresent()) { - return Optional.of(new VotAndProfile(vot, matchedGpg45Profile.get())); + return Optional.of( + new VotMatchingResult(vot, matchedGpg45Profile.get(), gpg45Scores)); } } else if (hasOperationalProfileVc(vot, operationalVcs, contraIndicators)) { - return Optional.of(new VotAndProfile(vot, null)); + return Optional.of(new VotMatchingResult(vot, null, null)); } } return Optional.empty(); diff --git a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatchingResult.java similarity index 64% rename from libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java rename to libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatchingResult.java index d54835b409..43199c3e17 100644 --- a/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotAndProfile.java +++ b/libs/user-identity-service/src/main/java/uk/gov/di/ipv/core/library/service/VotMatchingResult.java @@ -2,7 +2,8 @@ import uk.gov.di.ipv.core.library.annotations.ExcludeFromGeneratedCoverageReport; import uk.gov.di.ipv.core.library.enums.Vot; +import uk.gov.di.ipv.core.library.gpg45.Gpg45Scores; import uk.gov.di.ipv.core.library.gpg45.enums.Gpg45Profile; @ExcludeFromGeneratedCoverageReport -public record VotAndProfile(Vot vot, Gpg45Profile gpg45Profile) {} +public record VotMatchingResult(Vot vot, Gpg45Profile gpg45Profile, Gpg45Scores gpg45Scores) {} diff --git a/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java b/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java index 54ff7fd691..f2a204e71d 100644 --- a/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java +++ b/libs/user-identity-service/src/test/java/uk/gov/di/ipv/core/library/service/VotMatcherTest.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Optional; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @@ -48,8 +49,9 @@ public static void beforeAll() throws Exception { } @Test - void shouldReturnFirstMatchedGpg45VotAndProfile() throws Exception { + void shouldReturnFirstMatchedGpg45Vot() throws Exception { when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(false); + when(mockGpg45ProfileEvaluator.buildScore(gpg45Vcs)).thenReturn(GPG_45_SCORES); when(mockGpg45ProfileEvaluator.getFirstMatchingProfile( GPG_45_SCORES, P2.getSupportedGpg45Profiles())) .thenReturn(Optional.empty()); @@ -57,54 +59,41 @@ void shouldReturnFirstMatchedGpg45VotAndProfile() throws Exception { GPG_45_SCORES, P1.getSupportedGpg45Profiles())) .thenReturn(Optional.of(L1A)); - var votAndProfile = + var votMatch = votMatcher.matchFirstVot( - SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - gpg45Vcs, - GPG_45_SCORES, - true, - List.of(), - List.of()); + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, gpg45Vcs, List.of(), true); - assertEquals(Optional.of(new VotAndProfile(P1, L1A)), votAndProfile); + assertEquals(Optional.of(new VotMatchingResult(P1, L1A, GPG_45_SCORES)), votMatch); } @Test void shouldReturnFirstMatchedOperationalVot() throws Exception { - var operationalVcs = List.of(pcl200vc); - when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(false); - var votAndProfile = + var votMatch = votMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - gpg45Vcs, - GPG_45_SCORES, - true, - operationalVcs, - List.of()); + Stream.concat(gpg45Vcs.stream(), Stream.of(pcl200vc)).toList(), + List.of(), + true); - assertEquals(Optional.of(new VotAndProfile(PCL200, null)), votAndProfile); + assertEquals(Optional.of(new VotMatchingResult(PCL200, null, null)), votMatch); } @Test void shouldReturnEmptyOptionalIfNoVotMatched() throws Exception { - var votAndProfile = + var votMatch = votMatcher.matchFirstVot( - SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - List.of(), - GPG_45_SCORES, - true, - List.of(), - List.of()); + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, List.of(), List.of(), true); - assertEquals(Optional.empty(), votAndProfile); + assertEquals(Optional.empty(), votMatch); } @Test void shouldMatchWeakerGpg45VotIfStrongerVotHasBreachingCi() throws Exception { var contraIndicators = List.of(new ContraIndicator()); + when(mockGpg45ProfileEvaluator.buildScore(gpg45Vcs)).thenReturn(GPG_45_SCORES); when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(false); when(mockGpg45ProfileEvaluator.getFirstMatchingProfile( GPG_45_SCORES, P2.getSupportedGpg45Profiles())) @@ -114,51 +103,38 @@ void shouldMatchWeakerGpg45VotIfStrongerVotHasBreachingCi() throws Exception { .thenReturn(Optional.of(L1A)); when(mockCimitUtilityService.isBreachingCiThreshold(contraIndicators, P2)).thenReturn(true); - var votAndProfile = + var votMatch = votMatcher.matchFirstVot( - SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - gpg45Vcs, - GPG_45_SCORES, - true, - List.of(), - contraIndicators); + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, gpg45Vcs, contraIndicators, true); - assertEquals(Optional.of(new VotAndProfile(P1, L1A)), votAndProfile); + assertEquals(Optional.of(new VotMatchingResult(P1, L1A, GPG_45_SCORES)), votMatch); } @Test void shouldMatchWeakerOperationalVotIfStrongerVotHasBreachingCi() throws Exception { - var operationalVcs = List.of(pcl250vc, pcl200vc); var contraIndicators = List.of(new ContraIndicator()); when(mockCimitUtilityService.isBreachingCiThreshold(contraIndicators, PCL250)) .thenReturn(true); - var votAndProfile = + var votMatch = votMatcher.matchFirstVot( SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - gpg45Vcs, - GPG_45_SCORES, - true, - operationalVcs, - contraIndicators); + Stream.concat(gpg45Vcs.stream(), Stream.of(pcl250vc, pcl200vc)).toList(), + contraIndicators, + true); - assertEquals(Optional.of(new VotAndProfile(PCL200, null)), votAndProfile); + assertEquals(Optional.of(new VotMatchingResult(PCL200, null, null)), votMatch); } @Test void shouldNotMatchGpg45VotIfRequiresAdditionalEvidence() throws Exception { when(mockUseridentityService.checkRequiresAdditionalEvidence(gpg45Vcs)).thenReturn(true); - var votAndProfile = + var votMatch = votMatcher.matchFirstVot( - SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, - gpg45Vcs, - GPG_45_SCORES, - true, - List.of(), - List.of()); + SUPPORTED_VOTS_BY_DESCENDING_STRENGTH, gpg45Vcs, List.of(), true); - assertEquals(Optional.empty(), votAndProfile); + assertEquals(Optional.empty(), votMatch); } } From c0428321abfc64766f3a975d1c673a24b97167d7 Mon Sep 17 00:00:00 2001 From: Chris Wynne Date: Fri, 6 Dec 2024 11:41:32 +0000 Subject: [PATCH 6/6] PYIC-7076: Skip CI check with TICF To keep inline with how we handle CIs when a user is on a reverification journey. --- .../BuildUserIdentityHandlerTest.java | 6 +- .../core/callticfcri/CallTicfCriHandler.java | 51 ++++--- .../callticfcri/CallTicfCriHandlerTest.java | 132 ++++++++++++------ .../CheckReverificationIdentityHandler.java | 1 - .../persistence/item/IpvSessionItem.java | 2 +- 5 files changed, 125 insertions(+), 67 deletions(-) diff --git a/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java b/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java index ed36a4ce15..f0f975e581 100644 --- a/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java +++ b/lambdas/build-user-identity/src/test/java/uk/gov/di/ipv/core/builduseridentity/BuildUserIdentityHandlerTest.java @@ -350,11 +350,11 @@ void shouldReturnJourneyErrorResponseIfTargetVotIsNull() throws Exception { var response = buildUserIdentityHandler.handleRequest(testEvent, mockContext); // Assert - Map responseBody = + Map body = OBJECT_MAPPER.readValue(response.getBody(), new TypeReference<>() {}); assertEquals(500, response.getStatusCode()); - assertEquals(MISSING_TARGET_VOT.getCode(), Integer.valueOf(responseBody.get("error"))); - assertEquals(MISSING_TARGET_VOT.getMessage(), responseBody.get("error_description")); + assertEquals(MISSING_TARGET_VOT.getCode(), Integer.valueOf(body.get("error"))); + assertEquals(MISSING_TARGET_VOT.getMessage(), body.get("error_description")); } @Test diff --git a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java b/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java index c0aed83867..9d97f17dce 100644 --- a/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java +++ b/lambdas/call-ticf-cri/src/main/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandler.java @@ -18,7 +18,6 @@ import uk.gov.di.ipv.core.library.domain.JourneyResponse; import uk.gov.di.ipv.core.library.domain.ProcessRequest; import uk.gov.di.ipv.core.library.enums.Vot; -import uk.gov.di.ipv.core.library.exceptions.ClientOauthSessionNotFoundException; import uk.gov.di.ipv.core.library.exceptions.ConfigException; import uk.gov.di.ipv.core.library.exceptions.HttpResponseExceptionWithErrorBody; import uk.gov.di.ipv.core.library.exceptions.IpvSessionNotFoundException; @@ -34,13 +33,16 @@ import uk.gov.di.ipv.core.library.service.ConfigService; import uk.gov.di.ipv.core.library.service.IpvSessionService; import uk.gov.di.ipv.core.library.verifiablecredential.service.SessionCredentialsService; -import uk.gov.di.model.ContraIndicator; import java.util.List; import java.util.Map; +import java.util.Optional; +import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR; import static uk.gov.di.ipv.core.library.domain.Cri.TICF; import static uk.gov.di.ipv.core.library.domain.ErrorResponse.ERROR_PROCESSING_TICF_CRI_RESPONSE; +import static uk.gov.di.ipv.core.library.domain.ErrorResponse.MISSING_TARGET_VOT; +import static uk.gov.di.ipv.core.library.domain.ScopeConstants.REVERIFICATION; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_ERROR_PATH; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_NEXT_PATH; @@ -149,7 +151,7 @@ public Map handleRequest(ProcessRequest request, Context context private Map callTicfCri(IpvSessionItem ipvSessionItem, ProcessRequest request) throws TicfCriServiceException, CiRetrievalException, VerifiableCredentialException, CiPostMitigationsException, CiPutException, ConfigException, - UnrecognisedVotException, ClientOauthSessionNotFoundException { + UnrecognisedVotException, HttpResponseExceptionWithErrorBody { configService.setFeatureSet(RequestHelper.getFeatureSet(request)); var clientOAuthSessionItem = clientOAuthSessionDetailsService.getClientOAuthSession( @@ -173,26 +175,33 @@ private Map callTicfCri(IpvSessionItem ipvSessionItem, ProcessRe ipvSessionItem, List.of()); - List cis = - cimitService.getContraIndicators( - clientOAuthSessionItem.getUserId(), - clientOAuthSessionItem.getGovukSigninJourneyId(), - request.getIpAddress()); - - var thresholdVot = ipvSessionItem.getThresholdVot(); - - var journeyResponse = - cimitUtilityService.getMitigationJourneyIfBreaching(cis, thresholdVot); - if (journeyResponse.isPresent()) { - LOGGER.info( - LogHelper.buildLogMessage( - "CI score is breaching threshold - setting VOT to P0")); - ipvSessionItem.setVot(Vot.P0); - - return journeyResponse.get().toObjectMap(); + if (!clientOAuthSessionItem.getScopeClaims().contains(REVERIFICATION)) { + var cis = + cimitService.getContraIndicators( + clientOAuthSessionItem.getUserId(), + clientOAuthSessionItem.getGovukSigninJourneyId(), + request.getIpAddress()); + + var journeyResponse = + cimitUtilityService.getMitigationJourneyIfBreaching( + cis, + Optional.ofNullable(ipvSessionItem.getThresholdVot()) + .orElseThrow( + () -> + new HttpResponseExceptionWithErrorBody( + SC_INTERNAL_SERVER_ERROR, + MISSING_TARGET_VOT))); + if (journeyResponse.isPresent()) { + LOGGER.info( + LogHelper.buildLogMessage( + "CI score is breaching threshold - setting VOT to P0")); + ipvSessionItem.setVot(Vot.P0); + + return journeyResponse.get().toObjectMap(); + } + LOGGER.info(LogHelper.buildLogMessage("CI score not breaching threshold")); } - LOGGER.info(LogHelper.buildLogMessage("CI score not breaching threshold")); return JOURNEY_NEXT; } } diff --git a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java b/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java index 951c47bfa2..14b08a3d0c 100644 --- a/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java +++ b/lambdas/call-ticf-cri/src/test/java/uk/gov/di/ipv/core/callticfcri/CallTicfCriHandlerTest.java @@ -11,6 +11,7 @@ import org.mockito.InOrder; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import uk.gov.di.ipv.core.callticfcri.exception.TicfCriServiceException; import uk.gov.di.ipv.core.callticfcri.service.TicfCriService; @@ -22,7 +23,6 @@ import uk.gov.di.ipv.core.library.domain.JourneyResponse; import uk.gov.di.ipv.core.library.domain.ProcessRequest; import uk.gov.di.ipv.core.library.domain.VerifiableCredential; -import uk.gov.di.ipv.core.library.enums.Vot; import uk.gov.di.ipv.core.library.exceptions.VerifiableCredentialException; import uk.gov.di.ipv.core.library.persistence.item.ClientOAuthSessionItem; import uk.gov.di.ipv.core.library.persistence.item.IpvSessionItem; @@ -51,17 +51,22 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static uk.gov.di.ipv.core.library.domain.Cri.TICF; +import static uk.gov.di.ipv.core.library.domain.ScopeConstants.OPENID; +import static uk.gov.di.ipv.core.library.domain.ScopeConstants.REVERIFICATION; +import static uk.gov.di.ipv.core.library.enums.Vot.P0; +import static uk.gov.di.ipv.core.library.enums.Vot.P2; import static uk.gov.di.ipv.core.library.journeys.JourneyUris.JOURNEY_FAIL_WITH_CI_PATH; @ExtendWith(MockitoExtension.class) class CallTicfCriHandlerTest { private static final String TEST_USER_ID = "a-user-id"; - private static final ClientOAuthSessionItem clientOAuthSessionItem = + private static final ClientOAuthSessionItem CLIENT_OAUTH_SESSION_ITEM = ClientOAuthSessionItem.builder() .userId(TEST_USER_ID) .govukSigninJourneyId("a-govuk-journey-id") + .scope(OPENID) .build(); - private static final ProcessRequest input = + private static final ProcessRequest INPUT = ProcessRequest.processRequestBuilder() .ipvSessionId("a-session-id") .ipAddress("an-ip-address") @@ -74,6 +79,7 @@ class CallTicfCriHandlerTest { private static final JourneyResponse JOURNEY_FAIL_WITH_CI = new JourneyResponse(JOURNEY_FAIL_WITH_CI_PATH); + @Spy private IpvSessionItem ipvSessionItem; @Mock private Context mockContext; @Mock private ConfigService mockConfigService; @Mock private IpvSessionService mockIpvSessionService; @@ -82,14 +88,14 @@ class CallTicfCriHandlerTest { @Mock private CimitService mockCimitService; @Mock private CimitUtilityService mockCimitUtilityService; @Mock private CriStoringService mockCriStoringService; - @Mock private IpvSessionItem mockIpvSessionItem; @Mock private VerifiableCredential mockVerifiableCredential; @Mock private AuditService mockAuditService; @InjectMocks private CallTicfCriHandler callTicfCriHandler; @BeforeEach public void setUp() { - mockIpvSessionItem.setIpvSessionId("a-session-id"); + ipvSessionItem.setIpvSessionId("a-session-id"); + ipvSessionItem.setVot(P2); } @AfterEach @@ -102,13 +108,13 @@ void checkAuditEventWait() { @Test void handleRequestShouldCallTicfCriAndReturnJourneyNextIfNoBreachingCiReceived() throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(clientOAuthSessionItem); - when(mockTicfCriService.getTicfVc(clientOAuthSessionItem, mockIpvSessionItem)) + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); + when(mockTicfCriService.getTicfVc(CLIENT_OAUTH_SESSION_ITEM, ipvSessionItem)) .thenReturn(List.of(mockVerifiableCredential)); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); verify(mockCriStoringService) .storeVcs( @@ -116,15 +122,15 @@ void handleRequestShouldCallTicfCriAndReturnJourneyNextIfNoBreachingCiReceived() "an-ip-address", "device-information", List.of(mockVerifiableCredential), - clientOAuthSessionItem, - mockIpvSessionItem, + CLIENT_OAUTH_SESSION_ITEM, + ipvSessionItem, List.of()); verify(mockCimitService) .getContraIndicators(TEST_USER_ID, "a-govuk-journey-id", "an-ip-address"); InOrder inOrder = inOrder(mockIpvSessionService); - inOrder.verify(mockIpvSessionService).updateIpvSession(mockIpvSessionItem); + inOrder.verify(mockIpvSessionService).updateIpvSession(ipvSessionItem); inOrder.verifyNoMoreInteractions(); assertEquals("/journey/next", lambdaResult.get("journey")); @@ -132,20 +138,20 @@ void handleRequestShouldCallTicfCriAndReturnJourneyNextIfNoBreachingCiReceived() @Test void handleRequestShouldReturnJourneyNextIfEmptyListReceived() throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(clientOAuthSessionItem); - when(mockTicfCriService.getTicfVc(clientOAuthSessionItem, mockIpvSessionItem)) + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); + when(mockTicfCriService.getTicfVc(CLIENT_OAUTH_SESSION_ITEM, ipvSessionItem)) .thenReturn(List.of()); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); verify(mockCriStoringService, never()) .storeVcs(any(), any(), any(), any(), any(), any(), any()); verify(mockCimitService, never()).getContraIndicators(any(), any(), any()); InOrder inOrder = inOrder(mockIpvSessionService); - inOrder.verify(mockIpvSessionService).updateIpvSession(mockIpvSessionItem); + inOrder.verify(mockIpvSessionService).updateIpvSession(ipvSessionItem); inOrder.verifyNoMoreInteractions(); assertEquals("/journey/next", lambdaResult.get("journey")); @@ -153,19 +159,19 @@ void handleRequestShouldReturnJourneyNextIfEmptyListReceived() throws Exception @Test void handleRequestShouldReturnFailWithCiIfBreachingCiReceived() throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(clientOAuthSessionItem); - when(mockTicfCriService.getTicfVc(clientOAuthSessionItem, mockIpvSessionItem)) + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); + when(mockTicfCriService.getTicfVc(CLIENT_OAUTH_SESSION_ITEM, ipvSessionItem)) .thenReturn(List.of(mockVerifiableCredential)); when(mockCimitUtilityService.getMitigationJourneyIfBreaching(any(), any())) .thenReturn(Optional.of(JOURNEY_FAIL_WITH_CI)); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); - InOrder inOrder = inOrder(mockIpvSessionItem, mockIpvSessionService); - inOrder.verify(mockIpvSessionItem).setVot(Vot.P0); - inOrder.verify(mockIpvSessionService).updateIpvSession(mockIpvSessionItem); + InOrder inOrder = inOrder(ipvSessionItem, mockIpvSessionService); + inOrder.verify(ipvSessionItem).setVot(P0); + inOrder.verify(mockIpvSessionService).updateIpvSession(ipvSessionItem); inOrder.verifyNoMoreInteractions(); assertEquals("/journey/fail-with-ci", lambdaResult.get("journey")); @@ -173,24 +179,50 @@ void handleRequestShouldReturnFailWithCiIfBreachingCiReceived() throws Exception @Test void handleRequestShouldReturnEnhancedVerificationIfBreachingCiReceived() throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(clientOAuthSessionItem); - when(mockTicfCriService.getTicfVc(clientOAuthSessionItem, mockIpvSessionItem)) + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); + when(mockTicfCriService.getTicfVc(CLIENT_OAUTH_SESSION_ITEM, ipvSessionItem)) .thenReturn(List.of(mockVerifiableCredential)); when(mockCimitUtilityService.getMitigationJourneyIfBreaching(any(), any())) .thenReturn(Optional.of(new JourneyResponse(JOURNEY_ENHANCED_VERIFICATION))); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); - InOrder inOrder = inOrder(mockIpvSessionItem, mockIpvSessionService); - inOrder.verify(mockIpvSessionItem).setVot(Vot.P0); - inOrder.verify(mockIpvSessionService).updateIpvSession(mockIpvSessionItem); + InOrder inOrder = inOrder(ipvSessionItem, mockIpvSessionService); + inOrder.verify(ipvSessionItem).setVot(P0); + inOrder.verify(mockIpvSessionService).updateIpvSession(ipvSessionItem); inOrder.verifyNoMoreInteractions(); assertEquals(JOURNEY_ENHANCED_VERIFICATION, lambdaResult.get("journey")); } + @Test + void handleRequestShouldSkipCiCheckIfReverificationJourney() throws Exception { + var reverificationClientSessionItem = + ClientOAuthSessionItem.builder().scope(REVERIFICATION).build(); + + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); + when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) + .thenReturn(reverificationClientSessionItem); + when(mockTicfCriService.getTicfVc(reverificationClientSessionItem, ipvSessionItem)) + .thenReturn(List.of(mockVerifiableCredential)); + + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); + + assertEquals("/journey/next", lambdaResult.get("journey")); + verify(mockCriStoringService) + .storeVcs( + TICF, + "an-ip-address", + "device-information", + List.of(mockVerifiableCredential), + reverificationClientSessionItem, + ipvSessionItem, + List.of()); + verify(mockCimitService, never()).getContraIndicators(any(), any(), any()); + } + @Test void handleRequestShouldReturnJourneyErrorResponseIfMissingIpvSessionId() { ProcessRequest inputWithoutSessionId = new ProcessRequest(); @@ -205,15 +237,33 @@ void handleRequestShouldReturnJourneyErrorResponseIfMissingIpvSessionId() { ErrorResponse.MISSING_IPV_SESSION_ID.getMessage(), lambdaResult.get("message")); } + @Test + void handleRequestShouldReturnJourneyErrorResponseIfMissingTargetVot() throws Exception { + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); + when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); + when(mockTicfCriService.getTicfVc(CLIENT_OAUTH_SESSION_ITEM, ipvSessionItem)) + .thenReturn(List.of(mockVerifiableCredential)); + ipvSessionItem.setTargetVot(null); + ipvSessionItem.setVot(P0); + + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); + + assertEquals("/journey/error", lambdaResult.get("journey")); + assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, lambdaResult.get("statusCode")); + assertEquals(ErrorResponse.MISSING_TARGET_VOT.getCode(), lambdaResult.get("code")); + assertEquals(ErrorResponse.MISSING_TARGET_VOT.getMessage(), lambdaResult.get("message")); + } + @Test void handleRequestShouldReturnJourneyErrorResponseIfTicfCriServiceThrows() throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(new ClientOAuthSessionItem()); + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); when(mockTicfCriService.getTicfVc(any(), any())) .thenThrow(new TicfCriServiceException("Oh dear")); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); assertEquals("/journey/error", lambdaResult.get("journey")); assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, lambdaResult.get("statusCode")); @@ -236,7 +286,7 @@ private static Stream ciStoringExceptions() { @MethodSource("ciStoringExceptions") void handleRequestShouldReturnJourneyErrorResponseIfCiStoringServiceThrows(Exception e) throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) .thenReturn(new ClientOAuthSessionItem()); when(mockTicfCriService.getTicfVc(any(), any())) @@ -245,7 +295,7 @@ void handleRequestShouldReturnJourneyErrorResponseIfCiStoringServiceThrows(Excep .when(mockCriStoringService) .storeVcs(any(), any(), any(), any(), any(), any(), any()); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); assertEquals("/journey/error", lambdaResult.get("journey")); assertEquals(HttpStatus.SC_INTERNAL_SERVER_ERROR, lambdaResult.get("statusCode")); @@ -259,18 +309,18 @@ void handleRequestShouldReturnJourneyErrorResponseIfCiStoringServiceThrows(Excep @Test void handleRequestShouldReturnJourneyErrorResponseIfCimitServiceThrows() throws Exception { - when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(mockIpvSessionItem); + when(mockIpvSessionService.getIpvSession("a-session-id")).thenReturn(ipvSessionItem); when(mockClientOAuthSessionDetailsService.getClientOAuthSession(any())) - .thenReturn(new ClientOAuthSessionItem()); + .thenReturn(CLIENT_OAUTH_SESSION_ITEM); when(mockTicfCriService.getTicfVc(any(), any())) .thenReturn(List.of(mockVerifiableCredential)); when(mockCimitService.getContraIndicators(any(), any(), any())) .thenThrow(new CiRetrievalException("Oh dear")); - Map lambdaResult = callTicfCriHandler.handleRequest(input, mockContext); + Map lambdaResult = callTicfCriHandler.handleRequest(INPUT, mockContext); InOrder inOrder = inOrder(mockIpvSessionService); - inOrder.verify(mockIpvSessionService).updateIpvSession(mockIpvSessionItem); + inOrder.verify(mockIpvSessionService).updateIpvSession(ipvSessionItem); inOrder.verifyNoMoreInteractions(); assertEquals("/journey/error", lambdaResult.get("journey")); @@ -295,7 +345,7 @@ void shouldLogRuntimeExceptionsAndRethrow() throws Exception { var thrown = assertThrows( Exception.class, - () -> callTicfCriHandler.handleRequest(input, mockContext), + () -> callTicfCriHandler.handleRequest(INPUT, mockContext), "Expected handleRequest() to throw, but it didn't"); // Assert diff --git a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java index a14fb63526..274a6fe575 100644 --- a/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java +++ b/lambdas/check-reverification-identity/src/main/java/uk/gov/di/ipv/core/checkreverificationidentity/CheckReverificationIdentityHandler.java @@ -64,7 +64,6 @@ public CheckReverificationIdentityHandler( ClientOAuthSessionDetailsService clientOAuthSessionDetailsService, EvcsService evcsService, UserIdentityService userIdentityService, - Gpg45ProfileEvaluator gpg45ProfileEvaluator, VotMatcher votMatcher) { this.configService = configService; this.ipvSessionService = ipvSessionService; diff --git a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java index 3f722680a8..ae86feb82e 100644 --- a/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java +++ b/libs/common-services/src/main/java/uk/gov/di/ipv/core/library/persistence/item/IpvSessionItem.java @@ -112,7 +112,7 @@ public JourneyState getPreviousState() { // If the user has achieved a profile we should use that, if they haven't then we should use the // target Vot. public Vot getThresholdVot() { - return vot == Vot.P0 ? targetVot : vot; + return Vot.P0 == vot ? targetVot : vot; } public void setJourneyContext(String journeyContext) {