Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make AuditorAware work with Spring Data Mongo Reactive

Spring Security 5 provides a ReactiveSecurityContextHolder to fetch the SecurityContext from a Reactive context, but when I want to implement AuditorAware and get audition work automatically, but it does not work. Currently I can not find a Reactive variant for AuditorAware.

@Bean
public AuditorAware<Username> auditor() {
    return () -> ReactiveSecurityContextHolder.getContext()
        .map(SecurityContext::getAuthentication)
        .log()
        .filter(a -> a != null && a.isAuthenticated())
        .map(Authentication::getPrincipal)
        .cast(UserDetails.class)
        .map(auth -> new Username(auth.getName()))
        .switchIfEmpty(Mono.empty())
        .blockOptional();
}

I have added @EnableMongoAuduting on my boot Application class.

On the Mongo document class. I added audition related annotations.

@CreatedDate
private LocalDateTime createdDate;

@CreatedBy
private Username author;

When I added a post, the createdDate is filled, but author is null.

{"id":"5a49ccdb9222971f40a4ada1","title":"my first post","content":"content of my first post","createdDate":"2018-01-01T13:53:31.234","author":null}

The complete codes is here, based on Spring Boot 2.0.0.M7.

Update: Spring Boot 2.4.0-M2/Spring Data Common 2.4.0-M2/Spring Data Mongo 3.1.0-M2 includes a ReactiveAuditorAware, Check this new sample, Note: use @EnableReactiveMongoAuditing to activiate it.

like image 769
Hantsy Avatar asked Nov 19 '22 00:11

Hantsy


2 Answers

I am posting another solution which counts with input id to support update operations:

@Component
@RequiredArgsConstructor
public class AuditCallback implements ReactiveBeforeConvertCallback<AuditableEntity> {

    private final ReactiveMongoTemplate mongoTemplate;

    private Mono<?> exists(Object id, Class<?> entityClass) {
        if (id == null) {
            return Mono.empty();
        }
        return mongoTemplate.findById(id, entityClass);
    }

    @Override
    public Publisher<AuditableEntity> onBeforeConvert(AuditableEntity entity, String collection) {
        var securityContext = ReactiveSecurityContextHolder.getContext();
        return securityContext
                .zipWith(exists(entity.getId(), entity.getClass()))
                .map(tuple2 -> {
                    var auditableEntity = (AuditableEntity) tuple2.getT2();
                    auditableEntity.setLastModifiedBy(tuple2.getT1().getAuthentication().getName());
                    auditableEntity.setLastModifiedDate(Instant.now());
                    return auditableEntity;
                })
                .switchIfEmpty(Mono.zip(securityContext, Mono.just(entity))
                        .map(tuple2 -> {
                            var auditableEntity = (AuditableEntity) tuple2.getT2();
                            String principal = tuple2.getT1().getAuthentication().getName();
                            Instant now = Instant.now();
                            auditableEntity.setLastModifiedBy(principal);
                            auditableEntity.setCreatedBy(principal);
                            auditableEntity.setLastModifiedDate(now);
                            auditableEntity.setCreatedDate(now);
                            return auditableEntity;
                        }));
    }
}
like image 122
Adam Ostrožlík Avatar answered Dec 17 '22 08:12

Adam Ostrožlík


Deprecated: see the update solution in the original post

Before the official reactive AuditAware is provided, there is an alternative to implement these via Spring Data Mongo specific ReactiveBeforeConvertCallback.

  1. Do not use @EnableMongoAuditing
  2. Implement your own ReactiveBeforeConvertCallback, here I use a PersistentEntity interface for those entities that need to be audited.
public class PersistentEntityCallback implements ReactiveBeforeConvertCallback<PersistentEntity> {

    @Override
    public Publisher<PersistentEntity> onBeforeConvert(PersistentEntity entity, String collection) {
        var user = ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .filter(it -> it != null && it.isAuthenticated())
                .map(Authentication::getPrincipal)
                .cast(UserDetails.class)
                .map(userDetails -> new Username(userDetails.getUsername()))
                .switchIfEmpty(Mono.empty());

        var currentTime = LocalDateTime.now();

        if (entity.getId() == null) {
            entity.setCreatedDate(currentTime);
        }
        entity.setLastModifiedDate(currentTime);

        return user
                .map(u -> {
                            if (entity.getId() == null) {
                                entity.setCreatedBy(u);
                            }
                            entity.setLastModifiedBy(u);

                            return entity;
                        }
                )
                .defaultIfEmpty(entity);
    }
}

Check the complete codes here.

like image 38
Hantsy Avatar answered Dec 17 '22 06:12

Hantsy