initial commit

This commit is contained in:
allard
2025-11-23 18:58:51 +01:00
commit 376a944abc
1553 changed files with 314731 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
**/target
!.keep
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
## Local configuration files
/local/config/*
*.swo
*.swp
*.~

View File

@@ -0,0 +1,115 @@
## Building individual microservices
### User service
```
mvn -pl :user-service -am package
docker build -t user-service:1.0-SNAPSHOT user-service
```
### Transaction service
```
mvn -pl :transaction-service -am package
docker build -t transaction-service:1.0-SNAPSHOT transaction-service
```
## Configuration
### Secrets
```
kubectl create secret generic bank-db-secret --from-literal=DB_SERVERNAME=<host> --from-literal=DB_PORTNUMBER=<port> --from-literal=DB_DATABASENAME=ibmclouddb --from-literal=DB_USER=<user> --from-literal=DB_PASSWORD=<password>
kubectl create secret generic bank-oidc-secret --from-literal=OIDC_JWKENDPOINTURL=<oauthServerUrl>/publickeys --from-literal=OIDC_ISSUERIDENTIFIER=<issuer> --from-literal=OIDC_AUDIENCES=<audience>
```
## Curl commands
### Users
```
curl -X POST -H "Authorization: Bearer <access-token>" -H "Content-Type: application/json" -d "{\"consentGiven\": \"true\"}" -k https://localhost:9443/bank/v1/users
curl -X GET "Authorization: Bearer <access-token>" -k https://localhost:9443/bank/v1/users/self
curl -X PUT "Authorization: Bearer <access-token>" -H "Content-Type: application/json" -d "{\"consentGiven\": \"false\"}" -k https://localhost:9443/bank/v1/users/self
curl -X DELETE "Authorization: Bearer <access-token>" -k https://localhost:9443/bank/v1/users/self
```
### User Events
```
curl -X POST "Authorization: Bearer <access-token>" -H "Content-Type: application/json" -d "{\"eventId\": \"871859e4-9fca-4bcf-adb5-e7d063d0747e\"}" -k https://localhost:9443/bank/v1/userEvents
curl -X GET "Authorization: Bearer <access-token>" -k https://localhost:9443/bank/v1/userEvents/self
curl -X GET "Authorization: Bearer <access-token>" -k https://localhost:9443/bank/v1/userEvents/self/info
```
### Events
```
curl -X POST "Authorization: Bearer <access-token>" -H "Content-Type: application/json" -d "{\"eventName\": \"Event name\", \"pointValue\": 100}" -k https://localhost:9444/bank/v1/events
curl -X GET "Authorization: Bearer <access-token>" -k https://localhost:9444/bank/v1/events/{eventId}
curl -X PUT "Authorization: Bearer <access-token>" -H "Content-Type: application/json" -d "{\"eventName\": \"Event name\", \"pointValue\": 100}" -k https://localhost:9444/bank/v1/events/{eventId}
curl -X GET "Authorization: Bearer <access-token>" -k https://localhost:9444/bank/v1/events
curl -X GET "Authorization: Bearer <access-token>" -k "https://localhost:9444/bank/v1/events?id=&id=&id="
```
## Running the integration tests
### Set environment variables
Base URL where users and events services are deployed
```
export USERS_BASEURL=http://<host>:<port>
export EVENTS_BASEURL=http://<host>:<port>
```
Prefix for test user names and the password they should use. These users are created dynamically by the tests.
```
export TEST_USER_PREFIX=<testUsername>
export TEST_PASSWORD=<testUserPassword>
```
Admin user name and password. This user name must exist in App Id prior to running the test and must have the admin role.
```
export TEST_ADMIN_USER=<adminUsername>
export TEST_ADMIN_PASSWORD=<adminUserPassword>
```
App Id service URL. Change to correct URL for the region where your App Id instance is deployed.
```
export APPID_SERVICE_URL=https://us-south.appid.cloud.ibm.com
```
App Id tenant id, client id, and client password (secret)
```
export APPID_TENANTID=<tenant id>
export OIDC_CLIENTID=<client id>
export OIDC_CLIENTPASSWORD=<client secret>
export OIDC_ISSUERIDENTIFIER=%APPID_SERVICE_URL%/%APPID_TENANTID%
```
IAM API key (needed for authentication to App Id)
```
export IAM_APIKEY=<apikey>
export IAM_SERVICE_URL=https://iam.cloud.ibm.com/identity/token
```
### Run the tests
```
mvn -pl :integration-tests -am verify
```

View File

@@ -0,0 +1,34 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<!-- Open Liberty Features -->
<dependency>
<groupId>io.openliberty.features</groupId>
<artifactId>microProfile-3.0</artifactId>
<type>esa</type>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package com.ibm.codey.bank;
import javax.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
public class BaseResource {
@Inject
@Claim("sub")
private String subject;
@Inject
@Claim(standard = Claims.raw_token)
private String rawToken;
protected String getCallerSubject() {
return subject;
}
protected String getCallerCredentials() {
return "Bearer " + rawToken;
}
}

View File

@@ -0,0 +1,15 @@
package com.ibm.codey.bank.accounts.json;
import javax.json.bind.annotation.JsonbProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter @ToString
public class UserRegistration {
@JsonbProperty
private boolean consentGiven;
}

View File

@@ -0,0 +1,18 @@
package com.ibm.codey.bank.accounts.json;
import javax.json.bind.annotation.JsonbProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter @ToString
public class UserRegistrationInfo {
@JsonbProperty
private String userId;
@JsonbProperty
private boolean consentGiven;
}

View File

@@ -0,0 +1,23 @@
package com.ibm.codey.bank.catalog;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import javax.enterprise.context.Dependent;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@Dependent
@RegisterRestClient
public interface KnativeService {
@POST
@Path("process")
public CompletionStage<String> processTransaction(@QueryParam("transactionId") String transactionId, @QueryParam("category") String category, @QueryParam("amount") String amount);
}

View File

@@ -0,0 +1,30 @@
package com.ibm.codey.bank.catalog;
import java.util.List;
import java.util.Map;
import javax.enterprise.context.Dependent;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import com.ibm.codey.bank.accounts.json.UserRegistration;
import com.ibm.codey.bank.accounts.json.UserRegistrationInfo;
@Dependent
@RegisterRestClient
public interface UserService {
@GET
@Path("self")
@Produces(MediaType.APPLICATION_JSON)
public UserRegistrationInfo getUserConsent(@HeaderParam("Authorization") String authorizationHeader);
}

View File

@@ -0,0 +1,23 @@
package com.ibm.codey.bank.catalog.json;
import java.math.BigDecimal;
import javax.json.bind.annotation.JsonbProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter @ToString
public class CreateTransactionDefinition {
@JsonbProperty
private String transactionName;
@JsonbProperty
private String category;
@JsonbProperty
private BigDecimal amount;
}

View File

@@ -0,0 +1,17 @@
package com.ibm.codey.bank.catalog.json;
import java.math.BigDecimal;
import javax.json.bind.annotation.JsonbProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter @Setter @ToString
public class RewardTransactionDefinition {
@JsonbProperty
private BigDecimal pointsEarned;
}

View File

@@ -0,0 +1,66 @@
package com.ibm.codey.bank.interceptor;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.InvocationContext;
import javax.json.Json;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.Claim;
/*
* This interceptor is used with the JAXRS resource classes to log any exception and return a 500 status code to the client.
* This could have been accomplished with an ExceptionMapper as well but an interceptor lets us also log information about
* the failing method and input parameters.
*/
public class LoggingInterceptor {
private static final Logger log = Logger.getLogger(LoggingInterceptor.class.getName());
@Inject
@Claim("sub")
private String subject;
@AroundInvoke
public Object logInvocation(InvocationContext ctx) {
try {
Object result = ctx.proceed();
logRequestAndResult(ctx, result);
return result;
} catch(Throwable e) {
String clz = ctx.getMethod().getDeclaringClass().getName();
String method = ctx.getMethod().getName();
Object[] params = ctx.getParameters();
if (params != null && params.length > 0) {
log.log(Level.SEVERE, "***** Exception in " + clz + "." + method, params);
} else {
log.log(Level.SEVERE, "***** Exception in " + clz + "." + method);
}
e.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
private void logRequestAndResult(InvocationContext ctx, Object result) {
String methodName = ctx.getMethod().getName();
Object[] params = ctx.getParameters();
JsonObjectBuilder requestBuilder = Json.createObjectBuilder()
.add("subject", subject)
.add("action", methodName);
if (params != null && params.length > 0) {
requestBuilder.add("input", Arrays.toString(params));
}
if (result instanceof Response) {
Response response = (Response)result;
requestBuilder.add("statuscode", response.getStatus());
}
log.log(Level.INFO, "API REQUEST", requestBuilder.build());
}
}

View File

@@ -0,0 +1,38 @@
package com.ibm.codey.bank.interceptor;
import javax.annotation.Priority;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.Claim;
import com.ibm.codey.bank.interceptor.binding.RequiresAuthorization;
/*
* This interceptor is used with the JAXRS resource classes to enforce a client scope for authorization purposes.
*/
@RequiresAuthorization @Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class SecurityInterceptor {
@Inject
@Claim("scope")
private String scope;
@AroundInvoke
public Object checkScope(InvocationContext ctx) throws Exception {
String[] scopeList = scope.split(" ");
for(String hasScope : scopeList) {
if (hasScope.equals("admin")) {
Object result = ctx.proceed();
return result;
}
}
return Response.status(Response.Status.FORBIDDEN).entity("admin permission required").build();
}
}

View File

@@ -0,0 +1,18 @@
package com.ibm.codey.bank.interceptor.binding;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Target({TYPE, METHOD})
@Retention(RUNTIME)
public @interface RequiresAuthorization {
}

View File

@@ -0,0 +1,43 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>integration-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Plugin to run functional tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,175 @@
package it.com.ibm.codey.loyalty;
import java.lang.reflect.Type;
import java.util.Map;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.Response;
import static org.junit.Assert.fail;
import com.ibm.codey.loyalty.accounts.json.UserRegistration;
import it.com.ibm.codey.loyalty.util.TestSecurityHelper;
public class EndpointTestBase {
protected static String USERS_BASEURL;
protected static String EVENTS_BASEURL;
protected static String TEST_USER_PREFIX;
protected static String TEST_USER;
protected static String TEST_PASSWORD;
protected static String userAccessToken;
protected static String TEST_ADMIN_USER;
protected static String TEST_ADMIN_PASSWORD;
protected static String adminAccessToken;
protected static final String USERS_ENDPOINT = "/loyalty/v1/users";
protected static final String USERS_SELF_ENDPOINT = "/loyalty/v1/users/self";
protected static final String USER_EVENTS_ENDPOINT = "/loyalty/v1/userEvents";
protected static final String USER_EVENTS_SELF_ENDPOINT = "/loyalty/v1/userEvents/self";
protected static final String USER_EVENTS_SELF_INFO_ENDPOINT = "/loyalty/v1/userEvents/self/info";
protected static final String EVENTS_ENDPOINT = "/loyalty/v1/events";
protected static boolean CONSENT_GIVEN = true;
protected static boolean CONSENT_NOT_GIVEN = false;
static {
USERS_BASEURL = System.getenv("USERS_BASEURL");
EVENTS_BASEURL = System.getenv("EVENTS_BASEURL");
TEST_USER_PREFIX = System.getenv("TEST_USER_PREFIX");
TEST_PASSWORD = System.getenv("TEST_PASSWORD");
TEST_ADMIN_USER = System.getenv("TEST_ADMIN_USER");
TEST_ADMIN_PASSWORD = System.getenv("TEST_ADMIN_PASSWORD");
}
private Client client;
protected void setup() {
client = ClientBuilder.newClient();
TEST_USER = TEST_USER_PREFIX + (int) ((Math.random() * 999999) + 1);
}
protected void teardown() {
client.close();
}
protected <T> T get(String baseUrl, String endpoint, Map<String, Object> queryParams, String accessToken, Response.Status expectedStatusCode, Type returnType) {
String url = baseUrl + endpoint;
WebTarget target = client.target(url);
if (queryParams != null) {
for (String key: queryParams.keySet()) {
target = target.queryParam(key, queryParams.get(key));
}
}
MultivaluedHashMap<String,Object> headers = new MultivaluedHashMap<String,Object>();
if (accessToken != null) {
String authHeader = "Bearer " + accessToken;
headers.putSingle(HttpHeaders.AUTHORIZATION, authHeader);
}
try (Response response = target.request().headers(headers).get()) {
checkStatusCode(url, response, expectedStatusCode);
if (returnType == Void.class) {
return null;
}
String jsonString = response.readEntity(String.class);
if (returnType.equals(String.class)) {
return (T)jsonString;
}
Jsonb jsonb = JsonbBuilder.create();
return jsonb.fromJson(jsonString, returnType);
}
}
protected <T> T put(String baseUrl, String endpoint, Object body, String accessToken, Response.Status expectedStatusCode, Class<T> returnType) {
String url = baseUrl + endpoint;
Jsonb jsonb = JsonbBuilder.create();
String jsonBody = jsonb.toJson(body);
MultivaluedHashMap<String,Object> headers = new MultivaluedHashMap<String,Object>();
if (accessToken != null) {
String authHeader = "Bearer " + accessToken;
headers.putSingle(HttpHeaders.AUTHORIZATION, authHeader);
}
try (Response response = client.target(url).request().headers(headers).buildPut(Entity.json(jsonBody)).invoke()) {
checkStatusCode(url, response, expectedStatusCode);
if (returnType == Void.class) {
return null;
}
String jsonString = response.readEntity(String.class);
if (returnType.equals(String.class)) {
return (T)jsonString;
}
return jsonb.fromJson(jsonString, returnType);
}
}
protected <T> T post(String baseUrl, String endpoint, Object body, String accessToken, Response.Status expectedStatusCode, Class<T> returnType) {
String url = baseUrl + endpoint;
Jsonb jsonb = JsonbBuilder.create();
String jsonBody = jsonb.toJson(body);
MultivaluedHashMap<String,Object> headers = new MultivaluedHashMap<String,Object>();
if (accessToken != null) {
String authHeader = "Bearer " + accessToken;
headers.putSingle(HttpHeaders.AUTHORIZATION, authHeader);
}
try (Response response = client.target(url).request().headers(headers).buildPost(Entity.json(jsonBody)).invoke()) {
checkStatusCode(url, response, expectedStatusCode);
if (returnType == Void.class) {
return null;
}
String jsonString = response.readEntity(String.class);
if (returnType.equals(String.class)) {
return (T)jsonString;
}
return jsonb.fromJson(jsonString, returnType);
}
}
protected void delete(String baseUrl, String endpoint, String accessToken, Response.Status expectedStatusCode) {
String url = baseUrl + endpoint;
MultivaluedHashMap<String,Object> headers = new MultivaluedHashMap<String,Object>();
if (accessToken != null) {
String authHeader = "Bearer " + accessToken;
headers.putSingle(HttpHeaders.AUTHORIZATION, authHeader);
}
try (Response response = client.target(url).request().headers(headers).buildDelete().invoke()) {
checkStatusCode(url, response, expectedStatusCode);
}
}
protected void setupUser() {
// Create a user in the user registry.
TestSecurityHelper.createUser(TEST_USER, TEST_PASSWORD);
// Log the user in and obtain an access token for invoking the API.
userAccessToken = TestSecurityHelper.signOn(TEST_USER, TEST_PASSWORD);
// Create user registration
UserRegistration userRegistration = new UserRegistration();
userRegistration.setConsentGiven(CONSENT_GIVEN);
post(USERS_BASEURL, USERS_ENDPOINT, userRegistration, userAccessToken, Response.Status.NO_CONTENT, Void.class);
}
protected void removeUser() {
// Use DELETE to remove user registration.
delete(USERS_BASEURL, USERS_SELF_ENDPOINT, userAccessToken, Response.Status.NO_CONTENT);
}
private void checkStatusCode(String url, Response response, Response.Status expectedStatusCode) {
if (expectedStatusCode.getStatusCode() != response.getStatus()) {
fail("Unexpected response code " + response.getStatus() +
" (expected " + expectedStatusCode.getStatusCode() +
") from " + url + " Response=" + response.readEntity(String.class));
}
}
}

View File

@@ -0,0 +1,66 @@
package it.com.ibm.codey.loyalty.accounts;
import javax.ws.rs.core.Response;
import static org.junit.Assert.assertEquals;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.ibm.codey.loyalty.accounts.json.UserRegistration;
import it.com.ibm.codey.loyalty.EndpointTestBase;
import it.com.ibm.codey.loyalty.util.TestSecurityHelper;
public class UserEndpointTest extends EndpointTestBase {
@Before
public void setup() {
super.setup();
}
@After
public void teardown() {
super.teardown();
}
@Test
public void testUserRegistrationAndDeletion() {
try {
setupUser();
// Use GET to get the user registration.
UserRegistration checkUserRegistration = get(USERS_BASEURL, USERS_SELF_ENDPOINT, null, userAccessToken, Response.Status.OK, UserRegistration.class);
assertEquals("Consent flag is incorrect", CONSENT_GIVEN, checkUserRegistration.isConsentGiven());
} finally {
removeUser();
}
}
@Test
public void testUserRegistrationModificationAndDeletion() {
try {
setupUser();
// Use PUT to change the user registration.
UserRegistration userRegistration = new UserRegistration();
userRegistration.setConsentGiven(CONSENT_NOT_GIVEN);
put(USERS_BASEURL, USERS_SELF_ENDPOINT, userRegistration, userAccessToken, Response.Status.NO_CONTENT, Void.class);
// Use GET to get the user registration.
UserRegistration checkUserRegistration = get(USERS_BASEURL, USERS_SELF_ENDPOINT, null, userAccessToken, Response.Status.OK, UserRegistration.class);
assertEquals("Consent flag is incorrect", CONSENT_NOT_GIVEN, checkUserRegistration.isConsentGiven());
} finally {
removeUser();
}
}
@Test
public void testAuthenticationFailure() {
// Make calls without an authentication header and verify they are rejected.
UserRegistration userRegistration = new UserRegistration();
userRegistration.setConsentGiven(CONSENT_GIVEN);
post(USERS_BASEURL, USERS_ENDPOINT, userRegistration, null, Response.Status.UNAUTHORIZED, Void.class);
get(USERS_BASEURL, USERS_SELF_ENDPOINT, null, null, Response.Status.UNAUTHORIZED, Void.class);
put(USERS_BASEURL, USERS_SELF_ENDPOINT, userRegistration, null, Response.Status.UNAUTHORIZED, Void.class);
delete(USERS_BASEURL, USERS_SELF_ENDPOINT, null, Response.Status.UNAUTHORIZED);
}
}

View File

@@ -0,0 +1,177 @@
package it.com.ibm.codey.loyalty.accounts;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.core.Response;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.ibm.codey.loyalty.accounts.json.UserEventCheckIn;
import com.ibm.codey.loyalty.accounts.json.UserEventInfo;
import com.ibm.codey.loyalty.accounts.json.UserRegistration;
import com.ibm.codey.loyalty.catalog.json.EventDefinition;
import it.com.ibm.codey.loyalty.EndpointTestBase;
import it.com.ibm.codey.loyalty.util.TestSecurityHelper;
public class UserEventsEndpointTest extends EndpointTestBase {
private static String normalPointsEventId, doublePointsEventId;
private static final int NORMAL_POINTS = 10;
private static final int DOUBLE_POINTS = NORMAL_POINTS*2;
private static final String NORMAL_POINTS_EVENT_NAME = "test event normal points";
private static final String DOUBLE_POINTS_EVENT_NAME = "test event double points";
private static boolean eventsCreated = false;
@Before
public void setup() {
super.setup();
// Create events. These are reused for all tests.
// This isn't done in a BeforeClass method because it depends on the non-static post() method in the superclass.
if (!eventsCreated) {
adminAccessToken = TestSecurityHelper.signOn(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
normalPointsEventId = createEvent(NORMAL_POINTS_EVENT_NAME, NORMAL_POINTS);
doublePointsEventId = createEvent(DOUBLE_POINTS_EVENT_NAME, DOUBLE_POINTS);
eventsCreated = true;
}
}
@After
public void teardown() {
super.teardown();
}
@Test
public void testEventCheckin() {
try {
setupUser();
// Verify no events attended or points earned yet
UserEventInfo userEventInfo = get(USERS_BASEURL, USER_EVENTS_SELF_INFO_ENDPOINT, null, userAccessToken, Response.Status.OK, UserEventInfo.class);
assertEquals("initial event count is incorrect", 0, userEventInfo.getEventCount());
assertEquals("initial points earned is incorrect", 0, userEventInfo.getPointsEarned());
// Check in to first event
UserEventCheckIn checkIn1 = new UserEventCheckIn();
checkIn1.setEventId(normalPointsEventId);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, userAccessToken, Response.Status.NO_CONTENT, Void.class);
// Verify check in to first event
String[] eventIds = get(USERS_BASEURL, USER_EVENTS_SELF_ENDPOINT, null, userAccessToken, Response.Status.OK, String[].class);
assertEquals("GET returned incorrect number of events checked in", 1, eventIds.length);
assertEquals("Event id is incorrect", normalPointsEventId, eventIds[0]);
// Verify points earned
UserEventInfo userEventInfo2 = get(USERS_BASEURL, USER_EVENTS_SELF_INFO_ENDPOINT, null, userAccessToken, Response.Status.OK, UserEventInfo.class);
assertEquals("event count is incorrect", 1, userEventInfo2.getEventCount());
assertEquals("points earned is incorrect", NORMAL_POINTS, userEventInfo2.getPointsEarned());
// Check in to second event
UserEventCheckIn checkIn2 = new UserEventCheckIn();
checkIn2.setEventId(doublePointsEventId);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn2, userAccessToken, Response.Status.NO_CONTENT, Void.class);
// Verify check in to both events
String[] eventIds2 = get(USERS_BASEURL, USER_EVENTS_SELF_ENDPOINT, null, userAccessToken, Response.Status.OK, String[].class);
assertEquals("GET returned incorrect number of events checked in", 2, eventIds2.length);
if (eventIds2[0].equals(normalPointsEventId)) {
assertEquals("Event id [1] is incorrect", doublePointsEventId, eventIds2[1]);
} else {
assertEquals("Event id [0] is incorrect", doublePointsEventId, eventIds2[0]);
assertEquals("Event id [1] is incorrect", normalPointsEventId, eventIds2[1]);
}
// Verify points earned
UserEventInfo userEventInfo3 = get(USERS_BASEURL, USER_EVENTS_SELF_INFO_ENDPOINT, null, userAccessToken, Response.Status.OK, UserEventInfo.class);
assertEquals("event count is incorrect", 2, userEventInfo3.getEventCount());
assertEquals("points earned is incorrect", NORMAL_POINTS+DOUBLE_POINTS, userEventInfo3.getPointsEarned());
} finally {
removeUser();
}
}
@Test
public void testDuplicateEventCheckin() {
try {
setupUser();
// Check in to first event
UserEventCheckIn checkIn1 = new UserEventCheckIn();
checkIn1.setEventId(normalPointsEventId);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, userAccessToken, Response.Status.NO_CONTENT, Void.class);
// Check in to first event again
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, userAccessToken, Response.Status.BAD_REQUEST, Void.class);
} finally {
removeUser();
}
}
@Test
public void testWithNonConsentedUser() {
try {
setupUser();
// Use PUT to change user registration to withdraw consent
UserRegistration userRegistration = new UserRegistration();
userRegistration.setConsentGiven(CONSENT_NOT_GIVEN);
put(USERS_BASEURL, USERS_SELF_ENDPOINT, userRegistration, userAccessToken, Response.Status.NO_CONTENT, Void.class);
// Try to check into an event or get information
UserEventCheckIn checkIn1 = new UserEventCheckIn();
checkIn1.setEventId(normalPointsEventId);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, userAccessToken, Response.Status.CONFLICT, Void.class);
get(USERS_BASEURL, USER_EVENTS_SELF_ENDPOINT, null, userAccessToken, Response.Status.CONFLICT, Void.class);
get(USERS_BASEURL, USER_EVENTS_SELF_INFO_ENDPOINT, null, userAccessToken, Response.Status.CONFLICT, Void.class);
} finally {
removeUser();
}
}
@Test
public void testWithUnregisteredUser() {
setupUser();
removeUser();
// Try to check into an event or get information
UserEventCheckIn checkIn1 = new UserEventCheckIn();
checkIn1.setEventId(normalPointsEventId);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, userAccessToken, Response.Status.BAD_REQUEST, Void.class);
get(USERS_BASEURL, USER_EVENTS_SELF_ENDPOINT, null, userAccessToken, Response.Status.BAD_REQUEST, Void.class);
get(USERS_BASEURL, USER_EVENTS_SELF_INFO_ENDPOINT, null, userAccessToken, Response.Status.BAD_REQUEST, Void.class);
}
@Test
public void testAuthenticationFailure() {
// Make calls without an authentication header
UserEventCheckIn checkIn1 = new UserEventCheckIn();
checkIn1.setEventId(normalPointsEventId);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, null, Response.Status.UNAUTHORIZED, Void.class);
get(USERS_BASEURL, USER_EVENTS_SELF_ENDPOINT, null, null, Response.Status.UNAUTHORIZED, Void.class);
get(USERS_BASEURL, USER_EVENTS_SELF_INFO_ENDPOINT, null, null, Response.Status.UNAUTHORIZED, Void.class);
}
@Test
public void testBadEventId() {
String badEventId1 = "1";
String badEventId2 = "/deadbeef-0000-0000-0000-badbadbadbad";
UserEventCheckIn checkIn1 = new UserEventCheckIn();
checkIn1.setEventId(badEventId1);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn1, userAccessToken, Response.Status.BAD_REQUEST, Void.class);
UserEventCheckIn checkIn2 = new UserEventCheckIn();
checkIn2.setEventId(badEventId2);
post(USERS_BASEURL, USER_EVENTS_ENDPOINT, checkIn2, userAccessToken, Response.Status.BAD_REQUEST, Void.class);
}
private String createEvent(String eventName, int pointValue) {
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
String eventId = post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, adminAccessToken, Response.Status.CREATED, String.class);
return eventId;
}
}

