Add distribution set and target type fine grained permissions (#2545)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-14 14:52:36 +03:00
committed by GitHub
parent c3fdd9fcc8
commit e7373275bf
53 changed files with 506 additions and 476 deletions

View File

@@ -18,7 +18,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.ContextAware;
@@ -27,11 +26,8 @@ import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ObjectUtils;
@@ -41,10 +37,7 @@ public class DefaultAccessController<A extends Enum<A> & RsqlQueryField, T> impl
private final Class<A> rsqlQueryFieldType;
private final Map<Operation, List<String>> permissions = new EnumMap<>(Operation.class);
@Value("${hawkbit.jpa.security.default-access-controller.strict:false}")
private boolean strict;
private ContextAware contextAware;
private RoleHierarchy roleHierarchy;
public DefaultAccessController(final Class<A> rsqlQueryFieldType, final String... permissionTypes) {
if (ObjectUtils.isEmpty(permissionTypes)) {
@@ -64,11 +57,6 @@ public class DefaultAccessController<A extends Enum<A> & RsqlQueryField, T> impl
this.contextAware = contextAware;
}
@Autowired(required = false)
void setRoleHierarchy(final RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
@Override
public Optional<Specification<T>> getAccessRules(final Operation operation) {
if (contextAware.getCurrentTenant() != null && SYSTEM_USER.equals(contextAware.getCurrentUsername())) {
@@ -104,43 +92,26 @@ public class DefaultAccessController<A extends Enum<A> & RsqlQueryField, T> impl
// returns null if ALL entities are accessible, otherwise returns a list of scopes
// throws InsufficientPermissionException if no matching authority found (should not happen - should be already checked with @PreAuthorize)
// java:S1168 - returns null with purpose to indicate no scopes, privately used with attention
// java:S1168 - better readable at one place
@SuppressWarnings({ "java:S1168", "java:S1168" })
@SuppressWarnings("java:S1168") // java:S1168 - returns null with purpose to indicate no scopes, privately used with attention
private List<String> getScopes(final Operation operation) {
final List<String> operationPermissions = permissions.get(operation);
final List<String> scopes = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.map(Permission::from)
.flatMap(permission -> roleHierarchy == null
? (operationPermissions.contains(permission.name()) ? Stream.of(permission) : Stream.empty())
: roleHierarchy.getReachableGrantedAuthorities(List.of(new SimpleGrantedAuthority(permission.name())))
.stream()
.map(GrantedAuthority::getAuthority)
.filter(operationPermissions::contains)
.map(reachableAuthority -> new Permission(reachableAuthority, permission.scope())))
.filter(permission -> operationPermissions.contains(permission.name()))
.map(Permission::scope)
.distinct() // remove duplicates
.toList();
if (scopes.isEmpty()) {
// no matching authority found for the operation
// the needed permission should have already been checked with @PreAuthorize
// could happen, for instance, in controller management, that checks ROLE_CONTROLLER and on its behalf
// calls pure repository methods as privileged
if (strict) {
throw new InsufficientPermissionException(
String.format(
"No matching authority found for operation %s" +
" (expects %s, should not happen - shall have already been checked with @PreAuthorize)",
operation, operationPermissions));
} else {
// TODO - maybe in some future we could adapt permissions so controller roles to somehow apply what is needed
// and to do not "assume" and to throw exception always
log.debug(
"[{}] No matching authority found for operation {} (expects {}), they shall have already been checked with @PreAuthorize)",
rsqlQueryFieldType, operation, operationPermissions);
return null;
}
// no matching permission scope found for the operation
// the required for the method permissions should have already been checked with @PreAuthorize
// however it could happen that there is no entity permission, e.g.:
// * in controller management, that checks ROLE_CONTROLLER and on its behalf calls pure repository methods as privileged
// * in case the entity permission(s) are implied - e.g. there is READ_REPOSITORY which implies READ_DISTRIBUTION_SET
log.debug(
"[{}] No matching authority found for operation {} (expects {}), they shall have already been checked with @PreAuthorize)",
rsqlQueryFieldType, operation, operationPermissions);
return null;
} else if (scopes.contains(null)) {
return null; // not scoped at all
} else {

View File

@@ -32,12 +32,12 @@ public class DefaultAccessControllerConfiguration {
@Bean
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.target-type.enabled", havingValue = "true", matchIfMissing = true)
AccessController<JpaTargetType> targetTypeAccessController() {
return new DefaultAccessController<>(TargetTypeFields.class, "TARGET", "TARGET_TYPE");
return new DefaultAccessController<>(TargetTypeFields.class, "TARGET_TYPE");
}
@Bean
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.distribution-set.enabled", havingValue = "true", matchIfMissing = true)
AccessController<JpaDistributionSet> distributionSetAccessController() {
return new DefaultAccessController<>(DistributionSetFields.class, "REPOSITORY", "DISTRIBUTION_SET");
return new DefaultAccessController<>(DistributionSetFields.class, "DISTRIBUTION_SET");
}
}

View File

@@ -29,6 +29,7 @@ import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.im.authentication.SpPermission;
import org.eclipse.hawkbit.im.authentication.SpRole;
import org.eclipse.hawkbit.repository.DistributionSetManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryProperties;
@@ -200,7 +201,7 @@ public class JpaRolloutManagement implements RolloutManagement {
// scheduled rollout, the creator shall have permissions to start rollout
if (rolloutRequest.getStartAt() != null && rolloutRequest.getStartAt() != Long.MAX_VALUE && // if scheduled rollout
!systemSecurityContext.hasPermission(SpPermission.HANDLE_ROLLOUT) &&
!systemSecurityContext.hasPermission(SpPermission.SpringEvalExpressions.SYSTEM_ROLE)) {
!systemSecurityContext.hasPermission(SpRole.SYSTEM_ROLE)) {
throw new InsufficientPermissionException("You need permission to start rollouts to create a scheduled rollout");
}
if (dynamicRolloutGroupTemplate != null && !rolloutRequest.isDynamic()) {