Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,24 +1,42 @@
package io.openaev.aop.audit_log;

import static io.openaev.helper.CryptoHelper.hashWithSHA256;

import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
import io.openaev.database.audit.AuditLogHash;
import io.openaev.database.audit.AuditLogIgnore;
import io.openaev.database.audit.AuditLogRedact;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;

/** Dedicated mapper for audit payloads that can ignore {@link AuditLogIgnore} fields. */
/** Dedicated mapper for audit payloads that can ignore/mask annotated fields. */
@Component
public class AuditObjectMapper {

private final ObjectMapper mapper;

public AuditObjectMapper(ObjectMapper source) {
ObjectMapper copy = source.copy();
copy.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
AnnotationIntrospector base = copy.getSerializationConfig().getAnnotationIntrospector();
copy.setAnnotationIntrospector(
AnnotationIntrospector.pair(new AuditLogIgnoreIntrospector(), base));
copy.setSerializerFactory(
copy.getSerializerFactory().withSerializerModifier(new AuditMaskingSerializerModifier()));
this.mapper = copy;
}

Expand All @@ -32,4 +50,93 @@ public boolean hasIgnoreMarker(AnnotatedMember member) {
return member.hasAnnotation(AuditLogIgnore.class) || super.hasIgnoreMarker(member);
}
}

private static class AuditMaskingSerializerModifier extends BeanSerializerModifier {

@Override
public List<BeanPropertyWriter> changeProperties(
SerializationConfig config,
com.fasterxml.jackson.databind.BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
for (BeanPropertyWriter writer : beanProperties) {
AnnotatedMember member = writer.getMember();
if (member == null) {
continue;
}
if (member.hasAnnotation(AuditLogHash.class)) {
writer.assignSerializer(AuditHashSerializer.INSTANCE);
} else if (member.hasAnnotation(AuditLogRedact.class)) {
writer.assignSerializer(AuditRedactSerializer.INSTANCE);
}
}
return beanProperties;
}
}

private static class AuditHashSerializer extends StdSerializer<Object> {

private static final AuditHashSerializer INSTANCE = new AuditHashSerializer();
private static final ObjectMapper HASH_INPUT_MAPPER = new ObjectMapper();

private AuditHashSerializer() {
super(TypeFactory.defaultInstance().constructType(Object.class));
}

@Override
public void serialize(
Object value, com.fasterxml.jackson.core.JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (value == null) {
provider.defaultSerializeNull(gen);
return;
}
gen.writeString(hashWithSHA256(toHashInput(value)));
}

private static String toHashInput(Object value) throws IOException {
if (value.getClass().isArray() || value instanceof List<?>) {
return HASH_INPUT_MAPPER.writeValueAsString(normalizeStructuredValue(value));
}
return String.valueOf(value);
}

private static Object normalizeStructuredValue(Object value) {
if (value == null) {
return null;
}
if (value.getClass().isArray()) {
int length = Array.getLength(value);
List<Object> normalized = new ArrayList<>(length);
for (int index = 0; index < length; index++) {
normalized.add(normalizeStructuredValue(Array.get(value, index)));
}
return normalized;
}
if (value instanceof List<?> listValue) {
List<Object> normalized = new ArrayList<>(listValue.size());
for (Object element : listValue) {
normalized.add(normalizeStructuredValue(element));
}
return normalized;
}
return value;
}
}

private static class AuditRedactSerializer extends StdSerializer<Object> {

private static final AuditRedactSerializer INSTANCE = new AuditRedactSerializer();
private static final String REDACTED = "[REDACTED]";

private AuditRedactSerializer() {
super(TypeFactory.defaultInstance().constructType(Object.class));
}

@Override
public void serialize(
Object value, com.fasterxml.jackson.core.JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(REDACTED);
}
}
}
20 changes: 14 additions & 6 deletions openaev-api/src/main/java/io/openaev/api/users/dto/UserInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,27 @@
import static io.openaev.config.AppConfig.PHONE_REGEXP;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogIgnore;
import io.openaev.database.audit.AuditLogRedact;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.util.List;