View File

@@ -0,0 +1,175 @@
package it.com.ibm.codey.loyalty.catalog;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.RandomStringUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.ibm.codey.loyalty.catalog.json.EventDefinition;
import it.com.ibm.codey.loyalty.EndpointTestBase;
import it.com.ibm.codey.loyalty.util.TestSecurityHelper;
public class EventsEndpointTest extends EndpointTestBase {
private String eventName;
private int pointValue;
private String eventDescription;
private String eventLocation;
private OffsetDateTime startTime;
private OffsetDateTime endTime;
@Before
public void setup() {
super.setup();
// Set up a normal user to test methods which don't require admin.
setupUser();
// Set up an admin user.
adminAccessToken = TestSecurityHelper.signOn(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
// Set up event attributes.
String suffix = RandomStringUtils.randomAlphabetic(8);
eventName = "test event " + suffix;
eventDescription = "all about " + suffix;
eventLocation = "at " + suffix;
startTime = OffsetDateTime.now();
endTime = OffsetDateTime.now().plusHours(1);
pointValue = (int) ((Math.random() * 99) + 1);
}
@After
public void teardown() {
removeUser();
super.teardown();
}
@Test
public void testCreateEvent() {
// Use POST to create an event.
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
eventDefinition.setEventDescription(eventDescription);
eventDefinition.setEventLocation(eventLocation);
eventDefinition.setStartTime(startTime);
eventDefinition.setEndTime(endTime);
String eventId = post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, adminAccessToken, Response.Status.CREATED, String.class);
// Use GET to get the event. This method does not require admin.
EventDefinition checkEventDefinition = get(EVENTS_BASEURL, EVENTS_ENDPOINT + '/' + eventId, null, userAccessToken, Response.Status.OK, EventDefinition.class);
assertEquals("Event name is incorrect", eventName, checkEventDefinition.getEventName());
assertEquals("Point value is incorrect", pointValue, checkEventDefinition.getPointValue());
assertEquals("Event description is incorrect", eventDescription, checkEventDefinition.getEventDescription());
assertEquals("Event location is incorrect", eventLocation, checkEventDefinition.getEventLocation());
assertEquals("Event start time is incorrect", startTime.toInstant(), checkEventDefinition.getStartTime().toInstant()); // Use toInstant to normalize timezones
assertEquals("Event end time is incorrect", endTime.toInstant(), checkEventDefinition.getEndTime().toInstant());
}
@Test
public void testGetAllEvents() {
// Use POST to create an event. An admin user must do this.
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
eventDefinition.setEventDescription(eventDescription);
eventDefinition.setEventLocation(eventLocation);
eventDefinition.setStartTime(startTime);
eventDefinition.setEndTime(endTime);
String eventId = post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, adminAccessToken, Response.Status.CREATED, String.class);
// Use GET to get all events. This method does not require admin.
GenericType<Map<String, EventDefinition>> eventDefinitionMapType = new GenericType<Map<String, EventDefinition>>() {};
Map<String, EventDefinition> eventDefinitionsMap = get(EVENTS_BASEURL, EVENTS_ENDPOINT, null, userAccessToken, Response.Status.OK, eventDefinitionMapType.getType());
assertNotNull("GET did not return any events", eventDefinitionsMap);
EventDefinition checkEventDefinition = eventDefinitionsMap.get(eventId);
assertNotNull("GET did not return the event that was just created", checkEventDefinition);
assertEquals("Event name is incorrect", eventName, checkEventDefinition.getEventName());
assertEquals("Point value is incorrect", pointValue, checkEventDefinition.getPointValue());
assertEquals("Event description is incorrect", eventDescription, checkEventDefinition.getEventDescription());
assertEquals("Event location is incorrect", eventLocation, checkEventDefinition.getEventLocation());
assertEquals("Event start time is incorrect", startTime.toInstant(), checkEventDefinition.getStartTime().toInstant()); // Use toInstant to normalize timezones
assertEquals("Event end time is incorrect", endTime.toInstant(), checkEventDefinition.getEndTime().toInstant());
}
@Test
public void testSearchEvent() {
// Use POST to create an event. An admin user must do this.
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
eventDefinition.setEventDescription(eventDescription);
eventDefinition.setEventLocation(eventLocation);
eventDefinition.setStartTime(startTime);
eventDefinition.setEndTime(endTime);
String eventId = post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, adminAccessToken, Response.Status.CREATED, String.class);
// Use GET to search for this event. This method does not require admin.
Map<String,Object> queryParams = Collections.singletonMap("id", eventId);
GenericType<Map<String, EventDefinition>> eventDefinitionMapType = new GenericType<Map<String, EventDefinition>>() {};
Map<String, EventDefinition> eventDefinitionsMap = get(EVENTS_BASEURL, EVENTS_ENDPOINT, queryParams, userAccessToken, Response.Status.OK, eventDefinitionMapType.getType());
assertNotNull("GET did not return any events", eventDefinitionsMap);
EventDefinition checkEventDefinition = eventDefinitionsMap.get(eventId);
assertNotNull("GET did not return the event that was just created", checkEventDefinition);
assertEquals("Event name is incorrect", eventName, checkEventDefinition.getEventName());
assertEquals("Point value is incorrect", pointValue, checkEventDefinition.getPointValue());
assertEquals("Event description is incorrect", eventDescription, checkEventDefinition.getEventDescription());
assertEquals("Event location is incorrect", eventLocation, checkEventDefinition.getEventLocation());
assertEquals("Event start time is incorrect", startTime.toInstant(), checkEventDefinition.getStartTime().toInstant()); // Use toInstant to normalize timezones
assertEquals("Event end time is incorrect", endTime.toInstant(), checkEventDefinition.getEndTime().toInstant());
}
@Test
public void testCreateAndUpdateEvent() {
// Use POST to create an event. An admin user must do this.
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
eventDefinition.setEventDescription(eventDescription);
eventDefinition.setEventLocation(eventLocation);
eventDefinition.setStartTime(startTime);
eventDefinition.setEndTime(endTime);
String eventId = post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, adminAccessToken, Response.Status.CREATED, String.class);
// Use PUT to modify the event. An admin user must do this.
eventDefinition.setEventName(eventName + eventName);
eventDefinition.setPointValue(pointValue*2);
put(EVENTS_BASEURL, EVENTS_ENDPOINT + '/' + eventId, eventDefinition, adminAccessToken, Response.Status.NO_CONTENT, Void.class);
// Use GET to get the event. This method does not require admin.
EventDefinition checkEventDefinition = get(EVENTS_BASEURL, EVENTS_ENDPOINT + '/' + eventId, null, userAccessToken, Response.Status.OK, EventDefinition.class);
assertEquals("Event name is incorrect", eventDefinition.getEventName(), checkEventDefinition.getEventName());
assertEquals("Point value is incorrect", eventDefinition.getPointValue(), checkEventDefinition.getPointValue());
assertEquals("Event description is incorrect", eventDescription, checkEventDefinition.getEventDescription());
assertEquals("Event location is incorrect", eventLocation, checkEventDefinition.getEventLocation());
assertEquals("Event start time is incorrect", startTime.toInstant(), checkEventDefinition.getStartTime().toInstant()); // Use toInstant to normalize timezones
assertEquals("Event end time is incorrect", endTime.toInstant(), checkEventDefinition.getEndTime().toInstant());
}
@Test
public void testAuthenticationFailure() {
// Make calls without an authentication header.
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, null, Response.Status.UNAUTHORIZED, Void.class);
put(EVENTS_BASEURL, EVENTS_ENDPOINT + "/deadbeef-0000-0000-0000-badbadbadbad", eventDefinition, null, Response.Status.UNAUTHORIZED, Void.class);
get(EVENTS_BASEURL, EVENTS_ENDPOINT, null, null, Response.Status.UNAUTHORIZED, Void.class);
get(EVENTS_BASEURL, EVENTS_ENDPOINT + "/deadbeef-0000-0000-0000-badbadbadbad", null, null, Response.Status.UNAUTHORIZED, Void.class);
}
@Test
public void testAuthorizationFailure() {
// Normal users do not have access to POST or PUT.
EventDefinition eventDefinition = new EventDefinition();
eventDefinition.setEventName(eventName);
eventDefinition.setPointValue(pointValue);
post(EVENTS_BASEURL, EVENTS_ENDPOINT, eventDefinition, userAccessToken, Response.Status.FORBIDDEN, Void.class);
put(EVENTS_BASEURL, EVENTS_ENDPOINT + "/deadbeef-0000-0000-0000-badbadbadbad", eventDefinition, userAccessToken, Response.Status.FORBIDDEN, Void.class);
}
}

