Skip to content

Commit

Permalink
Fix #306: Implement approval UI extension payload (#307)
Browse files Browse the repository at this point in the history
* Fix #306: Implement approval UI extension payload

* Change type of ES_OPERATION_TEMPLATE.UI to CLOB

* Convert PAS OperationDetail#riskFlags to UiExtensions

* Add RISK_FLAG_FRAUD_WARNING

* Add test

* Remove underscores from test

* Replace the Czech text by English one

Co-authored-by: Lubos Racansky <lubos.racansky@gmail.com>
  • Loading branch information
petrdvorak and banterCZ authored Dec 15, 2022
1 parent 59ccdd2 commit 744a7cf
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 10 deletions.
3 changes: 2 additions & 1 deletion docs/sql/mysql/enrollment/create-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ CREATE TABLE es_operation_template (
language VARCHAR(8) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
attributes TEXT
attributes TEXT,
ui TEXT
) ENGINE=InnoDB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE UNIQUE INDEX es_operation_template_placeholder ON es_operation_template(placeholder, language);
3 changes: 2 additions & 1 deletion docs/sql/oracle/enrollment/create-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ CREATE TABLE ES_OPERATION_TEMPLATE (
LANGUAGE VARCHAR2(8 CHAR) NOT NULL,
TITLE VARCHAR2(255 CHAR) NOT NULL,
MESSAGE CLOB NOT NULL,
ATTRIBUTES CLOB
ATTRIBUTES CLOB,
UI CLOB
);

