Skip to content

Commit

Permalink
enable native run, add WebFilter and request-logging
Browse files Browse the repository at this point in the history
  • Loading branch information
wisskirchenj committed Jul 22, 2023
1 parent 6f5dafc commit bebea3a
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 18 deletions.
7 changes: 7 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ configurations {
}
}

graalvmNative {
binaries.all {
resources.autodetect()
}
toolchainDetection = false
}

repositories {
mavenCentral()
}
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/de/cofinpro/account/AccountReactiveApplication.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
package de.cofinpro.account;

import de.cofinpro.account.admin.LockUserToggleRequest;
import de.cofinpro.account.admin.RoleToggleRequest;
import de.cofinpro.account.admin.UserDeletedResponse;
import de.cofinpro.account.authentication.ChangepassRequest;
import de.cofinpro.account.authentication.ChangepassResponse;
import de.cofinpro.account.authentication.SignupRequest;
import de.cofinpro.account.authentication.SignupResponse;
import de.cofinpro.account.domain.SalaryRecord;
import de.cofinpro.account.domain.SalaryResponse;
import de.cofinpro.account.domain.StatusResponse;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@RegisterReflectionForBinding({SignupRequest.class,
SignupResponse.class,
ChangepassRequest.class,
ChangepassResponse.class,
LockUserToggleRequest.class,
RoleToggleRequest.class,
UserDeletedResponse.class,
SalaryRecord.class,
SalaryResponse.class,
StatusResponse.class})
public class AccountReactiveApplication {

public static void main(String[] args) {
Expand Down
24 changes: 19 additions & 5 deletions src/main/java/de/cofinpro/account/admin/AdminHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import de.cofinpro.account.persistence.LoginRoleReactiveRepository;
import de.cofinpro.account.persistence.Role;
import de.cofinpro.account.persistence.SalaryReactiveRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.support.DefaultMessageSourceResolvable;
Expand All @@ -29,8 +30,18 @@
import java.util.function.Function;
import java.util.stream.Collectors;

import static de.cofinpro.account.configuration.AdminConfiguration.*;
import static de.cofinpro.account.configuration.AdminConfiguration.ADMIN_ROLE;
import static de.cofinpro.account.configuration.AdminConfiguration.CANT_DELETE_ADMIN_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.CANT_LOCK_ADMIN_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.DELETED_SUCCESSFULLY;
import static de.cofinpro.account.configuration.AdminConfiguration.INVALID_ROLE_COMBINE_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.ROLE_NOT_FOUND_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.USER_HASNT_ROLE_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.USER_HAS_ROLE_ALREADY_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.USER_NEEDS_ROLE_ERRORMSG;
import static de.cofinpro.account.configuration.AdminConfiguration.USER_NOT_FOUND_ERRORMSG;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.EMAIL_REGEX;
import static de.cofinpro.account.configuration.ObservabilityConfiguration.extractAndLog;
import static java.lang.Boolean.TRUE;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

Expand All @@ -39,6 +50,7 @@
* /api/admin/user/{role, access} (PUT).
*/
@Service
@Slf4j
@DependsOn({"r2dbcScriptDatabaseInitializer"})
public class AdminHandler {

Expand Down Expand Up @@ -68,6 +80,7 @@ public AdminHandler(LoginReactiveRepository userRepository,
* GET /api/admin/user endpoint to display all users ascending by id with their roles, which are zipped to the Login.
*/
public Mono<ServerResponse> displayUsers(ServerRequest ignoredServerRequest) {
log.info("display users request received");
return ok().body(userRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))
.flatMap(login -> Mono.just(login)
.zipWith(roleRepository.findRolesByEmail(login.getEmail()), Login::setRoles))
Expand All @@ -81,6 +94,7 @@ public Mono<ServerResponse> displayUsers(ServerRequest ignoredServerRequest) {
*/
public Mono<ServerResponse> deleteUser(ServerRequest request) {
String email = request.pathVariable("email");
log.info("delete user '{}' request received", email);
if (!email.matches(EMAIL_REGEX)) {
return Mono.error(new ServerWebInputException("Invalid user email given: '" + email + "'!"));
}
Expand Down Expand Up @@ -128,17 +142,17 @@ private Mono<Boolean> isAdmin(List<String> roles) {
* which role to toggle for which user.
*/
public Mono<ServerResponse> toggleRole(ServerRequest request) {
return request.bodyToMono(RoleToggleRequest.class)
.flatMap(req -> ok().body(validateAndToggleRole(req, request.principal()),
SignupResponse.class));
return extractAndLog(request, RoleToggleRequest.class)
.flatMap(req -> ok().body(validateAndToggleRole(req, request.principal()),
SignupResponse.class));
}

/**
* PUT /api/admin/user/access endpoint that consumes a LockUserToggleRequest in the Request body with information on
* which user to lock or unlock.
*/
public Mono<ServerResponse> toggleUserLock(ServerRequest request) {
return request.bodyToMono(LockUserToggleRequest.class)
return extractAndLog(request, LockUserToggleRequest.class)
.flatMap(req -> ok().body(validateAndToggleLock(req, request.principal()), StatusResponse.class));
}

Expand Down
7 changes: 5 additions & 2 deletions src/main/java/de/cofinpro/account/audit/AuditHandler.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package de.cofinpro.account.audit;

import de.cofinpro.account.persistence.*;
import de.cofinpro.account.persistence.SecurityEvent;
import de.cofinpro.account.persistence.SecurityEventReactiveRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;


import static org.springframework.web.reactive.function.server.ServerResponse.ok;

/**
* service layer handler class for all audit specific endpoints: /api/security/events (GET).
*/
@Service
@Slf4j
public class AuditHandler {

private final SecurityEventReactiveRepository auditRepository;
Expand All @@ -28,6 +30,7 @@ public AuditHandler(SecurityEventReactiveRepository securityEventReactiveReposit
* @return ServerResponse Mono with a list of all security events from application runs stored in the database.
*/
public Mono<ServerResponse> getAuditEvents(ServerRequest ignoredRequest) {
log.info("get request for audit events received");
return ok().body(auditRepository.findAll(Sort.by(Sort.Direction.ASC, "id"))
.map(SecurityEvent::toResponse), AuditEventResponse.class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,24 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import org.springframework.validation.Validator;
import reactor.util.function.Tuple2;

import java.security.Principal;
import java.util.List;

import static de.cofinpro.account.configuration.AuthenticationConfiguration.*;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.MIN_PASSWORD_LENGTH;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.PASSWORD_HACKED_ERRORMSG;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.PASSWORD_TOO_SHORT_ERRORMSG;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.PASSWORD_UPDATEMSG;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.SAME_PASSWORD_ERRORMSG;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.USER_EXISTS_ERRORMSG;
import static de.cofinpro.account.configuration.AuthenticationConfiguration.passwordIsHacked;
import static de.cofinpro.account.configuration.ObservabilityConfiguration.extractAndLog;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

/**
Expand Down Expand Up @@ -58,7 +65,7 @@ public AuthenticationHandler(Validator validator,
*/
@Transactional
public Mono<ServerResponse> signup(ServerRequest request) {
return request.bodyToMono(SignupRequest.class)
return extractAndLog(request, SignupRequest.class)
.zipWith(userRepository.count())
.flatMap(tuple -> ok().body(validateAndSave(tuple), SignupResponse.class));
}
Expand Down Expand Up @@ -131,7 +138,7 @@ private Mono<SignupResponse> saveUser(SignupRequest signupRequest, String role)
* @return a ChangepassResponse Json (200) as body of a ServerResponse or a 400 if validation error or same password
*/
public Mono<ServerResponse> changePassword(ServerRequest request) {
return request.bodyToMono(ChangepassRequest.class)
return extractAndLog(request, ChangepassRequest.class)
.zipWith(request.principal())
.flatMap(tuple -> ok().body(validateAndChangepass(tuple), ChangepassResponse.class));
}
Expand All @@ -146,6 +153,7 @@ private Mono<ChangepassResponse> validateAndChangepass(Tuple2<ChangepassRequest,
final String newPassword = tuple.getT1().newPassword();
String passwordValidationError = validatePassword(newPassword);
if (!passwordValidationError.isEmpty()) {
log.warn("password validation failed!");
return Mono.error(new ServerWebInputException(passwordValidationError));
}
return userRepository.findByEmailIgnoreCase(tuple.getT2().getName())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.cofinpro.account.configuration;

import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

@Configuration
public class ObservabilityConfiguration {

public static <T> Mono<T> extractAndLog(ServerRequest request, Class<T> bodyClass) {
var logger = LoggerFactory.getLogger(bodyClass);
var user = MDC.get("user");
return request
.bodyToMono(bodyClass)
.doOnNext(body -> logger.info("{} {} {} with payload '{}'",
user,
request.method().name(),
request.path(),
body));
}

@Bean
ObservabilityFilter observabilityFilter() {
return new ObservabilityFilter();
}

static class ObservabilityFilter implements WebFilter {

@Override
@NonNull
public Mono<Void> filter(@NonNull ServerWebExchange serverWebExchange, @NonNull WebFilterChain webFilterChain) {
return ReactiveSecurityContextHolder.getContext().doOnNext(sc -> {
if (sc.getAuthentication() != null) {
MDC.put("user", "<" + sc.getAuthentication().getName() + ">");
}
}).then(webFilterChain.filter(serverWebExchange));
}
}
}
19 changes: 16 additions & 3 deletions src/main/java/de/cofinpro/account/domain/AccountHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,15 @@
import java.util.Optional;
import java.util.stream.Collectors;

import static de.cofinpro.account.configuration.AccountConfiguration.*;
import static de.cofinpro.account.configuration.AccountConfiguration.ADDED_SUCCESSFULLY;
import static de.cofinpro.account.configuration.AccountConfiguration.DUPLICATE_RECORDS_ERRORMSG;
import static de.cofinpro.account.configuration.AccountConfiguration.NO_SUCH_EMPLOYEE_ERRORMSG;
import static de.cofinpro.account.configuration.AccountConfiguration.NO_SUCH_SALES_RECORD_ERRORMSG;
import static de.cofinpro.account.configuration.AccountConfiguration.PERIOD_REGEX;
import static de.cofinpro.account.configuration.AccountConfiguration.RECORDMSG_START;
import static de.cofinpro.account.configuration.AccountConfiguration.RECORD_ALREADY_EXISTS_ERRORMSG;
import static de.cofinpro.account.configuration.AccountConfiguration.UPDATED_SUCCESSFULLY;
import static de.cofinpro.account.configuration.ObservabilityConfiguration.extractAndLog;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.function.Predicate.not;
Expand Down Expand Up @@ -58,12 +66,16 @@ public AccountHandler(LoginReactiveRepository userRepository,
*/
public Mono<ServerResponse> accessPayrolls(ServerRequest request) {
Optional<String> searchPeriod = request.queryParam("period");
log.info("access payrolls {}", searchPeriod.isPresent()
? "(period = %s)".formatted(searchPeriod.get())
: "without search period");
if (searchPeriod.isPresent() && !searchPeriod.get().matches(PERIOD_REGEX)) {
return Mono.error(new ServerWebInputException("Wrong Date: Use mm-yyyy format!"));
}
return request.principal()
.flatMap(principal -> ok().body(selectSalaries(principal.getName(), searchPeriod),
new ParameterizedTypeReference<>(){}));
new ParameterizedTypeReference<>() {
}));
}

/**
Expand Down Expand Up @@ -91,7 +103,7 @@ private Mono<List<SalaryResponse>> selectSalaries(String email, Optional<String>
* @return ServerResponse Mono with a success status or Error Mono if validation went wrong or data to update not found
*/
public Mono<ServerResponse> changePayrolls(ServerRequest request) {
return request.bodyToMono(SalaryRecord.class)
return extractAndLog(request, SalaryRecord.class)
.flatMap(salaryRecord -> ok().body(validateAndUpdate(salaryRecord), StatusResponse.class));
}

Expand Down Expand Up @@ -126,6 +138,7 @@ private Mono<StatusResponse> validateAndUpdate(SalaryRecord salaryRecord) {
*/
@Transactional
public Mono<ServerResponse> uploadPayrolls(ServerRequest request) {
log.info("post request to add payroll");
return request.bodyToFlux(SalaryRecord.class)
.index().flatMap(this::validateAll)
.collectList()
Expand Down
4 changes: 0 additions & 4 deletions src/main/resources/initTables.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
DROP TABLE IF EXISTS SALARY;
DROP TABLE IF EXISTS LOGIN_ROLES;
DROP TABLE IF EXISTS LOGIN;
DROP TABLE IF EXISTS ROLES;
DROP TABLE IF EXISTS AUDIT;
CREATE TABLE IF NOT EXISTS LOGIN (
id BIGSERIAL PRIMARY KEY NOT NULL,
name VARCHAR (64),
Expand Down
18 changes: 18 additions & 0 deletions src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

<appender name="Console"
class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{ISO8601} %highlight(%-5level) [%green(%t)] %magenta(%C{1}): %X{user} %msg%n%throwable
</pattern>
</encoder>
</appender>

<!-- LOG everything at INFO level -->
<root level="info">
<appender-ref ref="Console"/>
</root>

</configuration>
Binary file removed src/test/resources/data/account_template.mv.db
Binary file not shown.
42 changes: 42 additions & 0 deletions src/test/resources/initTables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
DROP TABLE IF EXISTS SALARY;
DROP TABLE IF EXISTS LOGIN_ROLES;
DROP TABLE IF EXISTS LOGIN;
DROP TABLE IF EXISTS ROLES;
DROP TABLE IF EXISTS AUDIT;
CREATE TABLE IF NOT EXISTS LOGIN
(
id BIGSERIAL PRIMARY KEY NOT NULL,
name VARCHAR(64),
lastname VARCHAR(64),
email VARCHAR(64) UNIQUE NOT NULL,
password VARCHAR(128) NOT NULL,
account_locked BOOL NOT NULL,
failed_logins SMALLINT
);
CREATE TABLE IF NOT EXISTS SALARY
(
id BIGSERIAL PRIMARY KEY NOT NULL,
email VARCHAR(64) NOT NULL,
period VARCHAR(7) NOT NULL,
salary BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS ROLES
(
id BIGSERIAL PRIMARY KEY NOT NULL,
user_role VARCHAR(20) UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS LOGIN_ROLES
(
id BIGSERIAL PRIMARY KEY NOT NULL,
email VARCHAR(64) NOT NULL,
user_role VARCHAR(20) NOT NULL
);
CREATE TABLE IF NOT EXISTS AUDIT
(
id BIGSERIAL PRIMARY KEY NOT NULL,
date DATE NOT NULL,
action VARCHAR(20) NOT NULL,
subject VARCHAR(64),
object VARCHAR(128),
path VARCHAR(64) NOT NULL
);

0 comments on commit bebea3a

Please sign in to comment.