View File

@@ -0,0 +1,104 @@
package it.com.ibm.codey.loyalty.util;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.json.Json;
import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
public class TestSecurityHelper {
private static String APPID_SERVICE_URL;
private static String APPID_TENANTID;
private static String IAM_APIKEY;
private static String IAM_SERVICE_URL;
private static String OIDC_ISSUERIDENTIFIER;
private static String OIDC_CLIENTID;
private static String OIDC_CLIENTPASSWORD;
private static String iamAuthHeader;
private static String oidcAuthHeader;
static {
APPID_SERVICE_URL = System.getenv("APPID_SERVICE_URL");
APPID_TENANTID = System.getenv("APPID_TENANTID");
IAM_APIKEY = System.getenv("IAM_APIKEY");
IAM_SERVICE_URL = System.getenv("IAM_SERVICE_URL");
OIDC_ISSUERIDENTIFIER = System.getenv("OIDC_ISSUERIDENTIFIER");
OIDC_CLIENTID = System.getenv("OIDC_CLIENTID");
OIDC_CLIENTPASSWORD = System.getenv("OIDC_CLIENTPASSWORD");
String oidcClientCredentials = OIDC_CLIENTID + ":" + OIDC_CLIENTPASSWORD;
oidcAuthHeader = "Basic " + Base64.getEncoder().encodeToString(oidcClientCredentials.getBytes(StandardCharsets.UTF_8));
}
public static void createUser(String user, String password) {
Client client = ClientBuilder.newClient();
client.register(JsrJsonpProvider.class);
// Get IAM bearer token when creating the first user. The token can be reused after that.
if (iamAuthHeader == null) {
Form form = new Form();
form.param("grant_type", "urn:ibm:params:oauth:grant-type:apikey");
form.param("apikey", IAM_APIKEY);
String iamToken;
try (Response response = client.target(IAM_SERVICE_URL).request(MediaType.APPLICATION_JSON).buildPost(Entity.form(form)).invoke()) {
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
throw new RuntimeException("TEST CASE FAILURE. Cannot obtain IAM access token. Status code " + response.getStatus() + " Response =" + response.readEntity(JsonObject.class));
}
JsonObject obj = response.readEntity(JsonObject.class);
iamToken = obj.getString("access_token");
}
iamAuthHeader = "Bearer " + iamToken;
}
// Create the user
JsonObject request = Json.createObjectBuilder()
.add("userName", user)
.add("password", password)
.add("active", true)
.add("emails", Json.createArrayBuilder()
.add(Json.createObjectBuilder()
.add("value", "ibmtestloyalty@yopmail.com")
.add("primary", true))
).build();
String createUserURL = APPID_SERVICE_URL + "/management/v4/" + APPID_TENANTID + "/cloud_directory/Users";
try (Response response = client.target(createUserURL).request(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, iamAuthHeader).buildPost(Entity.json(request)).invoke()) {
if (response.getStatus() != Response.Status.CREATED.getStatusCode()) {
throw new RuntimeException("TEST CASE FAILURE. Cannot create user. Status code " + response.getStatus() + " Response =" + response.readEntity(JsonObject.class));
}
}
}
public static String signOn(String user, String password) {
String url = OIDC_ISSUERIDENTIFIER + "/token";
Form form = new Form();
form.param("grant_type", "password");
form.param("username", user);
form.param("password", password);
Client client = ClientBuilder.newClient();
client.register(JsrJsonpProvider.class);
try (Response response = client.target(url).request(MediaType.APPLICATION_JSON).header(HttpHeaders.AUTHORIZATION, oidcAuthHeader).buildPost(Entity.form(form)).invoke()) {
if (response.getStatus() != Response.Status.OK.getStatusCode()) {
throw new RuntimeException("TEST CASE FAILURE. Cannot obtain access token. Status code " + response.getStatus() + " Response =" + response.readEntity(JsonObject.class));
}
JsonObject obj = response.readEntity(JsonObject.class);
return obj.getString("access_token");
}
}
}

