Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of @CreatedBy and @UpdatedBy #1537

Merged
merged 18 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
23 changes: 23 additions & 0 deletions security-audit/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id("io.micronaut.build.internal.security-module")
}

tasks.findBaseline.get().enabled = false

dependencies {
annotationProcessor(mn.micronaut.graal)
api(projects.micronautSecurity)
implementation(mnData.micronaut.data.runtime)

testImplementation(mnSerde.micronaut.serde.jackson)
testImplementation(mnReactor.micronaut.reactor)
testImplementation(mn.micronaut.http.client)
testAnnotationProcessor(projects.micronautSecurityAnnotations)
testAnnotationProcessor(mn.micronaut.inject.java)
testImplementation(mn.micronaut.http.server.netty)

testCompileOnly(mnData.micronaut.data.processor)
testImplementation(mnData.micronaut.data.jdbc)
testImplementation(mnSql.h2)
testImplementation(mnSql.micronaut.jdbc.hikari)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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.core.convert.ConversionContext;
import io.micronaut.core.convert.TypeConverter;
import io.micronaut.security.authentication.Authentication;
import jakarta.inject.Singleton;

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 io.micronaut.security.audit.annotation.CreatedBy} or {@link io.micronaut.security.audit.annotation.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
*/
@Singleton
public 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,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.audit.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.audit.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 {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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.event.listeners;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.convert.ConversionService;
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.audit.annotation.CreatedBy;
import io.micronaut.security.audit.annotation.UpdatedBy;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.utils.SecurityService;
import jakarta.inject.Singleton;

import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
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
*/
@Singleton
public class UserAuditingEntityEventListener extends AutoPopulatedEntityEventListener {

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) {
autoPopulateUserIdentity(context, false);
return true;
}

@Override
public boolean preUpdate(@NonNull EntityEventContext<Object> context) {
autoPopulateUserIdentity(context, true);
return true;
}

@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 void autoPopulateUserIdentity(@NonNull EntityEventContext<Object> context, boolean isUpdate) {
final RuntimePersistentProperty<Object>[] applicableProperties = getApplicableProperties(context.getPersistentEntity());
for (RuntimePersistentProperty<Object> persistentProperty : applicableProperties) {
if (isUpdate) {
if (!persistentProperty.getAnnotationMetadata().booleanValue(AutoPopulated.class, AutoPopulated.UPDATEABLE).orElse(true)) {
continue;
}
}

final BeanProperty<Object, Object> beanProperty = persistentProperty.getProperty();
getCurrentUserIdentityForProperty(beanProperty).ifPresent(identity -> context.setProperty(beanProperty, identity));
}
}

private Optional<Object> getCurrentUserIdentityForProperty(BeanProperty<Object, Object> beanProperty) {
return securityService.getAuthentication().flatMap(authentication -> conversionService.convert(authentication, beanProperty.getType()));
}
jeremyg484 marked this conversation as resolved.
Show resolved Hide resolved
}
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;
Loading
Loading