Skip to content

Commit

Permalink
feat: Implementation of @CreatedBy and @UpdatedBy (#1537)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremyg484 authored Jan 5, 2024
1 parent baaa4db commit 7ad6d49
Show file tree
Hide file tree
Showing 37 changed files with 1,685 additions and 5 deletions.
1 change: 1 addition & 0 deletions security-annotations/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ plugins {
}
dependencies {
compileOnly("io.micronaut:micronaut-core-processor")
compileOnly(mnData.micronaut.data.model)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.security.annotation;

import io.micronaut.data.annotation.AutoPopulated;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* An annotation for use with Micronaut Data entities that will cause the annotated field to be automatically
* populated on save with the identity of the currently authenticated user.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
@Documented
@AutoPopulated(updateable = false)
public @interface CreatedBy {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.security.annotation;

import io.micronaut.data.annotation.AutoPopulated;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* An annotation for use with Micronaut Data entities that will cause the annotated field to be automatically
* populated on both save and update with the identity of the currently authenticated user.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
@Documented
@AutoPopulated
public @interface UpdatedBy {
}
7 changes: 5 additions & 2 deletions security/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ dependencies {
api(projects.micronautSecurityAnnotations)
implementation(mnValidation.micronaut.validation)
implementation(mnReactor.micronaut.reactor)

compileOnly(mnData.micronaut.data.runtime)
compileOnly(mn.micronaut.http.server)
compileOnly(mn.micronaut.management)
compileOnly(mn.jackson.databind)

testCompileOnly(mnData.micronaut.data.processor)
testImplementation(mnSql.h2)
testImplementation(mnSql.micronaut.jdbc.hikari)
testImplementation(mnData.micronaut.data.jdbc)
testImplementation(mnSerde.micronaut.serde.jackson)
testImplementation(mnReactor.micronaut.reactor)
testImplementation(mn.micronaut.management)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.security.audit;

import io.micronaut.context.annotation.Requires;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.exceptions.ConversionErrorException;
import io.micronaut.data.annotation.AutoPopulated;
import io.micronaut.data.annotation.event.PrePersist;
import io.micronaut.data.annotation.event.PreUpdate;
import io.micronaut.data.event.EntityEventContext;
import io.micronaut.data.model.runtime.RuntimePersistentProperty;
import io.micronaut.data.runtime.event.listeners.AutoPopulatedEntityEventListener;
import io.micronaut.security.annotation.CreatedBy;
import io.micronaut.security.annotation.UpdatedBy;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.utils.SecurityService;
import jakarta.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.util.*;
import java.util.function.Predicate;

/**
* An event listener that handles auto-population of entity fields annotated with {@link CreatedBy} or
* {@link UpdatedBy} by mapping them from the current {@link Authentication}.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
@Requires(classes = { AutoPopulatedEntityEventListener.class, EntityEventContext.class })
@Singleton
final class UserAuditingEntityEventListener extends AutoPopulatedEntityEventListener {
private static final Logger LOG = LoggerFactory.getLogger(UserAuditingEntityEventListener.class);

private final SecurityService securityService;

private final ConversionService conversionService;

UserAuditingEntityEventListener(SecurityService securityService, ConversionService conversionService) {
this.securityService = securityService;
this.conversionService = conversionService;
}

@Override
public boolean prePersist(@NonNull EntityEventContext<Object> context) {
return populate(context, PrePersist.class);
}

@Override
public boolean preUpdate(@NonNull EntityEventContext<Object> context) {
return populate(context, PreUpdate.class);
}

@Override
protected @NonNull List<Class<? extends Annotation>> getEventTypes() {
return Arrays.asList(PrePersist.class, PreUpdate.class);
}

@Override
protected @NonNull Predicate<RuntimePersistentProperty<Object>> getPropertyPredicate() {
return property -> {
final AnnotationMetadata annotationMetadata = property.getAnnotationMetadata();
return annotationMetadata.hasAnnotation(CreatedBy.class) || annotationMetadata.hasAnnotation(UpdatedBy.class);
};
}

private boolean populate(@NonNull EntityEventContext<Object> context,
@NonNull Class<? extends Annotation> listenerAnnotation) {
try {
securityService.getAuthentication().ifPresent(authentication -> {
Map<Class<?>, Object> valueForType = new HashMap<>();
for (RuntimePersistentProperty<Object> persistentProperty : getApplicableProperties(context.getPersistentEntity())) {
if (shouldSetProperty(persistentProperty, listenerAnnotation)) {
final BeanProperty<Object, Object> beanProperty = persistentProperty.getProperty();
Object value = valueForType.computeIfAbsent(beanProperty.getType(), type -> convert(authentication, beanProperty));
if (value != null) {
context.setProperty(beanProperty, value);
}
}
}
});
return true;
} catch (ConversionErrorException e) {
return false;
}
}

@Nullable
private Object convert(@NonNull Authentication authentication, @NonNull BeanProperty<Object, Object> beanProperty) throws ConversionErrorException {
try {
return conversionService.convertRequired(authentication, beanProperty.getType());
} catch (ConversionErrorException e) {
if (LOG.isErrorEnabled()) {
LOG.error("Cannot convert from {} to {} for bean property {}", authentication.getClass().getSimpleName(), beanProperty.getType(), beanProperty.getName(), e);
}
throw e;
}
}

private boolean shouldSetProperty(@NonNull RuntimePersistentProperty<Object> persistentProperty, Class<? extends Annotation> listenerAnnotation) {
if (listenerAnnotation == PrePersist.class) {
return true;
}
if (listenerAnnotation == PreUpdate.class) {
return persistentProperty.getAnnotationMetadata().booleanValue(AutoPopulated.class, AutoPopulated.UPDATEABLE).orElse(true);
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Contains classes specific to adding security auditing integration with Micronaut Data.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
package io.micronaut.security.audit;
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2017-2023 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.security.converters;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.TypeConverter;
import io.micronaut.security.annotation.CreatedBy;
import io.micronaut.security.annotation.UpdatedBy;
import io.micronaut.security.authentication.Authentication;

import java.security.Principal;
import java.util.Optional;

/**
* A {@link Principal} to {@code String} converter.
*
* This is intended as the default implementation for conversion of the current {@link Authentication} to {@code String}
* entity fields annotated with either {@link CreatedBy} or {@link UpdatedBy},
* and simply converts to {@link Principal#getName()}.
* This implementation may be replaced for custom mapping of a unique {@link String} identifier, or additional converters
* may be provided for mapping to more complex types.
*
* @author Jeremy Grelle
* @since 4.5.0
*/
@Internal
final class PrincipalToStringConverter implements TypeConverter<Principal, String> {

/**
*
* @param principal The source principal
* @param targetType The target type being converted to
* @param context The {@link ConversionContext}
* @return The converted type or empty if the conversion is not possible
*/
@Override
public Optional<String> convert(Principal principal, Class<String> targetType, ConversionContext context) {
return Optional.ofNullable(principal.getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.security.converters;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.convert.MutableConversionService;
import io.micronaut.core.convert.TypeConverterRegistrar;

import java.security.Principal;

/**
* Registers security {@link io.micronaut.core.convert.TypeConverter}s.
* @author Sergio del Amo
* @since 4.5.0
*/
@Internal
public final class SecurityTypeConvertersRegistrar implements TypeConverterRegistrar {
@Override
public void register(MutableConversionService conversionService) {
conversionService.addConverter(Principal.class, String.class, new PrincipalToStringConverter());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Security {@link io.micronaut.core.convert.TypeConverter}s.
*/
package io.micronaut.security.converters;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.micronaut.security.converters.SecurityTypeConvertersRegistrar
Loading

0 comments on commit 7ad6d49

Please sign in to comment.