View File

@@ -0,0 +1,216 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- Plugins -->
<version.maven-war-plugin>3.2.2</version.maven-war-plugin>
<version.maven-surefire-plugin>3.0.0-M1</version.maven-surefire-plugin>
<version.maven-failsafe-plugin>3.0.0-M1</version.maven-failsafe-plugin>
</properties>
<dependencies>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<!-- For tests -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-client</artifactId>
<version>3.2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-extension-providers</artifactId>
<version>3.2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.4</version>
<scope>test</scope>
</dependency>
<!-- Support for JDK 9 and above -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
<scope>test</scope>
</dependency>
<!-- JSON-B provider for integration tests -->
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>1.0.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.openliberty.features</groupId>
<artifactId>features-bom</artifactId>
<version>19.0.0.12</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>${version.maven-war-plugin}</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<packagingExcludes>pom.xml</packagingExcludes>
</configuration>
</plugin>
<!-- Plugin to run unit tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${version.maven-surefire-plugin}</version>
<executions>
<execution>
<phase>test</phase>
<id>default-test</id>
<configuration>
<excludes>
<exclude>**/it/**</exclude>
</excludes>
<reportsDirectory>
${project.build.directory}/test-reports/unit
</reportsDirectory>
</configuration>
</execution>
</executions>
<configuration>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
<!-- Plugin to run functional tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${version.maven-failsafe-plugin}</version>
<executions>
<execution>
<phase>integration-test</phase>
<id>integration-test</id>
<goals>
<goal>integration-test</goal>
</goals>
<configuration>
<includes>
<include>**/it/**</include>
</includes>
<systemPropertyVariables>
<liberty.test.port>${http.port}</liberty.test.port>
<war.name>${app.name}</war.name>
</systemPropertyVariables>
<trimStackTrace>false</trimStackTrace>
</configuration>
</execution>
<execution>
<id>verify-results</id>
<goals>
<goal>verify</goal>
</goals>
</execution>
</executions>
<configuration>
<summaryFile>
${project.build.directory}/test-reports/it/failsafe-summary.xml
</summaryFile>
<reportsDirectory>
${project.build.directory}/test-reports/it
</reportsDirectory>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>16</source>
<target>16</target>
<fork>true</fork>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<modules>
<module>common</module>
<module>transaction-service</module>
<module>user-service</module>
<module>integration-tests</module>
</modules>
</project>

View File

@@ -0,0 +1,11 @@
FROM open-liberty:19.0.0.12-kernel-java8-openj9
USER root
RUN apt-get update && apt-get upgrade -y e2fsprogs libgnutls30 libgcrypt20 libsasl2-2
USER 1001
COPY --chown=1001:0 src/main/liberty/config/ /config/
COPY --chown=1001:0 src/main/resources/security/ /config/resources/security/
COPY --chown=1001:0 target/*.war /config/apps/
COPY --chown=1001:0 target/jdbc/* /config/jdbc/
RUN configure.sh

View File

@@ -0,0 +1,60 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: transaction-service
labels:
app: transaction-service
spec:
replicas: 1
selector:
matchLabels:
app: transaction-service
template:
metadata:
labels:
app: transaction-service
annotations:
sidecar.istio.io/inject: "false"
spec:
containers:
- name: transaction-service
image: ykoyfman/bank-transaction-service:1.0
imagePullPolicy: Always
ports:
- name: http-server
containerPort: 9080
envFrom:
- secretRef:
name: bank-db-secret
- secretRef:
name: bank-oidc-secret
env:
- name: USER_SERVICE_URL
value: "http://user-service:9080/bank/v1/users"
- name: KNATIVE_SERVICE_URL
value: "http://process-transaction.example-bank.svc.cluster.local"
- name: WLP_LOGGING_CONSOLE_LOGLEVEL
value: INFO
---
apiVersion: v1
kind: Service
metadata:
name: transaction-service
labels:
app: transaction-service
spec:
ports:
- port: 9080
targetPort: 9080
selector:
app: transaction-service
---
apiVersion: v1
kind: Route
metadata:
name: transaction-service
spec:
to:
kind: Service
name: transaction-service

View File

@@ -0,0 +1,78 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>transaction-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- Open Liberty Features -->
<dependency>
<groupId>io.openliberty.features</groupId>
<artifactId>microProfile-3.0</artifactId>
<type>esa</type>
</dependency>
<dependency>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<type>jar</type>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
</plugin>
<!-- Add JDBC driver to package -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>copy-jdbc-driver</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.8</version>
<outputDirectory>${project.build.directory}/jdbc</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<!-- Plugin to run unit tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<!-- Plugin to run functional tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,25 @@
package com.ibm.codey.bank;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
@Liveness
@ApplicationScoped
public class LivenessCheck implements HealthCheck {
private boolean isAlive() {
// perform health checks here
return true;
}
@Override
public HealthCheckResponse call() {
boolean up = isAlive();
return HealthCheckResponse.named(this.getClass().getSimpleName()).state(up).build();
}
}

View File

@@ -0,0 +1,9 @@
package com.ibm.codey.bank;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/bank")
public class LoyaltyApplication extends Application {
}

View File

@@ -0,0 +1,25 @@
package com.ibm.codey.bank;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;
@Readiness
@ApplicationScoped
public class ReadinessCheck implements HealthCheck {
private boolean isReady() {
// perform readiness checks, e.g. database connection, etc.
return true;
}
@Override
public HealthCheckResponse call() {
boolean up = isReady();
return HealthCheckResponse.named(this.getClass().getSimpleName()).state(up).build();
}
}

View File

@@ -0,0 +1,189 @@
package com.ibm.codey.bank.catalog;
import java.net.URL;
import java.time.OffsetDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import com.ibm.codey.bank.BaseResource;
import com.ibm.codey.bank.accounts.json.UserRegistration;
import com.ibm.codey.bank.accounts.json.UserRegistrationInfo;
import com.ibm.codey.bank.catalog.dao.TransactionDao;
import com.ibm.codey.bank.catalog.json.CreateTransactionDefinition;
import com.ibm.codey.bank.catalog.json.RewardTransactionDefinition;
import com.ibm.codey.bank.catalog.models.Category;
import com.ibm.codey.bank.catalog.models.Transaction;
import com.ibm.codey.bank.interceptor.LoggingInterceptor;
import com.ibm.codey.bank.interceptor.binding.RequiresAuthorization;
@RequestScoped
@Interceptors(LoggingInterceptor.class)
@Path("v1/transactions")
public class TransactionResource extends BaseResource {
@Inject
private TransactionDao transactionDao;
@Inject
@ConfigProperty(name = "USER_SERVICE_URL")
private URL userServiceURL;
/**
* This method creates a transaction.
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
public Response createTransaction(CreateTransactionDefinition createTransactionDefinition) {
Transaction newTransaction = new Transaction();
// create new uuid for new transaction
String transactionId = UUID.randomUUID().toString();
// get subject
String subject = this.getCallerSubject();
// get user
UserService userService = RestClientBuilder.newBuilder().baseUrl(userServiceURL).build(UserService.class);
try {
UserRegistrationInfo userRegistration = userService.getUserConsent(this.getCallerCredentials());
if (!userRegistration.isConsentGiven()) {
return Response.status(Response.Status.CONFLICT).entity("User has not consented to program").build();
}
newTransaction.setTransactionId(transactionId);
newTransaction.setUserId(userRegistration.getUserId());
newTransaction.setTransactionName(createTransactionDefinition.getTransactionName());
newTransaction.setCategory(createTransactionDefinition.getCategory());
newTransaction.setAmount(createTransactionDefinition.getAmount());
newTransaction.setProcessed(false);
newTransaction.setDate(OffsetDateTime.now());
transactionDao.createTransaction(newTransaction);
return Response.status(Response.Status.NO_CONTENT).build();
} catch(WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
return Response.status(Response.Status.NOT_FOUND).entity("User not registered").build();
} else {
wae.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}
/**
* This method gets the transactions of a user.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public Response getTransactions() {
// get subject
String subject = this.getCallerSubject();
// get user
UserService userService = RestClientBuilder.newBuilder().baseUrl(userServiceURL).build(UserService.class);
try {
UserRegistrationInfo userRegistration = userService.getUserConsent(this.getCallerCredentials());
if (!userRegistration.isConsentGiven()) {
return Response.status(Response.Status.CONFLICT).entity("User has not consented to program").build();
}
List<Transaction> transactions = transactionDao.findTransactionsByUser(userRegistration.getUserId());
return Response.status(Response.Status.OK).entity(transactions).build();
} catch(WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
return Response.status(Response.Status.NOT_FOUND).entity("User not registered").build();
} else {
wae.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}
/**
* This method gets the spending categories of a user.
*/
@GET
@Path("spending")
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public Response getCategory() {
// get subject
String subject = this.getCallerSubject();
// get user
UserService userService = RestClientBuilder.newBuilder().baseUrl(userServiceURL).build(UserService.class);
try {
UserRegistrationInfo userRegistration = userService.getUserConsent(this.getCallerCredentials());
if (!userRegistration.isConsentGiven()) {
return Response.status(Response.Status.CONFLICT).entity("User has not consented to program").build();
}
List<Category> categories = transactionDao.groupCategoriesForUser(userRegistration.getUserId());
return Response.status(Response.Status.OK).entity(categories).build();
} catch(WebApplicationException wae) {
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
return Response.status(Response.Status.NOT_FOUND).entity("User not registered").build();
} else {
wae.printStackTrace();
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}
// TODO: require admin scope
/**
* This method updates a transaction.
*/
@PUT
@Path("reward/{transactionId}")
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
@RequiresAuthorization
public Response updateTransaction(@PathParam("transactionId") String transactionId, RewardTransactionDefinition rewardTransactionDefinition) {
// Validate UUID is formatted correctly.
try {
UUID.fromString(transactionId);
} catch(IllegalArgumentException iae) {
return Response.status(Response.Status.BAD_REQUEST).entity("Invalid transaction id").build();
}
Transaction transaction = transactionDao.findTransactionById(transactionId);
if (transaction == null) {
return Response.status(Response.Status.NOT_FOUND).entity("Transaction not found").build();
}
if (transaction.isProcessed()) {
return Response.status(Response.Status.BAD_REQUEST).entity("Transaction already processed").build();
}
transaction.setPointsEarned(rewardTransactionDefinition.getPointsEarned());
transaction.setProcessed(true);
transactionDao.updateTransaction(transaction);
return Response.status(Response.Status.NO_CONTENT).build();
}
}

