Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions java/user-security-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
116 changes: 116 additions & 0 deletions java/user-security-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# CardDemo User Security Service

Spring Boot modernization of the CardDemo CICS user-security module — the COBOL programs that
maintain the `USRSEC` VSAM file.

| Legacy program | Function | Modern endpoint |
| --- | --- | --- |
| `COSGN00C` | Signon / authentication | `POST /api/signon` (alias `POST /api/auth/login`) |
| `COUSR00C` | List users, 10 per screen | `GET /api/users?page=0&size=10` |
| `COUSR02C` (lookup) | Read one user by ID | `GET /api/users/{userId}` |
| `COUSR01C` | Add user | `POST /api/users` |
| `COUSR02C` | Update user | `PUT /api/users/{userId}` |
| `COUSR03C` | Delete user | `DELETE /api/users/{userId}` |

## Running

```bash
cd java/user-security-service
mvn spring-boot:run
```

The service listens on `http://localhost:8080` with an H2 in-memory database
(`jdbc:h2:mem:usrsec`, console at `/h2-console`). A small operator screen that exercises every
endpoint — modeled on the BMS maps of the legacy transactions — is served at
`http://localhost:8080/`.

Build and test:

```bash
mvn package # compiles, runs tests, builds the executable jar
mvn test
```

## Data model

`User` maps `SEC-USER-DATA` from copybook `app/cpy/CSUSR01Y.cpy`:

| Field | COBOL | Java |
| --- | --- | --- |
| `SEC-USR-ID` | `PIC X(08)` | `userId` (primary key, max 8) |
| `SEC-USR-FNAME` | `PIC X(20)` | `firstName` (max 20) |
| `SEC-USR-LNAME` | `PIC X(20)` | `lastName` (max 20) |
| `SEC-USR-PWD` | `PIC X(08)` | `password` (max 8, never returned by the API) |
| `SEC-USR-TYPE` | `PIC X(01)` | `userType` — `A` admin, `U` regular |

User IDs, passwords and user types are trimmed and upper-cased on the way in, matching the
uppercase, space-padded fields the BMS maps handed to the COBOL programs.

## Seed data

`src/main/resources/usrsec-seed.txt` holds the in-stream `SYSUT1` records of
`app/jcl/ESDSRRDS.jcl` verbatim; `UsrsecSeeder` parses them with the `CSUSR01Y` layout
(ID 1-8, first name 9-28, last name 29-48, password 49-56, user type 57) and loads them on
startup when the table is empty.

Note that column 57 of those records is `SEC-USR-TYPE`, not the last character of the password:
the seeded password is `PASSWORD` for every user (as documented in the repository root
`README.md`), while the trailing `A`/`U` is the user type.

| User ID | Name | Password | Type |
| --- | --- | --- | --- |
| `ADMIN001` | MARGARET GOLD | `PASSWORD` | A |
| `ADMIN002` | RUSSELL RUSSELL | `PASSWORD` | A |
| `ADMIN003` | RAYMOND WHITMORE | `PASSWORD` | A |
| `ADMIN004` | EMMANUEL CASGRAIN | `PASSWORD` | A |
| `ADMIN005` | GRANVILLE LACHAPELLE | `PASSWORD` | A |
| `USER0001` | LAWRENCE THOMAS | `PASSWORD` | U |
| `USER0002` | AJITH KUMAR | `PASSWORD` | U |
| `USER0003` | LAURITZ ALME | `PASSWORD` | U |
| `USER0004` | AVERARDO MAZZI | `PASSWORD` | U |
| `USER0005` | LEE TING | `PASSWORD` | U |

## Error semantics

The HTTP status codes carry the message text of the original screens:

| Condition | Legacy message | Status |
| --- | --- | --- |
| Unknown user ID | `User ID NOT found...` | 404 |
| Wrong password on signon | `Wrong Password. Try again ...` | 401 |
| Duplicate user on add | `User ID already exist...` | 409 |
| Empty / oversized field | `First Name can NOT be empty...` etc. | 400 |

## Examples

