Skip to content

Commit

Permalink
[PAYOP-1116] Implement Larky function for rendering network token (AL…
Browse files Browse the repository at this point in the history
…T implementation) (#327)

* Add code provided by salava

* Interface

* Rename module

* Implement json path stuff

* Add token module stuff

* Add tests

* More test cases

* Add NT not found test

* Remove unused stuff

* Handle all the corner cases

* Make member private

* Extract json path version

* Remove json path deps

* New approach

* Update tests

* Implement the same interface with jsonpath in pure larky

* Add nts helpers

* Code style

* Update for feedbacks

* doc change

* Add payment details

* Fix the interface, add payment details

* Fix get network token tests

* Fix noop test

* Improve nts helper interface

* Update docs

* Update doc

* Fix a bug in test

* Move file around to vgs folder instead
  • Loading branch information
fangpenlin authored Mar 21, 2023
1 parent d15249b commit 42a83fb
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.verygood.security.larky.modules.BinasciiModule;
import com.verygood.security.larky.modules.C99MathModule;
import com.verygood.security.larky.modules.NetworkTokenModule;
import com.verygood.security.larky.modules.CerebroModule;
import com.verygood.security.larky.modules.ChaseModule;
import com.verygood.security.larky.modules.CodecsModule;
Expand Down Expand Up @@ -96,6 +97,7 @@ public class ModuleSupplier {

public static final ImmutableSet<StarlarkValue> VGS_MODULES = ImmutableSet.of(
VaultModule.INSTANCE,
NetworkTokenModule.INSTANCE,
CerebroModule.INSTANCE,
ChaseModule.INSTANCE,
JKSModule.INSTANCE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.verygood.security.larky.modules;

import com.google.common.collect.ImmutableList;
import com.verygood.security.larky.modules.vgs.nts.LarkyNetworkToken;
import com.verygood.security.larky.modules.vgs.nts.MockNetworkTokenService;
import com.verygood.security.larky.modules.vgs.nts.NoopNetworkTokenService;
import com.verygood.security.larky.modules.vgs.nts.spi.NetworkTokenService;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import net.starlark.java.annot.Param;
import net.starlark.java.annot.ParamType;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkInt;
import net.starlark.java.eval.StarlarkThread;

@StarlarkBuiltin(name = "nts", category = "BUILTIN", doc = "Overridable Network Token API in Larky")
public class NetworkTokenModule implements LarkyNetworkToken {
public static final NetworkTokenModule INSTANCE = new NetworkTokenModule();

public static final String ENABLE_MOCK_PROPERTY = "larky.modules.vgs.nts.enableMockNetworkToken";

private final NetworkTokenService networkTokenService;

public NetworkTokenModule() {
ServiceLoader<NetworkTokenService> loader = ServiceLoader.load(NetworkTokenService.class);
List<NetworkTokenService> networkTokenProviders = ImmutableList.copyOf(loader.iterator());

if (Boolean.getBoolean(ENABLE_MOCK_PROPERTY)) {
networkTokenService = new MockNetworkTokenService();
} else if (networkTokenProviders.isEmpty()) {
networkTokenService = new NoopNetworkTokenService();
} else {
if (networkTokenProviders.size() != 1) {
throw new IllegalArgumentException(
String.format(
"NetworkTokenModule expecting only 1 network token provider of type NetworkTokenService, found %d",
networkTokenProviders.size()));
}
networkTokenService = networkTokenProviders.get(0);
}
}

@StarlarkMethod(
name = "get_network_token",
doc = "Retrieves a network token for the given PAN alias.",
useStarlarkThread = true,
parameters = {
@Param(
name = "pan",
named = true,
doc = "PAN alias. Used to look up the corresponding network token to be returned",
allowedTypes = {@ParamType(type = String.class)}),
@Param(
name = "cvv",
named = true,
doc =
"CVV of the credit card. Used to pass to the network for retrieving the corresponding network token "
+ "and cryptogram to be returned",
allowedTypes = {@ParamType(type = String.class)}),
@Param(
name = "amount",
named = true,
doc =
"The amount of payment for the transaction to be made with the network token. Used to pass to the "
+ "network for retrieving the corresponding network token and cryptogram to be returned",
allowedTypes = {@ParamType(type = String.class)}),
@Param(
name = "currency_code",
named = true,
doc =
"The currency code of payment amount for the transaction to be made with the network token. Used to "
+ "pass to the network for retrieving the corresponding network token and cryptogram to be "
+ "returned",
allowedTypes = {@ParamType(type = String.class)}),
})
@Override
public Dict<String, Object> getNetworkToken(
String pan, String cvv, String amount, String currencyCode, StarlarkThread thread)
throws EvalException {
if (pan.trim().isEmpty()) {
throw Starlark.errorf("pan argument cannot be blank");
}
final Optional<NetworkTokenService.NetworkToken> networkTokenOptional;
try {
networkTokenOptional = networkTokenService.getNetworkToken(pan, cvv, amount, currencyCode);
} catch (UnsupportedOperationException exception) {
throw Starlark.errorf("nts.get_network_token operation must be overridden");
}
if (!networkTokenOptional.isPresent()) {
throw Starlark.errorf("network token is not found");
}
final NetworkTokenService.NetworkToken networkToken = networkTokenOptional.get();
return Dict.<String, Object>builder()
.put("token", networkToken.getToken())
.put("exp_month", StarlarkInt.of(networkToken.getExpireMonth()))
.put("exp_year", StarlarkInt.of(networkToken.getExpireYear()))
.put("cryptogram_value", networkToken.getCryptogramValue())
.put("cryptogram_eci", networkToken.getCryptogramEci())
.build(thread.mutability());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.verygood.security.larky.modules.vgs.nts;

import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.StarlarkValue;

public interface LarkyNetworkToken extends StarlarkValue {
/**
* Get network token by pan alias.
*
* @param pan the pan alias value for getting network token
* @param cvv cvv of card for retrieving cryptogram
* @param amount amount of payment for retrieving cryptogram
* @param currencyCode currency code of payment for retrieving cryptogram
* @param thread Starlark thread object
* @return a dict contains the network token values
*/
Dict<String, Object> getNetworkToken(
String pan, String cvv, String amount, String currencyCode, StarlarkThread thread)
throws EvalException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.verygood.security.larky.modules.vgs.nts;

import com.verygood.security.larky.modules.vgs.nts.spi.NetworkTokenService;
import java.util.Optional;

public class MockNetworkTokenService implements NetworkTokenService {
@Override
public Optional<NetworkToken> getNetworkToken(
String panAlias, String cvv, String amount, String currencyCode) {
if (panAlias.equals("NOT_FOUND")) {
return Optional.empty();
}
return Optional.of(
NetworkToken.builder()
.token("4242424242424242")
.expireMonth(12)
.expireYear(27)
.cryptogramValue("MOCK_CRYPTOGRAM_VALUE")
.cryptogramEci("MOCK_CRYPTOGRAM_ECI")
.build());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.verygood.security.larky.modules.vgs.nts;

import com.verygood.security.larky.modules.vgs.nts.spi.NetworkTokenService;
import java.util.Optional;

public class NoopNetworkTokenService implements NetworkTokenService {
@Override
public Optional<NetworkToken> getNetworkToken(
String panAlias, String cvv, String amount, String currencyCode) {
throw new UnsupportedOperationException("Not implemented");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.verygood.security.larky.modules.vgs.nts.spi;

import java.util.Optional;
import lombok.Builder;
import lombok.Data;

public interface NetworkTokenService {
/**
* Get network token for the given PAN alias.
*
* @param panAlias PAN alias of the network token to get
* @param cvv cvv of card for retrieving cryptogram
* @param amount amount of payment for retrieving cryptogram
* @param currencyCode currency code of payment for retrieving cryptogram
* @return the network token value
*/
Optional<NetworkToken> getNetworkToken(
String panAlias, String cvv, String amount, String currencyCode);

@Data
@Builder
class NetworkToken {
private final String token;
private final Integer expireMonth;
private final Integer expireYear;
private final String cryptogramValue;
private final String cryptogramEci;
}
}
104 changes: 104 additions & 0 deletions larky/src/main/resources/vgs/nts_helpers.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
load("@stdlib//larky", larky="larky")
load("@vendor//jsonpath_ng", jsonpath_ng="jsonpath_ng")
load("@vgs//nts", "nts")


def render(
input,
pan,
cvv,
amount,
currency_code,
raw_pan=False,
raw_cvv=False,
raw_amount=False,
raw_currency_code=False,
output_pan=None,
output_exp_month=None,
output_exp_year=None,
output_cryptogram_value=None,
output_cryptogram_eci=None,
):
"""Retrieves a network token for the given PAN alias, renders the cryptogram, and injects the network token values
into the payload.
For the output JSONPaths, please note that inserting a value into a non-existing deep nested note is not currently
supported. For example, for an input payload like this::
input = {
"data": {}
}
To insert into `$.data.network_token.exp_month` JSONPath, you need to place an empty value at the exact path first
like this in order to make JSONPath value insertion work::
input["data"]["network_token"] = {"exp_month": "TO_BE_REPLACED"}
nts_helpers.render(input, ...)
:param input: JSON payload to inject network token into
:param pan: JSONPath to the PAN alias in the input payload or a raw PAN alias value if `raw_pan` is true.
Used to look up the corresponding network token to be rendered and injected into the payload.
:param cvv: JSONPath to the CVV of the credit card in the input payload or a raw CVV value if `raw_cvv` is true.
Used to pass to the network for retrieving the corresponding network token and cryptogram to be returned.
:param amount: JSONPath to the amount of payment for the transaction to be made with the network token in the input
payload or a raw amount value if `raw_amount` is true. Used to pass to the network for retrieving the
corresponding network token and cryptogram to be returned.
:param currency_code: JSONPath to the currency code of payment amount for the transaction to be made with the
network token in the input payload or a raw amount value if `raw_amount` is true. Used to pass to the
network for retrieving the corresponding network token and cryptogram to be returned.
:param raw_pan: treat `pan` value as a raw input value instead of a JSONPath value
:param raw_cvv: treat `raw_cvv` value as a raw input value instead of a JSONPath value
:param raw_amount: treat `raw_amount` value as a raw input value instead of a JSONPath value
:param raw_currency_code: treat `raw_currency_code` value as a raw input value instead of a JSONPath value
:param output_pan: JSONPath to insert the PAN value of the network token within the input payload.
By default, the `pan` JSONPath path value will be used if no not provided.
:param output_exp_month: JSONPath to insert the expiration month of the network token within the input payload
:param output_exp_year: JSONPath to insert the expiration year of the network token within the input payload
:param output_cryptogram_value: JSONPath to insert the cryptogram value of the network token within the input
payload
:param output_cryptogram_eci: JSONPath to insert the cryptogram ECI of the network token within the input payload
:return: JSON payload injected with network token values
"""
if raw_pan:
pan_value = pan
else:
pan_value = jsonpath_ng.parse(pan).find(input).value
if raw_cvv:
cvv_value = cvv
else:
cvv_value = jsonpath_ng.parse(cvv).find(input).value
if raw_amount:
amount_value = amount
else:
amount_value = str(jsonpath_ng.parse(amount).find(input).value)
if raw_currency_code:
currency_code_value = currency_code
else:
currency_code_value = jsonpath_ng.parse(currency_code).find(input).value

network_token = nts.get_network_token(
pan=pan_value,
cvv=cvv_value,
amount=amount_value,
currency_code=currency_code_value,
)
output_pan_jp = output_pan
if output_pan_jp == None and not raw_pan:
output_pan_jp = pan
placements = [
(output_pan_jp, network_token["token"]),
(output_exp_month, network_token["exp_month"]),
(output_exp_year, network_token["exp_year"]),
(output_cryptogram_value, network_token["cryptogram_value"]),
(output_cryptogram_eci, network_token["cryptogram_eci"]),
]
for path, value in placements:
if path == None:
continue
input = jsonpath_ng.parse(path).update(input, value).value
return input


nts_helpers = larky.struct(
render=render
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.verygood.security.larky.modules.vgs.nts.MockNetworkTokenService
48 changes: 48 additions & 0 deletions larky/src/test/resources/vgs_tests/nts/test_default_nts.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
load("@vendor//asserts", "asserts")
load("@stdlib//unittest", "unittest")
load("@vgs//nts", "nts")


def _test_get_network_token():
output = nts.get_network_token(
pan="MOCK_PAN_ALIAS",
cvv="MOCK_CVV",
amount="123.45",
currency_code="USD",
)
asserts.assert_that(output).is_equal_to({
"token": "4242424242424242",
"exp_month": 12,
"exp_year": 27,
"cryptogram_value": "MOCK_CRYPTOGRAM_VALUE",
"cryptogram_eci": "MOCK_CRYPTOGRAM_ECI"
})


def _test_pan_empty_value():
asserts.assert_fails(lambda: nts.get_network_token("", cvv="MOCK_CVV", amount="123.45", currency_code="USD"),
"pan argument cannot be blank")


def _test_not_found():
input = {
"pan": "NOT_FOUND",
}
asserts.assert_fails(
lambda: nts.get_network_token("NOT_FOUND", cvv="MOCK_CVV", amount="123.45", currency_code="USD"),
"network token is not found")


def _suite():
_suite = unittest.TestSuite()

# Redact Tests
_suite.addTest(unittest.FunctionTestCase(_test_get_network_token))
_suite.addTest(unittest.FunctionTestCase(_test_pan_empty_value))
_suite.addTest(unittest.FunctionTestCase(_test_not_found))

return _suite


_runner = unittest.TextTestRunner()
_runner.run(_suite())
21 changes: 21 additions & 0 deletions larky/src/test/resources/vgs_tests/nts/test_noop_nts.star
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Unit tests for VaultModule.java using NoopVault API"""

load("@vendor//asserts", "asserts")
load("@stdlib//unittest", "unittest")
load("@vgs//nts", "nts")


def _test_get_network_token():
asserts.assert_fails(
lambda: nts.get_network_token(pan="MOCK_PAN_ALIAS", cvv="MOCK_CVV", amount="123.45", currency_code="USD"),
"nts.get_network_token operation must be overridden")


def _suite():
_suite = unittest.TestSuite()
_suite.addTest(unittest.FunctionTestCase(_test_get_network_token))
return _suite


_runner = unittest.TextTestRunner()
_runner.run(_suite())
Loading

0 comments on commit 42a83fb

Please sign in to comment.