View File

@@ -0,0 +1,79 @@
package com.ibm.codey.bank.catalog.dao;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import com.ibm.codey.bank.catalog.models.Category;
import com.ibm.codey.bank.catalog.models.Transaction;
@RequestScoped
public class TransactionDao {
@PersistenceContext(name = "jpa-unit")
private EntityManager em;
public void createTransaction(Transaction transaction) {
em.persist(transaction);
}
public void updateTransaction(Transaction transaction) {
em.merge(transaction);
}
public List<Transaction> findTransactions() {
return em.createNamedQuery("Transaction.findTransactions", Transaction.class)
.getResultList();
}
public List<Transaction> findTransactionsByUser(String userId) {
return em.createNamedQuery("Transaction.findTransactionsByUser", Transaction.class)
.setParameter("userId", userId)
.getResultList();
}
public Transaction findTransactionById(String transactionId) {
try {
return em.createNamedQuery("Transaction.findTransactionByIdOnly", Transaction.class)
.setParameter("transactionId", transactionId)
.getSingleResult();
} catch(NoResultException e) {
return null;
}
}
public Transaction findTransactionById(String transactionId, String userId) {
try {
return em.createNamedQuery("Transaction.findTransactionById", Transaction.class)
.setParameter("transactionId", transactionId)
.setParameter("userId", userId)
.getSingleResult();
} catch(NoResultException e) {
return null;
}
}
public List<Category> groupCategoriesForUser(String userId) {
try {
List<Object[][]> rows = em.createNamedQuery("Transaction.groupCategoriesForUser", Object[][].class)
.setParameter("userId", userId)
.getResultList();
List<Category> response = new ArrayList<>();
for (Object[] row: rows) {
if (row.length == 2) {
response.add(new Category(row[0].toString(), new BigDecimal(row[1].toString())));
}
}
return response;
} catch(NoResultException e) {
return null;
}
}
}

