Fine grained repository permissions (#2562)

1. Introduce @PrreAuthorize check based on hasPermission - allowing custom processing (compared with non-modifiable hasAuthority/Role processing)
2. Dedicated permissions could be implemented on management api level. Check is made by plugged in PermissionEvaluator
3. Thus common XXX_REPOSITORY permissions could differ for extending services
4. Change create/update entity builder pattern - not via EntityFactory but via clean static lombok based builders (with fine fluent api).
5. Implement abstract repository management jpa class that handles the boilerplate code from extending classes in single place consistently -> AbsreactJpaRepositoryManagement
6. Register management api-s as **Sevice**-s instead of **Bean**-s in order to make easier maintainable and get away from heavy argument forwading
7. Simplify custom hawkbit repository registration + adding proxy to handle exception mapping at lower level - thus not depending on Aspects for converting exceptions
8. Implemented general purpose 'copy' utility (ObjectCopyUtil) that using getter/setter patterns is able to copy (e.g. Create/Update) objects to other objects (e.g. JPA entity objects)
This commit is contained in:
Avgustin Marinov
2025-07-28 14:57:33 +03:00
committed by GitHub
parent 8cdbe54cbe
commit 2b66449ff1
214 changed files with 3456 additions and 4416 deletions

View File

@@ -11,10 +11,15 @@ package org.eclipse.hawkbit.exception;
import java.io.Serial;
import lombok.EqualsAndHashCode;
import lombok.ToString;
/**
* {@link GenericSpServerException} is thrown when a given entity in's actual and cannot be stored within the current session. Reason could be
* that it has been changed within another session.
*/
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class GenericSpServerException extends AbstractServerRtException {
@Serial

View File

@@ -0,0 +1,251 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.utils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.UnaryOperator;
import jakarta.validation.constraints.NotNull;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class ObjectCopyUtil {
private static final Map<FromTo, CopyFunction> FROM_TO_SETTER = new HashMap<>();
// java:S4276 - it is intended to be generic - not specialized
@SuppressWarnings("java:S4276")
public static boolean copy(final Object from, final Object to, boolean setNullValues, final UnaryOperator<Object> propertyProcessor) {
final FromTo fromTo = new FromTo(from.getClass(), to.getClass());
CopyFunction fromToFunction;
synchronized (FROM_TO_SETTER) {
fromToFunction = FROM_TO_SETTER.get(fromTo);
if (fromToFunction == null) {
fromToFunction = fromToFunction(from.getClass(), to.getClass());
FROM_TO_SETTER.put(fromTo, fromToFunction);
}
}
return fromToFunction.apply(from, to, setNullValues, propertyProcessor);
}
// java:S4276 - it is intended to be generic - not specialized
// java:S3776 - complexity is due to reflection and dynamic method invocation
// java:S1141 - better readable that way
// java:S3011 - low-level, reflection utility. intentionally changes the accessibility
@SuppressWarnings({ "java:S4276", "java:S3776", "java:S1141", "java:S3011" })
private static CopyFunction fromToFunction(final Class<?> fromClass, final Class<?> toClass) {
final List<CopyFunction> propertySetters = new ArrayList<>();
for (final Method fromMethod : getMethods(fromClass)) {
if (fromMethod.getParameterCount() == 0 && fromMethod.getReturnType() != void.class) {
final String methodName = fromMethod.getName();
if (methodName.equals("getClass")) {
continue; // skip
}
final boolean isGet = isGet(methodName);
if (isGet || isIs(fromMethod, methodName)) {
final String fieldName = Character.toLowerCase(methodName.charAt(isGet ? 3 : 2)) + methodName.substring(isGet ? 4 : 3);
fromMethod.setAccessible(true); // if needed
final UnaryOperator<Object> toGetter = toGetter(toClass, fromMethod.getName(), fieldName);
final String setterName = "set" + methodName.substring(isGet ? 3 : 2);
final BiConsumer<Object, Object> toSetter = toSetter(toClass, setterName, fieldName, fromMethod.getReturnType());
if (toSetter == null && toGetter == null) {
// we allow toSetter to be null, but in that case the toGetter must not be null and the
// from value shall always match the to value (without setting it)
throw new IllegalStateException("Setter counterpart for " + fromMethod + " is not found in " + toClass.getName());
}
propertySetters.add((from, to, setNullValues, propertyProcessor) -> {
final Object value;
try {
value = fromMethod.invoke(from);
} catch (final IllegalAccessException e) {
throw new IllegalStateException("Failed to get source value", e);
} catch (final InvocationTargetException e) {
throw new IllegalStateException(
"Failed to get source value",
e.getTargetException() == null ? e : e.getTargetException());
}
if (value == null && !setNullValues) { // if !setNullValues null means no change
return false;
}
if (toGetter != null) {
final Object currentValue = toGetter.apply(to);
if (Objects.equals(value, currentValue)) {
return false; // no change
}
}
if (toSetter == null) {
throw new IllegalStateException(
"Setter counterpart for " + fromMethod + " is not found in " + toClass.getName() +
" and the 'from' value is not equal to the 'to' value");
}
toSetter.accept(to, propertyProcessor.apply(value));
return true;
});
}
}
}
return (from, to, setNullValues, entityManager) -> {
boolean updated = false;
for (final CopyFunction fieldSetter : propertySetters) {
updated = fieldSetter.apply(from, to, setNullValues, entityManager) || updated;
}
return updated;
};
}
// java:S3011 - low-level, reflection utility. intentionally changes the accessibility
@SuppressWarnings("java:S3011")
private static BiConsumer<Object, Object> toSetter(
final Class<?> toClass, final String setterName, final String fieldName, final Class<?> type) {
try {
final Method toSetterMethod = getMethod(toClass, setterName, type);
return (to, value) -> {
try {
toSetterMethod.invoke(to, value);
} catch (final InvocationTargetException e) {
throw new IllegalStateException(
"Error invoking " + toSetterMethod,
e.getTargetException() == null ? e : e.getTargetException());
} catch (final IllegalAccessException | IllegalArgumentException e) {
throw new IllegalStateException("Error invoking " + toSetterMethod, e);
}
};
} catch (final NoSuchMethodException nsme) {
final Field field = getField(toClass, fieldName);
if (field == null) {
return null;
} else {
return (to, value) -> {
try {
field.set(to, value);
} catch (final IllegalAccessException | IllegalArgumentException e) {
throw new IllegalStateException("Error setting field " + field, e);
}
};
}
}
}
private static UnaryOperator<Object> toGetter(final Class<?> toClass, final String getterName, final String fieldName) {
try {
final Method toGetterMethod = getMethod(toClass, getterName);
return to -> {
try {
return toGetterMethod.invoke(to);
} catch (final Exception e) {
throw new IllegalStateException("Error invoking " + toGetterMethod, e);
}
};
} catch (final NoSuchMethodException e) {
// no method to get current value of the target field, try with field access
final Field field = getField(toClass, fieldName); // is or get
return field == null ? null : to -> {
try {
return field.get(to);
} catch (final Exception e2) {
throw new IllegalStateException("Error getting field " + field, e2);
}
};
}
}
private static boolean isIs(final Method fromMethod, final String methodName) {
return methodName.startsWith("is") && methodName.length() > 2 && Character.isUpperCase(methodName.charAt(2)) &&
fromMethod.getReturnType() == boolean.class;
}
private static boolean isGet(final String methodName) {
return methodName.startsWith("get") && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3));
}
@SuppressWarnings("java:S3011") // low-level, reflection utility. intentionally changes the accessibility
private static Field getField(final Class<?> clazz, final String fieldName) {
for (final Field field : clazz.getDeclaredFields()) {
if (fieldName.equals(field.getName())) {
field.setAccessible(true);
return field;
}
}
final Class<?> superClass = clazz.getSuperclass();
if (superClass == null) {
return null;
} else {
return getField(superClass, fieldName);
}
}
@NotNull
private static Method getMethod(final Class<?> clazz, final String methodName, final Class<?>... parameterTypes)
throws NoSuchMethodException {
try {
return getMethod(clazz, clazz, methodName, parameterTypes);
} catch (final NoSuchMethodException e) {
if (parameterTypes.length == 1 && parameterTypes[0] == Boolean.class) {
try {
return getMethod(clazz, methodName, boolean.class);
} catch (final NoSuchMethodException e2) {
throw e;
}
}
throw e;
}
}
@SuppressWarnings("java:S3011") // low-level, reflection utility. intentionally changes the accessibility
private static Method getMethod(final Class<?> target, final Class<?> clazz, final String methodName, final Class<?>... parameterTypes)
throws NoSuchMethodException {
try {
final Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
method.setAccessible(true);
return method;
} catch (final NoSuchMethodException e) {
final Class<?> superClass = clazz.getSuperclass();
if (superClass == null) {
throw new NoSuchMethodException("Method " + methodName + " not found in " + target.getSimpleName());
} else {
return getMethod(target, superClass, methodName, parameterTypes);
}
}
}
private static List<Method> getMethods(final Class<?> clazz) {
final List<Method> methods = new ArrayList<>();
for (final Method method : clazz.getDeclaredMethods()) {
if (!method.isSynthetic() && !method.isBridge()) {
methods.add(method);
}
}
final Class<?> superClass = clazz.getSuperclass();
if (superClass != null) {
methods.addAll(getMethods(superClass));
}
return methods;
}
// key for copy of class to class function
private record FromTo(Class<?> from, Class<?> to) {}
// functional interface to apply the copy operation
private interface CopyFunction {
boolean apply(Object from, Object to, boolean setNullValues, UnaryOperator<Object> propertyProcessor);
}
}