```bash
# Signon as an admin (COSGN00C -> COADM01C)
curl -sX POST localhost:8080/api/signon -H 'Content-Type: application/json' \
-d '{"userId":"ADMIN001","password":"PASSWORD"}'
# {"user":{"userId":"ADMIN001",...,"userType":"A"},"admin":true,"nextProgram":"COADM01C"}

# Wrong password -> 401, unknown user -> 404
curl -sX POST localhost:8080/api/signon -H 'Content-Type: application/json' \
-d '{"userId":"ADMIN001","password":"NOPE"}'

# List page 1 (COUSR00C)
curl -s 'localhost:8080/api/users?page=0&size=10'

# Add, update and delete (COUSR01C / COUSR02C / COUSR03C)
curl -sX POST localhost:8080/api/users -H 'Content-Type: application/json' \
-d '{"userId":"NEWUSR1","firstName":"NEW","lastName":"USER","password":"PWD00001","userType":"U"}'
curl -sX PUT localhost:8080/api/users/NEWUSR1 -H 'Content-Type: application/json' \
-d '{"firstName":"CHANGED","lastName":"USER","password":"PWD00002","userType":"A"}'
curl -sX DELETE localhost:8080/api/users/NEWUSR1
```

## Tests

* `UsrsecSeederTest` — fixed-width `ESDSRRDS` record parsing.
* `UserServiceTest` — signon success/failure, pagination, CRUD and the not-found/duplicate paths.
* `UserSecurityApiTest` — MockMvc coverage of every endpoint, including status codes and
validation messages.

```bash
mvn test
```
57 changes: 57 additions & 0 deletions java/user-security-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
<relativePath/>
</parent>

<groupId>com.carddemo</groupId>
<artifactId>user-security-service</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>CardDemo User Security Service</name>
<description>Modernized CardDemo USRSEC user-security module (COSGN00C, COUSR00C-COUSR03C)</description>

<properties>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.carddemo.usersecurity;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserSecurityApplication {

public static void main(String[] args) {
SpringApplication.run(UserSecurityApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.carddemo.usersecurity.config;

import com.carddemo.usersecurity.domain.User;
import com.carddemo.usersecurity.repository.UserRepository;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

/**
* Loads the fixed-width USRSEC records shipped in app/jcl/ESDSRRDS.jcl using the
* SEC-USER-DATA layout of copybook CSUSR01Y: user ID 1-8, first name 9-28,
* last name 29-48, password 49-56, user type 57.
*/
@Component
public class UsrsecSeeder implements CommandLineRunner {

private static final int USER_ID_END = 8;
private static final int FIRST_NAME_END = 28;
private static final int LAST_NAME_END = 48;
private static final int PASSWORD_END = 56;
private static final int USER_TYPE_END = 57;
private static final String ADMIN_ID_PREFIX = "ADMIN";

private final UserRepository userRepository;
private final Resource seedData;

public UsrsecSeeder(UserRepository userRepository,
@Value("classpath:usrsec-seed.txt") Resource seedData) {
this.userRepository = userRepository;
this.seedData = seedData;
}

@Override
public void run(String... args) {
if (userRepository.count() > 0) {
return;
}
userRepository.saveAll(readSeedRecords());
}

List<User> readSeedRecords() {
List<User> users = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(seedData.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
if (line.isBlank()) {
continue;
}
users.add(parse(line));
}
} catch (IOException e) {
throw new UncheckedIOException("Unable to read USRSEC seed data", e);
}
return users;
}

static User parse(String record) {
String padded = record.length() < USER_TYPE_END
? record + " ".repeat(USER_TYPE_END - record.length())
: record;
String userId = padded.substring(0, USER_ID_END).trim();
String firstName = padded.substring(USER_ID_END, FIRST_NAME_END).trim();
String lastName = padded.substring(FIRST_NAME_END, LAST_NAME_END).trim();
String password = padded.substring(LAST_NAME_END, PASSWORD_END).trim();
String userType = padded.substring(PASSWORD_END, USER_TYPE_END).trim();
if (userType.isEmpty()) {
userType = userId.startsWith(ADMIN_ID_PREFIX) ? "A" : "U";
}
return new User(userId, firstName, lastName, password, userType);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.carddemo.usersecurity.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

/**
* Maps the USRSEC record layout defined by copybook CSUSR01Y (SEC-USER-DATA).
*/
@Entity
@Table(name = "usrsec")
public class User {

@Id
@Column(name = "user_id", length = 8, nullable = false)
private String userId;

@Column(name = "first_name", length = 20, nullable = false)
private String firstName;

@Column(name = "last_name", length = 20, nullable = false)
private String lastName;

@Column(name = "password", length = 8, nullable = false)
private String password;

@Column(name = "user_type", length = 1, nullable = false)
private String userType;

protected User() {
}

public User(String userId, String firstName, String lastName, String password, String userType) {
this.userId = userId;
this.firstName = firstName;
this.lastName = lastName;
this.password = password;
this.userType = userType;
}

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getUserType() {
return userType;
}

public void setUserType(String userType) {
this.userType = userType;
}

public boolean isAdmin() {
return "A".equals(userType);
}
}
Loading