CREATE UNIQUE INDEX ES_OPERATION_TEMPLATE_PLACEHOLDER ON ES_OPERATION_TEMPLATE(PLACEHOLDER, LANGUAGE);
3 changes: 2 additions & 1 deletion docs/sql/postgresql/enrollment/create-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ CREATE TABLE es_operation_template (
language VARCHAR(8) NOT NULL,
title VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
attributes TEXT
attributes TEXT,
ui TEXT
);
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ public class OperationTemplateEntity implements Serializable {
@Column(name = "message", nullable = false)
private String message;

@Column(name = "ui")
private String ui;

@Column(name = "attributes")
private String attributes;

Expand All @@ -69,11 +72,12 @@ public boolean equals(Object o) {
&& Objects.equals(language, that.language)
&& Objects.equals(title, that.title)
&& Objects.equals(message, that.message)
&& Objects.equals(ui, that.ui)
&& Objects.equals(attributes, that.attributes);
}

@Override
public int hashCode() {
return Objects.hash(placeholder, language, title, message, attributes);
return Objects.hash(placeholder, language, title, message, ui, attributes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@
import com.wultra.app.enrollmentserver.errorhandling.MobileTokenConfigurationException;
import com.wultra.security.powerauth.client.model.enumeration.SignatureType;
import com.wultra.security.powerauth.client.model.response.OperationDetailResponse;
import com.wultra.security.powerauth.lib.mtoken.model.entity.AllowedSignatureType;
import com.wultra.security.powerauth.lib.mtoken.model.entity.FormData;
import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation;
import com.wultra.security.powerauth.lib.mtoken.model.entity.*;
import com.wultra.security.powerauth.lib.mtoken.model.entity.attributes.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.StringSubstitutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.util.ArrayList;
Expand All @@ -46,9 +44,12 @@
* @author Petr Dvorak, petr@wultra.com
*/
@Component
@Slf4j
public class MobileTokenConverter {

private static final Logger logger = LoggerFactory.getLogger(MobileTokenConverter.class);
private static final String RISK_FLAG_FLIP_BUTTONS = "X";
private static final String RISK_FLAG_BLOCK_APPROVAL_ON_CALL = "C";
private static final String RISK_FLAG_FRAUD_WARNING = "F";

private final ObjectMapper objectMapper;

Expand All @@ -75,6 +76,15 @@ private AllowedSignatureType convert(List<SignatureType> signatureType) {
return allowedSignatureType;
}

/**
* Convert operation detail from PowerAuth Server and operation template from Enrollment Server into an
* operation in API response.
*
* @param operationDetail Operation detail response obtained from PowerAuth Server.
* @param operationTemplate Operation template obtained from Enrollment Server.
* @return Operation for API response.
* @throws MobileTokenConfigurationException In case there is an error in configuration data.
*/
public Operation convert(OperationDetailResponse operationDetail, OperationTemplateEntity operationTemplate) throws MobileTokenConfigurationException {
try {
final Operation operation = new Operation();
Expand All @@ -86,6 +96,8 @@ public Operation convert(OperationDetailResponse operationDetail, OperationTempl
operation.setOperationExpires(operationDetail.getTimestampExpires());
operation.setStatus(operationDetail.getStatus().name());

operation.setUi(convertUiExtension(operationDetail, operationTemplate));

// Prepare title and message with substituted attributes
final FormData formData = new FormData();
final Map<String, String> parameters = operationDetail.getParameters();
Expand Down Expand Up @@ -120,6 +132,32 @@ public Operation convert(OperationDetailResponse operationDetail, OperationTempl
}
}

private UiExtensions convertUiExtension(final OperationDetailResponse operationDetail, final OperationTemplateEntity operationTemplate) throws JsonProcessingException {
if (StringUtils.hasText(operationTemplate.getUi())) {
final String uiJsonString = operationTemplate.getUi();
logger.debug("Deserializing ui: '{}' of OperationTemplate ID: {} to UiExtensions", uiJsonString, operationTemplate.getId());
return objectMapper.readValue(uiJsonString, UiExtensions.class);
} else if (StringUtils.hasText(operationDetail.getRiskFlags())) {
final String riskFlags = operationDetail.getRiskFlags();
logger.debug("Converting riskFlags: '{}' of OperationDetail ID: {} to UiExtensions", riskFlags, operationDetail.getId());
final UiExtensions ui = new UiExtensions();
if (riskFlags.contains(RISK_FLAG_FLIP_BUTTONS)) {
ui.setFlipButtons(true);
}
if (riskFlags.contains(RISK_FLAG_BLOCK_APPROVAL_ON_CALL)) {
ui.setBlockApprovalOnCall(true);
}
if (riskFlags.contains(RISK_FLAG_FRAUD_WARNING)) {
final PreApprovalScreen preApprovalScreen = new PreApprovalScreen();
preApprovalScreen.setType(PreApprovalScreen.ScreenType.WARNING);
ui.setPreApprovalScreen(preApprovalScreen);
}
return ui;
} else {
return null;
}
}

private Attribute buildAttribute(OperationTemplateParam templateParam, Map<String, String> params) {
final String type = templateParam.getType();
final String id = templateParam.getId();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2022 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.enrollmentserver.impl.service.converter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.wultra.app.enrollmentserver.database.entity.OperationTemplateEntity;
import com.wultra.security.powerauth.client.model.enumeration.OperationStatus;
import com.wultra.security.powerauth.client.model.enumeration.SignatureType;
import com.wultra.security.powerauth.client.model.response.OperationDetailResponse;
import com.wultra.security.powerauth.lib.mtoken.model.entity.Operation;
import com.wultra.security.powerauth.lib.mtoken.model.entity.PreApprovalScreen;
import com.wultra.security.powerauth.lib.mtoken.model.entity.UiExtensions;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
* Test for {@link MobileTokenConverter}.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
class MobileTokenConverterTest {

private static final String TEMPLATE_UI = "{\n" +
" \"flipButtons\": true,\n" +
" \"blockApprovalOnCall\": false,\n" +
" \"preApprovalScreen\": {\n" +
" \"type\": \"WARNING\",\n" +
" \"heading\": \"Watch out!\",\n" +
" \"message\": \"You may become a victim of an attack.\",\n" +
" \"items\": [\n" +
" \"You activate a new app and allow access to your accounts\",\n" +
" \"Make sure the activation takes place on your device\",\n" +
" \"If you have been prompted for this operation in connection with a payment, decline it\"\n" +
" ],\n" +
" \"approvalType\": \"SLIDER\"\n" +
" }\n" +
"}";

private final MobileTokenConverter tested = new MobileTokenConverter(new ObjectMapper());

@Test
void testConvertUiNull() throws Exception {
final OperationDetailResponse operationDetail = new OperationDetailResponse();
operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE));
operationDetail.setStatus(OperationStatus.APPROVED);

final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();

final Operation result = tested.convert(operationDetail, operationTemplate);

assertNull(result.getUi());
}

@Test
void testConvertUiOverriddenByEnrollment() throws Exception {
final OperationDetailResponse operationDetail = new OperationDetailResponse();
operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE));
operationDetail.setStatus(OperationStatus.APPROVED);
operationDetail.setRiskFlags("C");

final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();
operationTemplate.setUi(TEMPLATE_UI);

final Operation result = tested.convert(operationDetail, operationTemplate);

assertNotNull(result.getUi());

final UiExtensions ui = result.getUi();
assertEquals(true, ui.getFlipButtons());
assertEquals(false, ui.getBlockApprovalOnCall());
assertNotNull(ui.getPreApprovalScreen());

final PreApprovalScreen preApprovalScreen = ui.getPreApprovalScreen();
assertEquals(PreApprovalScreen.ScreenType.WARNING, preApprovalScreen.getType());
assertEquals("Watch out!", preApprovalScreen.getHeading());
assertEquals("You may become a victim of an attack.", preApprovalScreen.getMessage());
assertEquals(PreApprovalScreen.ApprovalType.SLIDER, preApprovalScreen.getApprovalType());
assertNotNull(preApprovalScreen.getItems());

final List<String> items = preApprovalScreen.getItems();
assertEquals(3, items.size());
assertEquals("You activate a new app and allow access to your accounts", items.get(0));
}

@Test
void testConvertUiRiskFlagsX() throws Exception {
final OperationDetailResponse operationDetail = new OperationDetailResponse();
operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE));
operationDetail.setStatus(OperationStatus.APPROVED);
operationDetail.setRiskFlags("X");

final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();

final Operation result = tested.convert(operationDetail, operationTemplate);

assertNotNull(result.getUi());

final UiExtensions ui = result.getUi();
assertEquals(true, ui.getFlipButtons());
assertNull(ui.getBlockApprovalOnCall());
assertNull(ui.getPreApprovalScreen());
}

@Test
void testConvertUiRiskFlagsC() throws Exception {
final OperationDetailResponse operationDetail = new OperationDetailResponse();
operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE));
operationDetail.setStatus(OperationStatus.APPROVED);
operationDetail.setRiskFlags("C");

final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();

final Operation result = tested.convert(operationDetail, operationTemplate);

assertNotNull(result.getUi());

final UiExtensions ui = result.getUi();
assertNull(ui.getFlipButtons());
assertEquals(true, ui.getBlockApprovalOnCall());
assertNull(ui.getPreApprovalScreen());
}

@Test
void testConvertUiRiskFlagsXCF() throws Exception {
final OperationDetailResponse operationDetail = new OperationDetailResponse();
operationDetail.setSignatureType(List.of(SignatureType.KNOWLEDGE));
operationDetail.setStatus(OperationStatus.APPROVED);
operationDetail.setRiskFlags("XCF");

final OperationTemplateEntity operationTemplate = new OperationTemplateEntity();

final Operation result = tested.convert(operationDetail, operationTemplate);

assertNotNull(result.getUi());

final UiExtensions ui = result.getUi();
assertEquals(true, ui.getFlipButtons());
assertEquals(true, ui.getBlockApprovalOnCall());
assertNotNull(ui.getPreApprovalScreen());

final PreApprovalScreen preApprovalScreen = ui.getPreApprovalScreen();
assertEquals(PreApprovalScreen.ScreenType.WARNING, preApprovalScreen.getType());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class Operation {
private String status;
private Date operationCreated;
private Date operationExpires;
private UiExtensions ui;
@NotNull
private AllowedSignatureType allowedSignatureType;
@NotNull
Expand Down
Loading

0 comments on commit 744a7cf

Please sign in to comment.