View File

@@ -0,0 +1,18 @@
package com.ibm.codey.bank.catalog.models;
import java.math.BigDecimal;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class Category {
private String category;
private BigDecimal amount;
public Category(String category, BigDecimal amount) {
this.category = category;
this.amount = amount;
}
}

View File

@@ -0,0 +1,64 @@
package com.ibm.codey.bank.catalog.models;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.OffsetDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "transactions")
@IdClass(TransactionPK.class)
@NamedQueries({
@NamedQuery(name = "Transaction.findTransactions", query = "SELECT t FROM Transaction t"),
@NamedQuery(name = "Transaction.findTransactionsByUser", query = "SELECT t FROM Transaction t WHERE t.userId = :userId"),
@NamedQuery(name = "Transaction.findTransactionById", query = "SELECT t FROM Transaction t WHERE t.transactionId = :transactionId AND t.userId = :userId"),
@NamedQuery(name = "Transaction.findTransactionByIdOnly", query = "SELECT t FROM Transaction t WHERE t.transactionId = :transactionId"),
@NamedQuery(name = "Transaction.groupCategoriesForUser", query = "SELECT COALESCE(t.category, 'Uncategorized'), SUM (t.amount) FROM Transaction t WHERE t.userId = :userId GROUP BY t.category")
})
@Getter @Setter
@EntityListeners(TransactionListener.class)
public class Transaction implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "transaction_id")
@Id
private String transactionId;
@Id
@Column(name = "usr")
private String userId;
@Column(name = "transaction_name")
private String transactionName;
@Column(name = "amount")
private BigDecimal amount;
@Column(name = "category")
private String category;
@Column(name = "points_earned")
private BigDecimal pointsEarned;
@Column(name = "processed")
private boolean processed;
@Column(name = "date")
private OffsetDateTime date;
public Transaction() {
}
}

