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,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>