Implement Action Access Control (#2687)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -9,7 +9,14 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.jpa.acm;
|
package org.eclipse.hawkbit.repository.jpa.acm;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import jakarta.persistence.criteria.From;
|
||||||
|
import jakarta.persistence.criteria.Join;
|
||||||
|
import jakarta.persistence.criteria.Root;
|
||||||
|
import jakarta.persistence.metamodel.EntityType;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||||
@@ -21,7 +28,9 @@ import org.eclipse.hawkbit.repository.SoftwareModuleTypeFields;
|
|||||||
import org.eclipse.hawkbit.repository.TagFields;
|
import org.eclipse.hawkbit.repository.TagFields;
|
||||||
import org.eclipse.hawkbit.repository.TargetFields;
|
import org.eclipse.hawkbit.repository.TargetFields;
|
||||||
import org.eclipse.hawkbit.repository.TargetTypeFields;
|
import org.eclipse.hawkbit.repository.TargetTypeFields;
|
||||||
|
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction_;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetType;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
||||||
@@ -31,6 +40,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.enabled", havingValue = "true", matchIfMissing = true)
|
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
@@ -42,11 +52,38 @@ public class DefaultAccessControllerConfiguration {
|
|||||||
return new DefaultAccessController<>(TargetFields.class, SpPermission.TARGET);
|
return new DefaultAccessController<>(TargetFields.class, SpPermission.TARGET);
|
||||||
}
|
}
|
||||||
|
|
||||||
// after adding support for internal action field search support it add matchIfMissing = true
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.action.enabled", havingValue = "true", matchIfMissing = false)
|
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.action.enabled", havingValue = "true", matchIfMissing = true)
|
||||||
AccessController<JpaAction> actionAccessController() {
|
AccessController<JpaAction> actionAccessController(final AccessController<JpaTarget> targetAccessController) {
|
||||||
return new DefaultAccessController<>(ActionFieldsInternal.class, SpPermission.TARGET);
|
return new AccessController<>() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Optional<Specification<JpaAction>> getAccessRules(final Operation operation) {
|
||||||
|
return targetAccessController.getAccessRules(operation).map(targetSpec -> (actionRoot, query, cb) -> {
|
||||||
|
final Join<JpaAction, JpaTarget> targetJoin = actionRoot.join(JpaAction_.target);
|
||||||
|
final EntityType<JpaTarget> targetModel = query.from(JpaTarget.class).getModel();
|
||||||
|
final Root<JpaTarget> targetRoot = (Root<JpaTarget>) Proxy.newProxyInstance(
|
||||||
|
actionRoot.getClass().getClassLoader(),
|
||||||
|
new Class[] { Root.class },
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if (method.getName().equals("getModel") && method.getParameterCount() == 0) {
|
||||||
|
return targetModel;
|
||||||
|
} else if (method.getDeclaringClass().isAssignableFrom(From.class)) {
|
||||||
|
return method.invoke(targetJoin, args);
|
||||||
|
} else {
|
||||||
|
return method.invoke(this, args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return targetSpec.toPredicate(targetRoot, query, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void assertOperationAllowed(final Operation operation, final JpaAction entity) throws InsufficientPermissionException {
|
||||||
|
targetAccessController.assertOperationAllowed(operation, entity.getTarget());
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* 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.repository.jpa.acm;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
|
||||||
|
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
|
||||||
|
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||||
|
import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
|
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||||
|
import org.eclipse.hawkbit.repository.model.Target;
|
||||||
|
import org.eclipse.hawkbit.repository.model.TargetType;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.context.ContextConfiguration;
|
||||||
|
|
||||||
|
@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class })
|
||||||
|
class ActionAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||||
|
|
||||||
|
private TargetType targetType1;
|
||||||
|
private TargetType targetType2;
|
||||||
|
private Target target1;
|
||||||
|
private Target target2;
|
||||||
|
private Action action11;
|
||||||
|
private Action action21;
|
||||||
|
private Action action22;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
targetType1 = testdataFactory.createTargetType("Type1", Set.of());
|
||||||
|
targetType2 = testdataFactory.createTargetType("Type2", Set.of());
|
||||||
|
target1 = testdataFactory.createTarget("controller1", "Controller 1", targetType1);
|
||||||
|
target2 = testdataFactory.createTarget("controller2", "Controller 2", targetType2);
|
||||||
|
final DistributionSet ds = testdataFactory.createDistributionSet();
|
||||||
|
action11 = createAction(target1, ds);
|
||||||
|
action21 = createAction(target2, ds);
|
||||||
|
action22 = createAction(target2, ds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void filterByControllerId() {
|
||||||
|
runAs(withUser("user", READ_TARGET + "/controllerId==" + target1.getControllerId()), () -> {
|
||||||
|
assertThat(deploymentManagement.findAction(action11.getId())).isPresent();
|
||||||
|
assertThat(deploymentManagement.findAction(action21.getId())).isEmpty();
|
||||||
|
assertThat(deploymentManagement.findAction(action22.getId())).isEmpty();
|
||||||
|
assertThat(deploymentManagement.findActionsAll(UNPAGED).getContent()).hasSize(1);
|
||||||
|
});
|
||||||
|
runAs(withUser("user", READ_TARGET + "/controllerId==" + target2.getControllerId()), () -> {
|
||||||
|
assertThat(deploymentManagement.findAction(action11.getId())).isEmpty();
|
||||||
|
assertThat(deploymentManagement.findAction(action21.getId())).isPresent();
|
||||||
|
assertThat(deploymentManagement.findAction(action22.getId())).isPresent();
|
||||||
|
assertThat(deploymentManagement.findActionsAll(UNPAGED).getContent()).hasSize(2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void filterByTargetTypeId() {
|
||||||
|
runAs(withUser("user", READ_TARGET + "/type.id==" + targetType1.getId()), () -> {
|
||||||
|
assertThat(deploymentManagement.findAction(action11.getId())).isPresent();
|
||||||
|
assertThat(deploymentManagement.findAction(action21.getId())).isEmpty();
|
||||||
|
assertThat(deploymentManagement.findAction(action22.getId())).isEmpty();
|
||||||
|
assertThat(deploymentManagement.findActionsAll(UNPAGED).getContent()).hasSize(1);
|
||||||
|
});
|
||||||
|
runAs(withUser("user", READ_TARGET + "/type.id=in=" + targetType2.getId()), () -> {
|
||||||
|
assertThat(deploymentManagement.findAction(action11.getId())).isEmpty();
|
||||||
|
assertThat(deploymentManagement.findAction(action21.getId())).isPresent();
|
||||||
|
assertThat(deploymentManagement.findAction(action22.getId())).isPresent();
|
||||||
|
assertThat(deploymentManagement.findActionsAll(UNPAGED).getContent()).hasSize(2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Action createAction(final Target target, final DistributionSet ds) {
|
||||||
|
final JpaAction generateAction = new JpaAction();
|
||||||
|
generateAction.setActionType(Action.ActionType.FORCED);
|
||||||
|
generateAction.setTarget(target);
|
||||||
|
generateAction.setDistributionSet(ds);
|
||||||
|
generateAction.setStatus(Action.Status.RUNNING);
|
||||||
|
generateAction.setInitiatedBy("DEFAULT");
|
||||||
|
generateAction.setWeight(1000);
|
||||||
|
|
||||||
|
return actionRepository.save(generateAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user