public record UserInput(
@JsonProperty(ALIAS_EMAIL) @NotBlank @Email(message = EMAIL_FORMAT) String email,
@JsonProperty(ALIAS_FIRSTNAME) String firstname,
@JsonProperty(ALIAS_LASTNAME) String lastname,
@JsonProperty(ALIAS_PLAIN_PASSWORD) String plainPassword,
@AuditLogIgnore @JsonProperty(ALIAS_EMAIL) @NotBlank @Email(message = EMAIL_FORMAT)
String email,
@AuditLogIgnore @JsonProperty(ALIAS_FIRSTNAME) String firstname,
@AuditLogIgnore @JsonProperty(ALIAS_LASTNAME) String lastname,
@AuditLogRedact @JsonProperty(ALIAS_PLAIN_PASSWORD) String plainPassword,
@JsonProperty(ALIAS_PGP_KEY) String pgpKey,
@JsonProperty(ALIAS_PHONE) @Pattern(regexp = PHONE_REGEXP, message = PHONE_FORMAT) String phone,
@JsonProperty(ALIAS_PHONE2) @Pattern(regexp = PHONE_REGEXP, message = PHONE_FORMAT)
@AuditLogIgnore
@JsonProperty(ALIAS_PHONE)
@Pattern(regexp = PHONE_REGEXP, message = PHONE_FORMAT)
String phone,
@AuditLogIgnore
@JsonProperty(ALIAS_PHONE2)
@Pattern(regexp = PHONE_REGEXP, message = PHONE_FORMAT)
String phone2,
@JsonProperty(ALIAS_ORGANIZATION) String organizationId,
@JsonProperty(ALIAS_TAGS) List<String> tagIds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
import static io.openaev.config.AppConfig.EMAIL_FORMAT;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogHash;
import io.openaev.database.audit.AuditLogIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import java.util.List;
import java.util.Set;

public record UserOutput(
@JsonProperty(ALIAS_ID) @NotBlank String id,
@JsonProperty(ALIAS_EMAIL) @NotBlank @Email(message = EMAIL_FORMAT) String email,
@JsonProperty(ALIAS_FIRSTNAME) String firstname,
@JsonProperty(ALIAS_LASTNAME) String lastname,
@JsonProperty(ALIAS_PGP_KEY) String pgpKey,
@JsonProperty(ALIAS_PHONE) String phone,
@JsonProperty(ALIAS_PHONE2) String phone2,
@AuditLogIgnore @JsonProperty(ALIAS_EMAIL) @NotBlank @Email(message = EMAIL_FORMAT)
String email,
@AuditLogIgnore @JsonProperty(ALIAS_FIRSTNAME) String firstname,
@AuditLogIgnore @JsonProperty(ALIAS_LASTNAME) String lastname,
@AuditLogHash @JsonProperty(ALIAS_PGP_KEY) String pgpKey,
@AuditLogIgnore @JsonProperty(ALIAS_PHONE) String phone,
@AuditLogIgnore @JsonProperty(ALIAS_PHONE2) String phone2,
@JsonProperty(ALIAS_ORGANIZATION_ID) String organizationId,
@JsonProperty(ALIAS_ORGANIZATION_NAME) String organizationName,
@JsonProperty(ALIAS_TAGS) Set<String> tags,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
import static io.openaev.config.AppConfig.MANDATORY_MESSAGE;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogRedact;
import jakarta.validation.constraints.NotBlank;

public class UpdateMePasswordInput {

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_current_password")
@AuditLogRedact
private String currentPassword;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_plain_password")
@AuditLogRedact
private String password;

public String getCurrentPassword() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static io.openaev.config.AppConfig.MANDATORY_MESSAGE;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
Expand All @@ -16,28 +17,33 @@ public class UpdateProfileInput {
@Email(message = EMAIL_FORMAT)
@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_email")
@AuditLogIgnore
private String email;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_firstname")
@AuditLogIgnore
private String firstname;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_lastname")
@AuditLogIgnore
private String lastname;

@JsonProperty("user_organization")
private String organizationId;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_lang")
@AuditLogIgnore
private String lang;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_theme")
private String theme;

@JsonProperty("user_country")
@AuditLogIgnore
private String country;

@JsonProperty("user_home_dashboard")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import static io.openaev.config.AppConfig.PHONE_REGEXP;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogHash;
import io.openaev.database.audit.AuditLogIgnore;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
Expand All @@ -20,18 +22,22 @@ public class PlayerInput {
@Email(message = EMAIL_FORMAT)
@NotBlank
@JsonProperty("user_email")
@AuditLogIgnore
private String email;

@JsonProperty("user_firstname")
@AuditLogIgnore
private String firstname;

@JsonProperty("user_lastname")
@AuditLogIgnore
private String lastname;

@JsonProperty("user_organization")
private String organizationId;

@JsonProperty("user_country")
@AuditLogIgnore
private String country;

@JsonProperty("user_tags")
Expand All @@ -42,12 +48,15 @@ public class PlayerInput {

@JsonProperty("user_phone")
@Pattern(regexp = PHONE_REGEXP, message = PHONE_FORMAT)
@AuditLogIgnore
private String phone;

@JsonProperty("user_phone2")
@Pattern(regexp = PHONE_REGEXP, message = PHONE_FORMAT)
@AuditLogIgnore
private String phone2;

@JsonProperty("user_pgp_key")
@AuditLogHash
private String pgpKey;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.openaev.rest.user.form.player;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogHash;
import io.openaev.database.audit.AuditLogIgnore;
import jakarta.validation.constraints.NotBlank;
import java.util.Set;
import lombok.Builder;
Expand All @@ -15,28 +17,35 @@ public class PlayerOutput {
private String id;

@JsonProperty("user_firstname")
@AuditLogIgnore
private String firstname;

@JsonProperty("user_lastname")
@AuditLogIgnore
private String lastname;

@JsonProperty("user_email")
@NotBlank
@AuditLogIgnore
private String email;

@JsonProperty("user_organization")
private String organization;

@JsonProperty("user_country")
@AuditLogIgnore
private String country;

@JsonProperty("user_phone")
@AuditLogIgnore
private String phone;

@JsonProperty("user_phone2")
@AuditLogIgnore
private String phone2;

@JsonProperty("user_pgp_key")
@AuditLogHash
private String pgpKey;

@JsonProperty("user_tags")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.openaev.config.AppConfig.MANDATORY_MESSAGE;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogRedact;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
Expand All @@ -13,10 +14,12 @@ public class ChangePasswordInput {
@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("password")
@Schema(description = "The new password")
@AuditLogRedact
private String password;

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("password_validation")
@Schema(description = "The new password again to validate it's been typed well")
@AuditLogRedact
private String passwordValidation;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import static io.openaev.config.AppConfig.MANDATORY_MESSAGE;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.openaev.database.audit.AuditLogRedact;
import jakarta.validation.constraints.NotBlank;

public class UpdatePasswordInput {

@NotBlank(message = MANDATORY_MESSAGE)
@JsonProperty("user_plain_password")
@AuditLogRedact
private String password;

public String getPassword() {
Expand Down
Loading