View File

@@ -0,0 +1,40 @@
package com.ibm.codey.bank.catalog.models;
import java.net.URL;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.persistence.PostPersist;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
import com.ibm.codey.bank.catalog.KnativeService;
@RequestScoped
public class TransactionListener {
@Inject
@ConfigProperty(name = "KNATIVE_SERVICE_URL")
private URL knativeServiceURL;
@PostPersist
public void sendToProcessing(Transaction transaction) {
KnativeService knativeService = RestClientBuilder.newBuilder().baseUrl(knativeServiceURL).build(KnativeService.class);
try {
knativeService.processTransaction(transaction.getTransactionId(), transaction.getCategory(), transaction.getAmount().toString());
} catch (WebApplicationException wae) {
System.out.print("web app exception");
int status = wae.getResponse().getStatus();
if (status == Response.Status.NOT_FOUND.getStatusCode()) {
// TODO: ..
} else {
wae.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,15 @@
package com.ibm.codey.bank.catalog.models;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class TransactionPK implements Serializable {
private String transactionId;
private String userId;
}

View File

@@ -0,0 +1,2 @@
default.http.port=9080
default.https.port=9443

View File

@@ -0,0 +1,2 @@
# This option is needed when using an IBM JRE to avoid a handshake failure when making a secure JDBC connection.
-Dcom.ibm.jsse2.overrideDefaultTLS=true

View File

@@ -0,0 +1,53 @@
<server description="Liberty server">
<featureManager>
<feature>jpa-2.2</feature>
<feature>microProfile-3.0</feature>
<feature>mpJwt-1.1</feature>
</featureManager>
<logging traceSpecification="eclipselink=all" maxFileSize="20" maxFiles="10"/>
<keyStore id="digicertRootCA" password="digicert" location="${server.config.dir}/resources/security/digicert-root-ca.jks"/>
<ssl id="defaultSSLConfig" keyStoreRef="defaultKeyStore" trustStoreRef="digicertRootCA" />
<httpEndpoint host="*" httpPort="${default.http.port}"
httpsPort="${default.https.port}" id="defaultHttpEndpoint"/>
<mpJwt
id="jwt"
issuer="${OIDC_ISSUERIDENTIFIER}"
jwksUri="${OIDC_JWKENDPOINTURL}"
audiences="${OIDC_AUDIENCES}"
userNameAttribute="sub"
/>
<library id="PostgresLib">
<fileset dir="${server.config.dir}/jdbc"/>
</library>
<dataSource id="AccountsDataSource" jndiName="jdbc/AccountsDataSource">
<jdbcDriver libraryRef="PostgresLib" />
<!-- Idle connections to this server are timing out after 5 minutes.
It is recommended to set maxIdleTime to half of that value to avoid jdbc failures (e.g. broken pipe).
Reap time is reduced from default of 3 minutes to close idle connections in time. -->
<connectionManager maxIdleTime="2m30s" reapTime="60s"/>
<properties.postgresql
serverName="${DB_SERVERNAME}"
portNumber="${DB_PORTNUMBER}"
databaseName="${DB_DATABASENAME}"
user="${DB_USER}"
password="${DB_PASSWORD}"
ssl="false"
/>
</dataSource>
<webApplication location="transaction-service.war" contextRoot="/">
<application-bnd>
<security-role name="authenticated">
<special-subject type="ALL_AUTHENTICATED_USERS"/>
</security-role>
</application-bnd>
</webApplication>
</server>

View File

@@ -0,0 +1,10 @@
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd"
version="2.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<schema>bank</schema>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="jpa-unit" transaction-type="JTA">
<jta-data-source>jdbc/AccountsDataSource</jta-data-source>
<shared-cache-mode>NONE</shared-cache-mode>
<properties>
<property name="eclipselink.target-database" value="PostgreSQL"/>
<property name="eclipselink.logging.level" value="ALL"/>
<property name="eclipselink.logging.parameters" value="true"/>
</properties>
</persistence-unit>
</persistence>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>

View File

@@ -0,0 +1,27 @@
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>transaction-service</display-name>
<security-role>
<role-name>authenticated</role-name>
</security-role>
<security-constraint>
<display-name>Security Constraints</display-name>
<web-resource-collection>
<web-resource-name>ProtectedArea</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>authenticated</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>

View File

@@ -0,0 +1,11 @@
FROM open-liberty:19.0.0.12-kernel-java8-openj9
USER root
RUN apt-get update && apt-get upgrade -y e2fsprogs libgnutls30 libgcrypt20 libsasl2-2
USER 1001
COPY --chown=1001:0 src/main/liberty/config/ /config/
COPY --chown=1001:0 src/main/resources/security/ /config/resources/security/
COPY --chown=1001:0 target/*.war /config/apps/
COPY --chown=1001:0 target/jdbc/* /config/jdbc/
RUN configure.sh

View File

@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
labels:
app: user-service
spec:
replicas: 1
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: anthonyamanse/user-service:example-bank-1.0
imagePullPolicy: Always
ports:
- name: http-server
containerPort: 9080
envFrom:
- secretRef:
name: bank-db-secret
- secretRef:
name: bank-oidc-secret
---
apiVersion: v1
kind: Service
metadata:
name: user-service
labels:
app: user-service
spec:
ports:
- port: 9080
targetPort: 9080
selector:
app: user-service
---
apiVersion: v1
kind: Route
metadata:
name: user-service
spec:
to:
kind: Service
name: user-service

View File

@@ -0,0 +1,78 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>user-service</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<!-- Open Liberty Features -->
<dependency>
<groupId>io.openliberty.features</groupId>
<artifactId>microProfile-3.0</artifactId>
<type>esa</type>
</dependency>
<dependency>
<groupId>com.ibm.codey.bank</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<type>jar</type>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
</plugin>
<!-- Add JDBC driver to package -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>copy-jdbc-driver</id>
<phase>package</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.8</version>
<outputDirectory>${project.build.directory}/jdbc</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<!-- Plugin to run unit tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
<!-- Plugin to run functional tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,25 @@
package com.ibm.codey.bank;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
@Liveness
@ApplicationScoped
public class LivenessCheck implements HealthCheck {
private boolean isAlive() {
// perform health checks here
return true;
}
@Override
public HealthCheckResponse call() {
boolean up = isAlive();
return HealthCheckResponse.named(this.getClass().getSimpleName()).state(up).build();
}
}

View File

@@ -0,0 +1,9 @@
package com.ibm.codey.bank;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("/bank")
public class LoyaltyApplication extends Application {
}

View File

@@ -0,0 +1,25 @@
package com.ibm.codey.bank;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Readiness;
@Readiness
@ApplicationScoped
public class ReadinessCheck implements HealthCheck {
private boolean isReady() {
// perform readiness checks, e.g. database connection, etc.
return true;
}
@Override
public HealthCheckResponse call() {
boolean up = isReady();
return HealthCheckResponse.named(this.getClass().getSimpleName()).state(up).build();
}
}

View File

@@ -0,0 +1,120 @@
package com.ibm.codey.bank.accounts;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.interceptor.Interceptors;
import javax.transaction.Transactional;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.POST;
import javax.ws.rs.DELETE;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.ibm.codey.bank.BaseResource;
import com.ibm.codey.bank.accounts.dao.UserDao;
import com.ibm.codey.bank.accounts.json.UserRegistration;
import com.ibm.codey.bank.accounts.json.UserRegistrationInfo;
import com.ibm.codey.bank.accounts.models.User;
import com.ibm.codey.bank.interceptor.LoggingInterceptor;
@RequestScoped
@Interceptors(LoggingInterceptor.class)
@Path("v1/users")
public class UserResource extends BaseResource {
@Inject
private UserDao userDAO;
/**
* This method creates a new user.
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
public Response registerUser(UserRegistration userRegistration) {
String subject = this.getCallerSubject();
if (subject == null) {
return Response.status(Response.Status.UNAUTHORIZED).entity("Missing subject").build();
}
if (userDAO.findUserByRegistryId(subject) != null) {
return Response.status(Response.Status.BAD_REQUEST).entity("User is already registered").build();
}
User newUser = new User();
newUser.setSubject(subject);
newUser.setConsentGiven(userRegistration.isConsentGiven());
userDAO.createUser(newUser);
return Response.status(Response.Status.NO_CONTENT).build();
}
/**
* This method returns the user registration data for a user.
*/
@GET
@Path("self")
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public Response getUser() {
String subject = this.getCallerSubject();
if (subject == null) {
return Response.status(Response.Status.UNAUTHORIZED).entity("Missing subject").build();
}
User prevUser = userDAO.findUserByRegistryId(subject);
if (prevUser == null) {
return Response.status(Response.Status.NOT_FOUND).entity("User is not registered").build();
}
UserRegistrationInfo userRegistration = new UserRegistrationInfo();
userRegistration.setUserId(prevUser.getUserId());
userRegistration.setConsentGiven(prevUser.isConsentGiven());
return Response.status(Response.Status.OK).entity(userRegistration).build();
}
/**
* This method updates the user registration data for a user.
*/
@PUT
@Path("self")
@Consumes(MediaType.APPLICATION_JSON)
@Transactional
public Response updateUser(UserRegistration userRegistration) {
String subject = this.getCallerSubject();
if (subject == null) {
return Response.status(Response.Status.UNAUTHORIZED).entity("Missing subject").build();
}
User prevUser = userDAO.findUserByRegistryId(subject);
if (prevUser == null) {
return Response.status(Response.Status.NOT_FOUND).entity("User is not registered").build();
}
if (prevUser.isDeleteRequested()) {
return Response.status(Response.Status.CONFLICT).entity("User has requested deletion").build();
}
prevUser.setConsentGiven(userRegistration.isConsentGiven());
userDAO.updateUser(prevUser);
return Response.status(Response.Status.NO_CONTENT).build();
}
/**
* This method schedules an asynchronous process to remove the user from the system.
*/
@DELETE
@Path("self")
@Transactional
public Response deleteUser() {
String subject = this.getCallerSubject();
if (subject == null) {
return Response.status(Response.Status.UNAUTHORIZED).entity("Missing subject").build();
}
User prevUser = userDAO.findUserByRegistryId(subject);
if (prevUser == null) {
return Response.status(Response.Status.NOT_FOUND).entity("User is not registered").build();
}
prevUser.setDeleteRequested(true);
prevUser.setSubject(null);
userDAO.updateUser(prevUser);
return Response.status(Response.Status.NO_CONTENT).build();
}
}

View File

@@ -0,0 +1,35 @@
package com.ibm.codey.bank.accounts.dao;
import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import com.ibm.codey.bank.accounts.models.User;
@RequestScoped
public class UserDao {
@PersistenceContext(name = "jpa-unit")
private EntityManager em;
public void createUser(User user) {
em.persist(user);
}
public void updateUser(User user) {
em.merge(user);
}
public User findUserByRegistryId(String subject) {
try {
return em.createNamedQuery("User.findUserByRegistryId", User.class)
.setParameter("subject", subject).getSingleResult();
} catch(NoResultException e) {
return null;
}
}
}

View File

@@ -0,0 +1,45 @@
package com.ibm.codey.bank.accounts.models;
import java.io.Serializable;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "users")
@NamedQueries({
@NamedQuery(name = "User.findUserByRegistryId", query = "SELECT e FROM User e WHERE e.subject = :subject"),
})
@Getter @Setter
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@Column(name = "user_id")
@Id
@Setter(AccessLevel.NONE)
private String userId;
@Column(name = "subject", unique=true)
private String subject;
@Column(name = "consent_given")
private boolean consentGiven;
@Column(name = "delete_requested")
private boolean deleteRequested;
public User() {
this.userId = UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,2 @@
default.http.port=9080
default.https.port=9443

View File

@@ -0,0 +1,2 @@
# This option is needed when using an IBM JRE to avoid a handshake failure when making a secure JDBC connection.
-Dcom.ibm.jsse2.overrideDefaultTLS=true

View File

@@ -0,0 +1,53 @@
<server description="Liberty server">
<featureManager>
<feature>jpa-2.2</feature>
<feature>microProfile-3.0</feature>
<feature>mpJwt-1.1</feature>
</featureManager>
<logging traceSpecification="eclipselink=all" maxFileSize="20" maxFiles="10"/>
<keyStore id="digicertRootCA" password="digicert" location="${server.config.dir}/resources/security/digicert-root-ca.jks"/>
<ssl id="defaultSSLConfig" keyStoreRef="defaultKeyStore" trustStoreRef="digicertRootCA" />
<httpEndpoint host="*" httpPort="${default.http.port}"
httpsPort="${default.https.port}" id="defaultHttpEndpoint"/>
<mpJwt
id="jwt"
issuer="${OIDC_ISSUERIDENTIFIER}"
jwksUri="${OIDC_JWKENDPOINTURL}"
audiences="${OIDC_AUDIENCES}"
userNameAttribute="sub"
/>
<library id="PostgresLib">
<fileset dir="${server.config.dir}/jdbc"/>
</library>
<dataSource id="AccountsDataSource" jndiName="jdbc/AccountsDataSource">
<jdbcDriver libraryRef="PostgresLib" />
<!-- Idle connections to this server are timing out after 5 minutes.
It is recommended to set maxIdleTime to half of that value to avoid jdbc failures (e.g. broken pipe).
Reap time is reduced from default of 3 minutes to close idle connections in time. -->
<connectionManager maxIdleTime="2m30s" reapTime="60s"/>
<properties.postgresql
serverName="${DB_SERVERNAME}"
portNumber="${DB_PORTNUMBER}"
databaseName="${DB_DATABASENAME}"
user="${DB_USER}"
password="${DB_PASSWORD}"
ssl="false"
/>
</dataSource>
<webApplication location="user-service.war" contextRoot="/">
<application-bnd>
<security-role name="authenticated">
<special-subject type="ALL_AUTHENTICATED_USERS"/>
</security-role>
</application-bnd>
</webApplication>
</server>

View File

@@ -0,0 +1,10 @@
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm orm_2_0.xsd"
version="2.0">
<persistence-unit-metadata>
<persistence-unit-defaults>
<schema>bank</schema>
</persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
<persistence-unit name="jpa-unit" transaction-type="JTA">
<jta-data-source>jdbc/AccountsDataSource</jta-data-source>
<shared-cache-mode>NONE</shared-cache-mode>
<properties>
<property name="eclipselink.target-database" value="PostgreSQL"/>
<property name="eclipselink.logging.level" value="ALL"/>
<property name="eclipselink.logging.parameters" value="true"/>
</properties>
</persistence-unit>
</persistence>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>

View File

@@ -0,0 +1,27 @@
<web-app
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>user-service</display-name>
<security-role>
<role-name>authenticated</role-name>
</security-role>
<security-constraint>
<display-name>Security Constraints</display-name>
<web-resource-collection>
<web-resource-name>ProtectedArea</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>authenticated</role-name>
</auth-constraint>
<user-data-constraint>
<transport-guarantee>NONE</transport-guarantee>
</user-data-constraint>
</security-constraint>
</web-app>