From 3ea2eb9989db27e0600e7ce48fa1932d38ef5094 Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Wed, 21 Jan 2026 20:46:33 +0700 Subject: [PATCH 1/7] Add security and new method --- pom.xml | 102 ++++++++++++++---- .../converter/JwtAuthConverter.java | 53 +++++++++ ...ymentInspectRequestToContextConverter.java | 14 +-- .../api/resource/ChargebackResource.java | 3 +- .../api/resource/FraudPaymentsResource.java | 3 +- .../api/resource/PaymentResource.java | 3 +- .../api/resource/RefundResource.java | 3 +- .../api/resource/WithdrawalResource.java | 3 +- .../service/FraudbustersInspectorService.java | 23 +++- src/main/resources/application.yml | 30 +++--- .../api/auth/JwtTokenTestConfiguration.java | 25 +++++ .../auth/KeycloakOpenIdTestConfiguration.java | 22 ++++ .../api/auth/utils/JwtTokenBuilder.java | 91 ++++++++++++++++ .../api/auth/utils/KeycloakOpenIdStub.java | 94 ++++++++++++++++ ...bstractKeycloakOpenIdAsWiremockConfig.java | 37 +++++++ .../api/resource/ResourceTest.java | 60 +++++++++-- .../api/testutil/GenerateSelfSigned.java | 37 +++++++ .../api/testutil/PublicKeyUtil.java | 25 +++++ .../fraudbusters/api/testutil/RandomUtil.java | 37 +++++++ .../api/utils/ApiBeanGenerator.java | 2 +- 20 files changed, 607 insertions(+), 60 deletions(-) create mode 100644 src/main/java/dev/vality/fraudbusters/api/configuration/converter/JwtAuthConverter.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/auth/JwtTokenTestConfiguration.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/auth/KeycloakOpenIdTestConfiguration.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/auth/utils/JwtTokenBuilder.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/auth/utils/KeycloakOpenIdStub.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/testutil/GenerateSelfSigned.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/testutil/PublicKeyUtil.java create mode 100644 src/test/java/dev/vality/fraudbusters/api/testutil/RandomUtil.java diff --git a/pom.xml b/pom.xml index a0d1dbc..4487067 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.vality service-parent-pom - 1.0.16 + 3.1.9 fraudbusters-api @@ -23,24 +23,18 @@ 8023 ${server.port} ${management.port} ${env.REGISTRY} - 1.47-7bd0be7-server + 1.117-7e0ac25 1.108-0800fde + 0.12.6 + 1.681-7a97267 - - dev.vality.woody - woody-thrift - - - dev.vality - shared-resources - dev.vality - swag-fraudbusters + swag-fraudbusters-server ${swag-fraudbusters.version} @@ -48,10 +42,25 @@ fraudbusters-proto ${fraudbusters-proto.version} + + dev.vality + damsel + ${damsel.version} + dev.vality.geck serializer + + dev.vality.woody + libthrift + 2.0.9 + + + dev.vality.woody + woody-thrift + 2.0.9 + dev.vality.geck common @@ -85,15 +94,29 @@ org.springframework.boot spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + - javax.servlet - javax.servlet-api + jakarta.servlet + jakarta.servlet-api + + + jakarta.validation + jakarta.validation-api + provided org.projectlombok lombok + 1.18.36 provided @@ -110,12 +133,36 @@ org.springframework.boot spring-boot-starter-test test - - - org.junit.vintage - junit-vintage-engine - - + + + io.jsonwebtoken + jjwt-api + ${jjwt-version} + test + + + io.jsonwebtoken + jjwt-impl + ${jjwt-version} + test + + + io.jsonwebtoken + jjwt-jackson + ${jjwt-version} + test + + + org.wiremock.integrations + wiremock-spring-boot + 3.8.2 + test + + + org.bouncycastle + bcpkix-jdk18on + 1.79 + test @@ -148,6 +195,23 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 15 + 15 + true + + + org.projectlombok + lombok + 1.18.36 + + + + org.apache.maven.plugins maven-remote-resources-plugin diff --git a/src/main/java/dev/vality/fraudbusters/api/configuration/converter/JwtAuthConverter.java b/src/main/java/dev/vality/fraudbusters/api/configuration/converter/JwtAuthConverter.java new file mode 100644 index 0000000..3da5440 --- /dev/null +++ b/src/main/java/dev/vality/fraudbusters/api/configuration/converter/JwtAuthConverter.java @@ -0,0 +1,53 @@ +package dev.vality.fraudbusters.api.configuration.converter; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Component +public class JwtAuthConverter implements Converter { + + private static final String principleAttribute = "preferred_username"; + private static final String resourceAttribute = "resource_access"; + + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + return new JwtAuthenticationToken( + jwt, + new HashSet<>(extractResourceRoles(jwt)), + getPrincipleClaimName(jwt) + ); + } + + private Collection extractResourceRoles(Jwt token) { + if (token.getClaim(resourceAttribute) == null) { + return Set.of(); + } + Map resourceAccess = token.getClaim(resourceAttribute); + if (resourceAccess.isEmpty()) { + return Set.of(); + } + + return resourceAccess.values().stream() + .map(resourceAccessInfo -> (Map) resourceAccessInfo) + .flatMap(resourceAccessInfo -> ((Collection) resourceAccessInfo.get("roles")).stream()) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + } + + private String getPrincipleClaimName(Jwt jwt) { + return jwt.getClaim(principleAttribute); + } +} diff --git a/src/main/java/dev/vality/fraudbusters/api/converter/PaymentInspectRequestToContextConverter.java b/src/main/java/dev/vality/fraudbusters/api/converter/PaymentInspectRequestToContextConverter.java index 3ba923e..6a0e3d2 100644 --- a/src/main/java/dev/vality/fraudbusters/api/converter/PaymentInspectRequestToContextConverter.java +++ b/src/main/java/dev/vality/fraudbusters/api/converter/PaymentInspectRequestToContextConverter.java @@ -1,12 +1,11 @@ package dev.vality.fraudbusters.api.converter; -import dev.vality.damsel.domain.BankCard; import dev.vality.damsel.domain.*; +import dev.vality.damsel.domain.BankCard; +import dev.vality.damsel.proxy_inspector.*; import dev.vality.damsel.proxy_inspector.Invoice; import dev.vality.damsel.proxy_inspector.InvoicePayment; -import dev.vality.damsel.proxy_inspector.Party; import dev.vality.damsel.proxy_inspector.Shop; -import dev.vality.damsel.proxy_inspector.*; import dev.vality.swag.fraudbusters.model.*; import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; @@ -38,9 +37,9 @@ public Context convert(PaymentInspectRequest request) { private Shop buildShop(Merchant merchant) { var shop = merchant.getShop(); return new Shop() - .setId(shop.getId()) - .setDetails(new ShopDetails() - .setName(shop.getName())) + .setShopRef(new ShopConfigRef() + .setId(shop.getId())) + .setName(shop.getName()) .setCategory(new Category() .setName(shop.getCategory()) .setDescription(MOCK_UNUSED_DATA)) @@ -49,7 +48,8 @@ private Shop buildShop(Merchant merchant) { private Party buildParty(Merchant merchant) { return new Party() - .setPartyId(merchant.getId()); + .setPartyRef(new PartyConfigRef() + .setId(merchant.getId())); } private Invoice buildInvoice(Payment payment) { diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/ChargebackResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/ChargebackResource.java index 7d8665a..5623f3e 100644 --- a/src/main/java/dev/vality/fraudbusters/api/resource/ChargebackResource.java +++ b/src/main/java/dev/vality/fraudbusters/api/resource/ChargebackResource.java @@ -12,7 +12,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; import java.util.List; @Slf4j @@ -24,7 +23,7 @@ public class ChargebackResource implements ChargebacksApi { private final Converter> chargebacksRequestToChargebacksConverter; @Override - public ResponseEntity insertChargebacks(@Valid ChargebacksRequest chargebacksRequest) { + public ResponseEntity insertChargebacks(ChargebacksRequest chargebacksRequest) { log.debug("-> insertChargebacks request: {}", chargebacksRequest); if (!CollectionUtils.isEmpty(chargebacksRequest.getChargebacks())) { List chargebacks = chargebacksRequestToChargebacksConverter.convert(chargebacksRequest); diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/FraudPaymentsResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/FraudPaymentsResource.java index ad5bd6f..72766a6 100644 --- a/src/main/java/dev/vality/fraudbusters/api/resource/FraudPaymentsResource.java +++ b/src/main/java/dev/vality/fraudbusters/api/resource/FraudPaymentsResource.java @@ -12,7 +12,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; import java.util.List; @Slf4j @@ -24,7 +23,7 @@ public class FraudPaymentsResource implements FraudPaymentsApi { private final Converter> fraudPaymentsRequestToFraudPaymentsConverter; @Override - public ResponseEntity insertFraudPayments(@Valid FraudPaymentsRequest fraudPaymentsRequest) { + public ResponseEntity insertFraudPayments(FraudPaymentsRequest fraudPaymentsRequest) { log.debug("-> insertFraudPayments request: {}", fraudPaymentsRequest); if (!CollectionUtils.isEmpty(fraudPaymentsRequest.getFraudPayments())) { List fraudPayments = diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/PaymentResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/PaymentResource.java index 152228f..71f2556 100644 --- a/src/main/java/dev/vality/fraudbusters/api/resource/PaymentResource.java +++ b/src/main/java/dev/vality/fraudbusters/api/resource/PaymentResource.java @@ -12,7 +12,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; import java.util.List; @Slf4j @@ -24,7 +23,7 @@ public class PaymentResource implements PaymentsApi { private final Converter> paymentsChangesRequestToPaymentsConverter; @Override - public ResponseEntity insertPaymentsChanges(@Valid PaymentsChangesRequest paymentsChangesRequest) { + public ResponseEntity insertPaymentsChanges(PaymentsChangesRequest paymentsChangesRequest) { log.debug("-> insertPaymentsChanges request: {}", paymentsChangesRequest); if (!CollectionUtils.isEmpty(paymentsChangesRequest.getPaymentsChanges())) { List payments = paymentsChangesRequestToPaymentsConverter.convert(paymentsChangesRequest); diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/RefundResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/RefundResource.java index 9b0ae51..74ad14c 100644 --- a/src/main/java/dev/vality/fraudbusters/api/resource/RefundResource.java +++ b/src/main/java/dev/vality/fraudbusters/api/resource/RefundResource.java @@ -12,7 +12,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; import java.util.List; @Slf4j @@ -24,7 +23,7 @@ public class RefundResource implements RefundsApi { private final Converter> refundsRequestToRefundsConverter; @Override - public ResponseEntity insertRefunds(@Valid RefundsRequest refundsRequest) { + public ResponseEntity insertRefunds(RefundsRequest refundsRequest) { log.debug("-> insertRefunds request: {}", refundsRequest); if (!CollectionUtils.isEmpty(refundsRequest.getRefunds())) { List refunds = refundsRequestToRefundsConverter.convert(refundsRequest); diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/WithdrawalResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/WithdrawalResource.java index 2b871ec..561081f 100644 --- a/src/main/java/dev/vality/fraudbusters/api/resource/WithdrawalResource.java +++ b/src/main/java/dev/vality/fraudbusters/api/resource/WithdrawalResource.java @@ -12,7 +12,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.RestController; -import javax.validation.Valid; import java.util.List; @Slf4j @@ -25,7 +24,7 @@ public class WithdrawalResource implements WithdrawalsApi { @Override - public ResponseEntity insertWithdrawals(@Valid WithdrawalsRequest withdrawalsRequest) { + public ResponseEntity insertWithdrawals(WithdrawalsRequest withdrawalsRequest) { log.debug("-> insertWithdrawals request: {}", withdrawalsRequest); if (!CollectionUtils.isEmpty(withdrawalsRequest.getWithdrawals())) { List withdrawals = withdrawalsRequestToWithdrawalsConverter.convert(withdrawalsRequest); diff --git a/src/main/java/dev/vality/fraudbusters/api/service/FraudbustersInspectorService.java b/src/main/java/dev/vality/fraudbusters/api/service/FraudbustersInspectorService.java index 1592ee0..5ee7394 100644 --- a/src/main/java/dev/vality/fraudbusters/api/service/FraudbustersInspectorService.java +++ b/src/main/java/dev/vality/fraudbusters/api/service/FraudbustersInspectorService.java @@ -1,12 +1,14 @@ package dev.vality.fraudbusters.api.service; import dev.vality.damsel.domain.RiskScore; -import dev.vality.damsel.proxy_inspector.Context; -import dev.vality.damsel.proxy_inspector.InspectorProxySrv; +import dev.vality.damsel.proxy_inspector.*; import dev.vality.fraudbusters.api.exceptions.RemoteInvocationException; +import dev.vality.swag.fraudbusters.model.Merchant; import dev.vality.swag.fraudbusters.model.RiskScoreResult; +import dev.vality.swag.fraudbusters.model.UserInspectResult; import lombok.RequiredArgsConstructor; import org.apache.thrift.TException; +import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Service; @Service @@ -14,6 +16,7 @@ public class FraudbustersInspectorService { private final InspectorProxySrv.Iface proxyInspectorSrv; + private final Converter shopContextToMerchantConverter; public dev.vality.swag.fraudbusters.model.RiskScore inspectPayment(Context context) { try { @@ -25,4 +28,20 @@ public dev.vality.swag.fraudbusters.model.RiskScore inspectPayment(Context conte throw new RemoteInvocationException(e); } } + + public UserInspectResult inspectUser(InspectUserContext context) { + try { + BlockedShops blockedShops = proxyInspectorSrv.inspectUser(context); + UserInspectResult result = new UserInspectResult(); + if (blockedShops != null && blockedShops.getShopList() != null) { + result.setBlockedMerchants(blockedShops.getShopList().stream() + .map(shopContextToMerchantConverter::convert) + .toList()); + } + return result; + } catch (TException e) { + throw new RemoteInvocationException(e); + } + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cbdc9b9..f9e6fa5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,29 +1,24 @@ server: - port: '@server.port@' + port: ${server.port} management: - security: - flag: false server: - port: '@management.port@' - metrics: - export: - statsd: - flavor: etsy - enabled: false - prometheus: - enabled: false + port: ${management.port} endpoint: health: show-details: always metrics: - enabled: true + access: unrestricted prometheus: - enabled: true + access: unrestricted endpoints: web: exposure: include: health,info,prometheus + prometheus: + metrics: + export: + enabled: false spring: application: @@ -31,6 +26,15 @@ spring: output: ansi: enabled: always + security: + oauth2: + resourceserver: + url: https://auth.domain + jwt: + realm: internal + issuer-uri: > + ${spring.security.oauth2.resourceserver.url}/auth/realms/ + ${spring.security.oauth2.resourceserver.jwt.realm} info: version: '@project.version@' stage: dev diff --git a/src/test/java/dev/vality/fraudbusters/api/auth/JwtTokenTestConfiguration.java b/src/test/java/dev/vality/fraudbusters/api/auth/JwtTokenTestConfiguration.java new file mode 100644 index 0000000..da1073f --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/auth/JwtTokenTestConfiguration.java @@ -0,0 +1,25 @@ +package dev.vality.fraudbusters.api.auth; + +import dev.vality.fraudbusters.api.auth.utils.JwtTokenBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +@Configuration +public class JwtTokenTestConfiguration { + + @Bean + public JwtTokenBuilder jwtTokenBuilder(KeyPair keyPair) { + return new JwtTokenBuilder(keyPair); + } + + @Bean + public KeyPair keyPair() throws GeneralSecurityException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); + keyGen.initialize(2048); + return keyGen.generateKeyPair(); + } +} diff --git a/src/test/java/dev/vality/fraudbusters/api/auth/KeycloakOpenIdTestConfiguration.java b/src/test/java/dev/vality/fraudbusters/api/auth/KeycloakOpenIdTestConfiguration.java new file mode 100644 index 0000000..59be5c4 --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/auth/KeycloakOpenIdTestConfiguration.java @@ -0,0 +1,22 @@ +package dev.vality.fraudbusters.api.auth; + + +import dev.vality.fraudbusters.api.auth.utils.JwtTokenBuilder; +import dev.vality.fraudbusters.api.auth.utils.KeycloakOpenIdStub; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KeycloakOpenIdTestConfiguration { + + @Bean + @SneakyThrows + public KeycloakOpenIdStub keycloakOpenIdStub( + @Value("${spring.security.oauth2.resourceserver.url}") String keycloakAuthServerUrl, + @Value("${spring.security.oauth2.resourceserver.jwt.realm}") String keycloakRealm, + JwtTokenBuilder jwtTokenBuilder) { + return new KeycloakOpenIdStub(keycloakAuthServerUrl + "/auth", keycloakRealm, jwtTokenBuilder); + } +} diff --git a/src/test/java/dev/vality/fraudbusters/api/auth/utils/JwtTokenBuilder.java b/src/test/java/dev/vality/fraudbusters/api/auth/utils/JwtTokenBuilder.java new file mode 100644 index 0000000..e0a75b1 --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/auth/utils/JwtTokenBuilder.java @@ -0,0 +1,91 @@ +package dev.vality.fraudbusters.api.auth.utils; + +import io.jsonwebtoken.Jwts; +import lombok.SneakyThrows; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.time.Instant; +import java.util.UUID; + +public class JwtTokenBuilder { + + public static final String DEFAULT_USERNAME = "Darth Vader"; + + public static final String DEFAULT_EMAIL = "darkside-the-best@mail.com"; + + private final String userId; + + private final String username; + + private final String email; + + private final PrivateKey privateKey; + + private final PublicKey publicKey; + + public JwtTokenBuilder(KeyPair keyPair) { + this(UUID.randomUUID().toString(), DEFAULT_USERNAME, DEFAULT_EMAIL, keyPair.getPrivate(), keyPair.getPublic()); + } + + public JwtTokenBuilder(String userId, String username, String email, PrivateKey privateKey, PublicKey publicKey) { + this.userId = userId; + this.username = username; + this.email = email; + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + public String generateJwtWithRoles(String issuer, String... roles) { + long iat = Instant.now().getEpochSecond(); + long exp = iat + 60 * 10; + return generateJwtWithRoles(iat, exp, issuer, roles); + } + + public String generateJwtWithRoles(long iat, long exp, String issuer, String... roles) { + return generateJwtWithRoles(privateKey, iat, exp, issuer, roles); + } + + public String generateJwtWithRoles(PrivateKey privateKey, long iat, long exp, String issuer, String... roles) { + String payload; + try { + payload = new JSONObject() + .put("jti", UUID.randomUUID().toString()) + .put("exp", exp) + .put("nbf", 0L) + .put("iat", iat) + .put("iss", issuer) + .put("aud", "private-api") + .put("sub", userId) + .put("typ", "Bearer") + .put("azp", "private-api") + .put("resource_access", new JSONObject() + .put("common-api", new JSONObject() + .put("roles", new JSONArray(roles)))) + .put("preferred_username", username) + .put("email", email).toString(); + } catch (JSONException e) { + throw new RuntimeException(e); + } + + return Jwts.builder() + .content(payload) + .signWith(privateKey, Jwts.SIG.RS256) + .compact(); + } + + @SneakyThrows + public PublicKey getPublicKey() { + return this.publicKey; + } + + @SneakyThrows + public PrivateKey getPrivateKey() { + return this.privateKey; + } + +} diff --git a/src/test/java/dev/vality/fraudbusters/api/auth/utils/KeycloakOpenIdStub.java b/src/test/java/dev/vality/fraudbusters/api/auth/utils/KeycloakOpenIdStub.java new file mode 100644 index 0000000..5f1bf8c --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/auth/utils/KeycloakOpenIdStub.java @@ -0,0 +1,94 @@ +package dev.vality.fraudbusters.api.auth.utils; + +import dev.vality.fraudbusters.api.testutil.GenerateSelfSigned; +import dev.vality.fraudbusters.api.testutil.PublicKeyUtil; +import lombok.SneakyThrows; + +import java.security.KeyPair; +import java.util.Base64; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; + +public class KeycloakOpenIdStub { + + private final String keycloakRealm; + private final String issuer; + private final String openidConfig; + private final String jwkConfig; + private final JwtTokenBuilder jwtTokenBuilder; + + @SneakyThrows + public KeycloakOpenIdStub(String keycloakAuthServerUrl, String keycloakRealm, JwtTokenBuilder jwtTokenBuilder) { + this.keycloakRealm = keycloakRealm; + this.jwtTokenBuilder = jwtTokenBuilder; + this.issuer = keycloakAuthServerUrl + "/realms/" + keycloakRealm; + this.openidConfig = "{\n" + + " \"issuer\": \"" + issuer + "\",\n" + + " \"authorization_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/auth\",\n" + + " \"token_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/token\",\n" + + " \"token_introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + + keycloakRealm + + "/protocol/openid-connect/token/introspect\",\n" + + " \"userinfo_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/userinfo\",\n" + + " \"end_session_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/logout\",\n" + + " \"jwks_uri\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/certs\",\n" + + " \"check_session_iframe\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/login-status-iframe.html\",\n" + + " \"registration_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/clients-registrations/openid-connect\",\n" + + " \"introspection_endpoint\": \"" + keycloakAuthServerUrl + "/realms/" + keycloakRealm + + "/protocol/openid-connect/token/introspect\"\n" + + "}"; + this.jwkConfig = """ + { + "keys": [ + { + "alg": "RS256", + "e": "%s", + "kid": "BZdHlAdlt3F1XatlYtZg3f1Cfpk5IpEINuIgviUW59s", + "kty": "RSA", + "n": "%s", + "use": "sig", + "x5c": [ + "%s" + ], + "x5t": "9APiqOME1mVmyv8hak6HB_PTezA", + "x5t#S256": "kweH93DnMHKD_NrAZF-mgpAM3Njv_8-oxaDAzki4t48" + } + ] + } + """.formatted( + PublicKeyUtil.getExponent(jwtTokenBuilder.getPublicKey()), + PublicKeyUtil.getModulus(jwtTokenBuilder.getPublicKey()), + Base64.getEncoder().encodeToString( + GenerateSelfSigned.generateCertificate(new KeyPair(jwtTokenBuilder.getPublicKey(), + jwtTokenBuilder.getPrivateKey())).getEncoded())); + } + + public void givenStub() { + stubFor(get(urlEqualTo(String.format("/auth/realms/%s/.well-known/openid-configuration", keycloakRealm))) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(openidConfig) + ) + ); + stubFor(get(urlEqualTo(String.format("/auth/realms/%s/protocol/openid-connect/certs", keycloakRealm))) + .willReturn(aResponse() + .withHeader("Content-Type", "application/json") + .withBody(jwkConfig) + )); + } + + public String generateJwt(String... roles) { + return jwtTokenBuilder.generateJwtWithRoles(issuer, roles); + } + + public String generateJwt(long iat, long exp, String... roles) { + return jwtTokenBuilder.generateJwtWithRoles(iat, exp, issuer, roles); + } +} diff --git a/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java b/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java new file mode 100644 index 0000000..452a35f --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java @@ -0,0 +1,37 @@ +package dev.vality.fraudbusters.api.config; + +import dev.vality.fraudbusters.api.FraudbustersApiApplication; +import dev.vality.fraudbusters.api.auth.utils.KeycloakOpenIdStub; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.wiremock.spring.EnableWireMock; + +@SuppressWarnings("LineLength") +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + classes = {FraudbustersApiApplication.class}, + properties = { + "spring.security.oauth2.resourceserver.url=${wiremock.server.baseUrl}", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=${wiremock.server.baseUrl}/auth/realms/" + + "${spring.security.oauth2.resourceserver.jwt.realm}"}) +@AutoConfigureMockMvc +@EnableWireMock +@ExtendWith(SpringExtension.class) +public abstract class AbstractKeycloakOpenIdAsWiremockConfig { + + @Autowired + private KeycloakOpenIdStub keycloakOpenIdStub; + + @BeforeEach + public void setUp(@Autowired KeycloakOpenIdStub keycloakOpenIdStub) throws Exception { + keycloakOpenIdStub.givenStub(); + } + + protected String generateSimpleJwt() { + return keycloakOpenIdStub.generateJwt(); + } +} diff --git a/src/test/java/dev/vality/fraudbusters/api/resource/ResourceTest.java b/src/test/java/dev/vality/fraudbusters/api/resource/ResourceTest.java index 5a9d5b5..25cc411 100644 --- a/src/test/java/dev/vality/fraudbusters/api/resource/ResourceTest.java +++ b/src/test/java/dev/vality/fraudbusters/api/resource/ResourceTest.java @@ -2,25 +2,32 @@ import dev.vality.damsel.fraudbusters.PaymentServiceSrv; import dev.vality.damsel.proxy_inspector.InspectorProxySrv; +import dev.vality.fraudbusters.api.config.AbstractKeycloakOpenIdAsWiremockConfig; import dev.vality.fraudbusters.api.service.FraudbustersDataService; import dev.vality.fraudbusters.api.service.FraudbustersInspectorService; import dev.vality.fraudbusters.api.utils.ApiBeanGenerator; import dev.vality.swag.fraudbusters.model.*; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Bean; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; +import java.time.Instant; import java.util.List; -import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class ResourceTest { +class ResourceTest extends AbstractKeycloakOpenIdAsWiremockConfig { public static final String CHARGEBACKS = "/chargebacks"; public static final String REFUNDS = "/refunds"; @@ -28,22 +35,31 @@ class ResourceTest { public static final String FRAUD_PAYMENTS = "/fraud-payments"; public static final String WITHDRAWALS = "/withdrawals"; public static final String INSPECTOR = "/inspect-payment"; + public static final String INSPECT_USER = "/inspect-user"; public static final String BASE_URL = "http://localhost:"; @LocalServerPort int serverPort; - @MockBean + @MockitoBean FraudbustersDataService fraudbustersDataService; - @MockBean + @MockitoBean FraudbustersInspectorService fraudbustersInspectorService; - @MockBean + @MockitoBean PaymentServiceSrv.Iface paymentServiceSrv; - @MockBean + @MockitoBean InspectorProxySrv.Iface proxyInspectorSrv; RestTemplate restTemplate = new RestTemplate(); + @BeforeEach + void setUp() { + restTemplate.getInterceptors().add((request, body, execution) -> { + request.getHeaders().setBearerAuth(generateSimpleJwt()); + return execution.execute(request, body); + }); + } + @Test void insertChargebacksTest() { ChargebacksRequest request = new ChargebacksRequest(); @@ -145,8 +161,36 @@ void inspectPaymentsTest() { verify(fraudbustersInspectorService, times(1)).inspectPayment(any()); } + @Test + void inspectUserTest() { + when(fraudbustersInspectorService.inspectUser(any())).thenReturn(new UserInspectResult()); + + UserInspectRequest request = new UserInspectRequest(); + assertThrows(HttpClientErrorException.BadRequest.class, () -> + restTemplate.postForEntity(initUrl(INSPECT_USER), request, UserInspectResult.class)); + + request.user(ApiBeanGenerator.initCustomer()); + request.merchants(List.of(ApiBeanGenerator.initMerchant())); + restTemplate.postForEntity(initUrl(INSPECT_USER), request, UserInspectResult.class); + verify(fraudbustersInspectorService, times(1)).inspectUser(any()); + } + private String initUrl(String fraudbustersRefunds) { return BASE_URL + serverPort + fraudbustersRefunds; } + @TestConfiguration + static class JwtTestConfig { + + @Bean + JwtDecoder jwtDecoder() { + return token -> Jwt.withTokenValue(token) + .header("alg", "none") + .claim("sub", "test") + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + } + } + } diff --git a/src/test/java/dev/vality/fraudbusters/api/testutil/GenerateSelfSigned.java b/src/test/java/dev/vality/fraudbusters/api/testutil/GenerateSelfSigned.java new file mode 100644 index 0000000..8bf217c --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/testutil/GenerateSelfSigned.java @@ -0,0 +1,37 @@ +package dev.vality.fraudbusters.api.testutil; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.Date; + +public class GenerateSelfSigned { + + public static X509Certificate generateCertificate(KeyPair keyPair) throws CertificateException, + OperatorCreationException { + X500Name x500Name = new X500Name("CN=***.com, OU=Security&Defense, O=*** Crypto., L=Ottawa, ST=Ontario, C=CA"); + SubjectPublicKeyInfo pubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()); + final Date start = new Date(); + final Date until = Date.from(LocalDate.now().plusDays(365).atStartOfDay().toInstant(ZoneOffset.UTC)); + final X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(x500Name, + new BigInteger(10, new SecureRandom()), start, until, x500Name, pubKeyInfo + ); + ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256WithRSA").build(keyPair.getPrivate()); + return new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()) + .getCertificate(certificateBuilder.build(contentSigner)); + } +} \ No newline at end of file diff --git a/src/test/java/dev/vality/fraudbusters/api/testutil/PublicKeyUtil.java b/src/test/java/dev/vality/fraudbusters/api/testutil/PublicKeyUtil.java new file mode 100644 index 0000000..44e0537 --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/testutil/PublicKeyUtil.java @@ -0,0 +1,25 @@ +package dev.vality.fraudbusters.api.testutil; + +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; + +import java.math.BigInteger; +import java.security.PublicKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Base64; + +@UtilityClass +public class PublicKeyUtil { + + @SneakyThrows + public String getModulus(PublicKey publicKey) { + BigInteger publicKeyModulus = ((RSAPublicKey) (publicKey)).getModulus(); + return Base64.getUrlEncoder().encodeToString(publicKeyModulus.toByteArray()); + } + + @SneakyThrows + public String getExponent(PublicKey publicKey) { + BigInteger publicKeyExponent = ((RSAPublicKey) (publicKey)).getPublicExponent(); + return Base64.getUrlEncoder().encodeToString(publicKeyExponent.toByteArray()); + } +} diff --git a/src/test/java/dev/vality/fraudbusters/api/testutil/RandomUtil.java b/src/test/java/dev/vality/fraudbusters/api/testutil/RandomUtil.java new file mode 100644 index 0000000..071d444 --- /dev/null +++ b/src/test/java/dev/vality/fraudbusters/api/testutil/RandomUtil.java @@ -0,0 +1,37 @@ +package dev.vality.fraudbusters.api.testutil; + +import lombok.experimental.UtilityClass; + +import java.nio.charset.StandardCharsets; +import java.util.Random; + +import static java.util.UUID.randomUUID; + +@UtilityClass +public class RandomUtil { + + private static final Random random = new Random(); + + public static int randomInt(int from, int to) { + return random.nextInt(to - from) + from; + } + + public static String randomIntegerAsString(int from, int to) { + return String.valueOf(random.nextInt(to - from) + from); + } + + public static String randomString(int length) { + return new String(randomBytes(length), StandardCharsets.UTF_8); + } + + public static byte[] randomBytes(int length) { + byte[] array = new byte[length]; + new Random().nextBytes(array); + return array; + } + + public static String randomRequestId() { + return randomUUID().toString().substring(0, 32); + } + +} diff --git a/src/test/java/dev/vality/fraudbusters/api/utils/ApiBeanGenerator.java b/src/test/java/dev/vality/fraudbusters/api/utils/ApiBeanGenerator.java index 20f68fa..4e5dbdb 100644 --- a/src/test/java/dev/vality/fraudbusters/api/utils/ApiBeanGenerator.java +++ b/src/test/java/dev/vality/fraudbusters/api/utils/ApiBeanGenerator.java @@ -1,7 +1,7 @@ package dev.vality.fraudbusters.api.utils; -import dev.vality.swag.fraudbusters.model.Error; import dev.vality.swag.fraudbusters.model.*; +import dev.vality.swag.fraudbusters.model.Error; import java.time.OffsetDateTime; import java.util.UUID; From 95d2bf5c21dc648ba3a3d7422418f0f4775ba155 Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Wed, 21 Jan 2026 20:52:50 +0700 Subject: [PATCH 2/7] Add security and new method --- .../api/configuration/SecurityConfig.java | 52 ++++++++++++++ .../ShopContextToMerchantConverter.java | 29 ++++++++ ...tRequestToInspectUserContextConverter.java | 72 +++++++++++++++++++ .../api/resource/UserInspectorResource.java | 32 +++++++++ 4 files changed, 185 insertions(+) create mode 100644 src/main/java/dev/vality/fraudbusters/api/configuration/SecurityConfig.java create mode 100644 src/main/java/dev/vality/fraudbusters/api/converter/ShopContextToMerchantConverter.java create mode 100644 src/main/java/dev/vality/fraudbusters/api/converter/UserInspectRequestToInspectUserContextConverter.java create mode 100644 src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java diff --git a/src/main/java/dev/vality/fraudbusters/api/configuration/SecurityConfig.java b/src/main/java/dev/vality/fraudbusters/api/configuration/SecurityConfig.java new file mode 100644 index 0000000..33a5955 --- /dev/null +++ b/src/main/java/dev/vality/fraudbusters/api/configuration/SecurityConfig.java @@ -0,0 +1,52 @@ +package dev.vality.fraudbusters.api.configuration; + +import dev.vality.fraudbusters.api.configuration.converter.JwtAuthConverter; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@ConditionalOnProperty(value = "auth.enabled", havingValue = "true") +public class SecurityConfig { + + private final JwtAuthConverter jwtAuthConverter; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable); + http.authorizeHttpRequests( + (authorize) -> authorize + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers(HttpMethod.GET, "/**/health/liveness").permitAll() + .requestMatchers(HttpMethod.GET, "/**/health/readiness").permitAll() + .requestMatchers(HttpMethod.GET, "/**/actuator/prometheus").permitAll() + .anyRequest().authenticated()); + http.oauth2ResourceServer(server -> server.jwt(token -> token.jwtAuthenticationConverter(jwtAuthConverter))); + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + http.cors(c -> c.configurationSource(corsConfigurationSource())); + return http.build(); + } + + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.applyPermitDefaultValues(); + configuration.addAllowedMethod(HttpMethod.PUT); + configuration.addAllowedMethod(HttpMethod.DELETE); + configuration.addAllowedMethod(HttpMethod.OPTIONS); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/dev/vality/fraudbusters/api/converter/ShopContextToMerchantConverter.java b/src/main/java/dev/vality/fraudbusters/api/converter/ShopContextToMerchantConverter.java new file mode 100644 index 0000000..c2f4ca1 --- /dev/null +++ b/src/main/java/dev/vality/fraudbusters/api/converter/ShopContextToMerchantConverter.java @@ -0,0 +1,29 @@ +package dev.vality.fraudbusters.api.converter; + +import dev.vality.damsel.proxy_inspector.ShopContext; +import dev.vality.swag.fraudbusters.model.Merchant; +import dev.vality.swag.fraudbusters.model.Shop; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +@Component +public class ShopContextToMerchantConverter implements Converter { + + @Override + public Merchant convert(ShopContext shopContext) { + var shop = shopContext.getShop(); + Shop swaggerShop = new Shop(); + if (shop != null) { + swaggerShop + .id(shop.getShopRef() != null ? shop.getShopRef().getId() : null) + .name(shop.getName()) + .category(shop.getCategory() != null ? shop.getCategory().getName() : null) + .location(shop.getLocation() != null ? shop.getLocation().getUrl() : null); + } + var party = shopContext.getParty(); + return new Merchant() + .id(party != null && party.getPartyRef() != null ? party.getPartyRef().getId() : null) + .shop(swaggerShop); + } +} + diff --git a/src/main/java/dev/vality/fraudbusters/api/converter/UserInspectRequestToInspectUserContextConverter.java b/src/main/java/dev/vality/fraudbusters/api/converter/UserInspectRequestToInspectUserContextConverter.java new file mode 100644 index 0000000..96b87d1 --- /dev/null +++ b/src/main/java/dev/vality/fraudbusters/api/converter/UserInspectRequestToInspectUserContextConverter.java @@ -0,0 +1,72 @@ +package dev.vality.fraudbusters.api.converter; + +import dev.vality.damsel.domain.*; +import dev.vality.damsel.proxy_inspector.InspectUserContext; +import dev.vality.damsel.proxy_inspector.Party; +import dev.vality.damsel.proxy_inspector.ShopContext; +import dev.vality.swag.fraudbusters.model.Contact; +import dev.vality.swag.fraudbusters.model.Customer; +import dev.vality.swag.fraudbusters.model.Merchant; +import dev.vality.swag.fraudbusters.model.UserInspectRequest; +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +public class UserInspectRequestToInspectUserContextConverter + implements Converter { + + private static final String MOCK_UNUSED_DATA = "MOCK_UNUSED_DATA"; + + @Override + public InspectUserContext convert(UserInspectRequest request) { + ContactInfo userInfo = buildUserInfo(request.getUser()); + List shopContexts = Optional.ofNullable(request.getMerchants()) + .orElse(Collections.emptyList()) + .stream() + .map(this::buildShopContext) + .collect(Collectors.toList()); + return new InspectUserContext() + .setUserInfo(userInfo) + .setShopList(shopContexts); + } + + private ContactInfo buildUserInfo(Customer user) { + Contact contact = Optional.ofNullable(user) + .map(Customer::getContact) + .orElseGet(Contact::new); + return new ContactInfo() + .setEmail(contact.getEmail()) + .setPhoneNumber(contact.getPhone()); + } + + private ShopContext buildShopContext(Merchant merchant) { + Party party = buildParty(merchant); + dev.vality.damsel.proxy_inspector.Shop shop = buildShop(merchant); + return new ShopContext() + .setParty(party) + .setShop(shop); + } + + private Party buildParty(Merchant merchant) { + return new Party() + .setPartyRef(new PartyConfigRef() + .setId(merchant.getId())); + } + + private dev.vality.damsel.proxy_inspector.Shop buildShop(Merchant merchant) { + dev.vality.swag.fraudbusters.model.Shop shop = merchant.getShop(); + return new dev.vality.damsel.proxy_inspector.Shop() + .setShopRef(new ShopConfigRef() + .setId(shop.getId())) + .setName(shop.getName()) + .setCategory(new Category() + .setName(shop.getCategory()) + .setDescription(MOCK_UNUSED_DATA)) + .setLocation(ShopLocation.url(shop.getLocation())); + } +} diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java new file mode 100644 index 0000000..7ab60d1 --- /dev/null +++ b/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java @@ -0,0 +1,32 @@ +package dev.vality.fraudbusters.api.resource; + +import dev.vality.damsel.proxy_inspector.InspectUserContext; +import dev.vality.fraudbusters.api.service.FraudbustersInspectorService; +import dev.vality.swag.fraudbusters.api.InspectUserApi; +import dev.vality.swag.fraudbusters.model.UserInspectRequest; +import dev.vality.swag.fraudbusters.model.UserInspectResult; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.convert.converter.Converter; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class UserInspectorResource implements InspectUserApi { + + private final FraudbustersInspectorService fraudbustersInspectorService; + private final Converter userInspectRequestToInspectUserContextConverter; + + @Override + public ResponseEntity inspectUser( + @Validated UserInspectRequest userInspectRequest) { + log.debug("-> inspectUser request: {}", userInspectRequest); + InspectUserContext context = userInspectRequestToInspectUserContextConverter.convert(userInspectRequest); + UserInspectResult result = fraudbustersInspectorService.inspectUser(context); + log.debug("<- inspectUser result: {}", result); + return ResponseEntity.ok(result); + } +} From 292ce5d62d6d9cd3ce55e8835b0f0dca0a8d8675 Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Wed, 21 Jan 2026 20:54:10 +0700 Subject: [PATCH 3/7] Add security and new method --- .../fraudbusters/api/resource/UserInspectorResource.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java b/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java index 7ab60d1..3232477 100644 --- a/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java +++ b/src/main/java/dev/vality/fraudbusters/api/resource/UserInspectorResource.java @@ -21,8 +21,7 @@ public class UserInspectorResource implements InspectUserApi { private final Converter userInspectRequestToInspectUserContextConverter; @Override - public ResponseEntity inspectUser( - @Validated UserInspectRequest userInspectRequest) { + public ResponseEntity inspectUser(UserInspectRequest userInspectRequest) { log.debug("-> inspectUser request: {}", userInspectRequest); InspectUserContext context = userInspectRequestToInspectUserContextConverter.convert(userInspectRequest); UserInspectResult result = fraudbustersInspectorService.inspectUser(context); From 3f4b6df3e9d3177937d368f0d2afa4c94b7aabce Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Wed, 21 Jan 2026 21:04:02 +0700 Subject: [PATCH 4/7] Add security and new method --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a654702..dbb1a95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,4 +7,4 @@ on: jobs: build: - uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v1 + uses: valitydev/java-workflow/.github/workflows/maven-service-build.yml@v3 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9162503..5cbf745 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,7 +7,7 @@ on: jobs: build-and-deploy: - uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v1 + uses: valitydev/java-workflow/.github/workflows/maven-service-deploy.yml@v3 with: ignore-coverage: true secrets: From 9449cb4579847d3907a0de48a0e07f376539f20d Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Wed, 21 Jan 2026 21:08:20 +0700 Subject: [PATCH 5/7] Add security and new method --- .../api/config/AbstractKeycloakOpenIdAsWiremockConfig.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java b/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java index 452a35f..66d642e 100644 --- a/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java +++ b/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java @@ -15,9 +15,9 @@ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {FraudbustersApiApplication.class}, properties = { - "spring.security.oauth2.resourceserver.url=${wiremock.server.baseUrl}", - "spring.security.oauth2.resourceserver.jwt.issuer-uri=${wiremock.server.baseUrl}/auth/realms/" + - "${spring.security.oauth2.resourceserver.jwt.realm}"}) + "spring.security.oauth2.resourceserver.url=${wiremock.server.baseUrl}", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=${wiremock.server.baseUrl}/auth/realms/" + + "${spring.security.oauth2.resourceserver.jwt.realm}"}) @AutoConfigureMockMvc @EnableWireMock @ExtendWith(SpringExtension.class) From f961a0ed890a96fe89d9bc379967f0838fd4fe85 Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Wed, 21 Jan 2026 21:10:31 +0700 Subject: [PATCH 6/7] Add security and new method --- .../api/config/AbstractKeycloakOpenIdAsWiremockConfig.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java b/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java index 66d642e..4c829b4 100644 --- a/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java +++ b/src/test/java/dev/vality/fraudbusters/api/config/AbstractKeycloakOpenIdAsWiremockConfig.java @@ -15,8 +15,8 @@ webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = {FraudbustersApiApplication.class}, properties = { - "spring.security.oauth2.resourceserver.url=${wiremock.server.baseUrl}", - "spring.security.oauth2.resourceserver.jwt.issuer-uri=${wiremock.server.baseUrl}/auth/realms/" + + "spring.security.oauth2.resourceserver.url=${wiremock.server.baseUrl}", + "spring.security.oauth2.resourceserver.jwt.issuer-uri=${wiremock.server.baseUrl}/auth/realms/" + "${spring.security.oauth2.resourceserver.jwt.realm}"}) @AutoConfigureMockMvc @EnableWireMock From aafa1d32fec4df24f3674cc2488b6ea217bdc683 Mon Sep 17 00:00:00 2001 From: kostyastruga Date: Thu, 22 Jan 2026 13:55:37 +0700 Subject: [PATCH 7/7] Change log path to vality --- src/test/resources/logback-test.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 766a9a2..0b7fc8e 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -6,5 +6,5 @@ - +