Add DistributionSetManagementTest and TargetTypeQueryManagementTest (#2727)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
@@ -35,6 +36,8 @@ abstract class AbstractAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
protected DistributionSetType dsType1;
|
||||
protected DistributionSetType dsType2;
|
||||
protected DistributionSetTag dsTag1;
|
||||
protected DistributionSetTag dsTag2;
|
||||
protected DistributionSet ds1Type1;
|
||||
protected DistributionSet ds2Type2;
|
||||
protected DistributionSet ds3Type2;
|
||||
@@ -58,12 +61,21 @@ abstract class AbstractAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
dsType1 = testdataFactory.findOrCreateDistributionSetType("DsType1", "DistributionSetType-1", List.of(smType1), List.of());
|
||||
dsType2 = testdataFactory.findOrCreateDistributionSetType("DsType2", "DistributionSetType-2", List.of(smType2), List.of(smType1));
|
||||
final List<DistributionSetTag> tags = testdataFactory.createDistributionSetTags(2);
|
||||
dsTag1 = tags.get(0);
|
||||
dsTag2 = tags.get(1);
|
||||
ds1Type1 = distributionSetManagement.lock(
|
||||
testdataFactory.createDistributionSet("Ds1Type1", "1.0", dsType1, List.of(sm1Type1)));
|
||||
distributionSetManagement.assignTag(
|
||||
List.of(testdataFactory.createDistributionSet("Ds1Type1", "1.0", dsType1, List.of(sm1Type1)).getId()),
|
||||
dsTag1.getId()).get(0));
|
||||
ds2Type2 = distributionSetManagement.lock(
|
||||
testdataFactory.createDistributionSet("Ds2Type2", "1.0", dsType2, List.of(sm2Type2, sm1Type1)));
|
||||
distributionSetManagement.assignTag(
|
||||
List.of(testdataFactory.createDistributionSet("Ds2Type2", "2.0", dsType2, List.of(sm2Type2, sm1Type1)).getId()),
|
||||
dsTag1.getId()).get(0));
|
||||
ds3Type2 = distributionSetManagement.lock(
|
||||
testdataFactory.createDistributionSet("Ds3Type2", "1.0", dsType2, List.of(sm3Type2, sm1Type1)));
|
||||
distributionSetManagement.assignTag(
|
||||
List.of(testdataFactory.createDistributionSet("Ds3Type2", "3.0", dsType2, List.of(sm3Type2, sm1Type1)).getId()),
|
||||
dsTag2.getId()).get(0));
|
||||
|
||||
targetType1 = testdataFactory.createTargetType("TargetType1", Set.of(dsType1, dsType2));
|
||||
targetType2 = testdataFactory.createTargetType("TargetType2", Set.of(dsType2));
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
/**
|
||||
* 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.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
|
||||
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
|
||||
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.hawkbit.repository.DistributionSetTagManagement;
|
||||
import org.eclipse.hawkbit.repository.Identifiable;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.AutoAssignDistributionSetUpdate;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
/**
|
||||
* Note: Still all test gets READ_REPOSITORY since find methods are inherited with request for READ_REPOSITORY. However,
|
||||
* using READ_DISTRIBUTION_SET scoping - the scopes still work.
|
||||
* <p/>
|
||||
* Feature: Component Tests - Access Control<br/>
|
||||
* Story: Test Distribution Set Access Controller
|
||||
*/
|
||||
@TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true")
|
||||
class DistributionSetAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
/**
|
||||
* Verifies read access rules for distribution sets
|
||||
*/
|
||||
@Test
|
||||
void verifyDistributionSetReadOperations() {
|
||||
final DistributionSet permitted = testdataFactory.createDistributionSet();
|
||||
final DistributionSet hidden = testdataFactory.createDistributionSet();
|
||||
|
||||
final Action permittedAction = testdataFactory.performAssignment(permitted);
|
||||
|
||||
runAs(withUser("user",
|
||||
READ_DISTRIBUTION_SET + "/id==" + permitted.getId(),
|
||||
READ_TARGET +"/controllerId==" + permittedAction.getTarget().getControllerId()), () -> {
|
||||
final Long permittedActionId = permitted.getId();
|
||||
|
||||
// verify distributionSetManagement#findAll
|
||||
assertThat(distributionSetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
|
||||
.containsOnly(permittedActionId);
|
||||
|
||||
// verify distributionSetManagement#findByRsql
|
||||
assertThat(distributionSetManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId)
|
||||
.toList()).containsOnly(permittedActionId);
|
||||
|
||||
// verify distributionSetManagement#get
|
||||
assertThat(distributionSetManagement.find(permittedActionId)).isPresent();
|
||||
final Long hiddenId = hidden.getId();
|
||||
assertThat(distributionSetManagement.find(hiddenId)).isEmpty();
|
||||
|
||||
// verify distributionSetManagement#getWithDetails
|
||||
assertThat(distributionSetManagement.getWithDetails(permittedActionId)).isNotNull();
|
||||
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> distributionSetManagement.getWithDetails(hiddenId));
|
||||
|
||||
// verify distributionSetManagement#get
|
||||
final List<Long> allActionIds = Arrays.asList(permittedActionId, hiddenId);
|
||||
assertThatThrownBy(() -> distributionSetManagement.get(allActionIds))
|
||||
.as("Fail if request hidden.").isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
// verify distributionSetManagement#getByNameAndVersion
|
||||
assertThat(distributionSetManagement.findByNameAndVersion(permitted.getName(), permitted.getVersion())).isNotNull();
|
||||
final String hiddenName = hidden.getName();
|
||||
final String hiddenVersion = hidden.getVersion();
|
||||
assertThatExceptionOfType(EntityNotFoundException.class)
|
||||
.isThrownBy(() -> distributionSetManagement.findByNameAndVersion(hiddenName, hiddenVersion));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies read access rules for distribution sets
|
||||
*/
|
||||
@Test
|
||||
void verifyDistributionSetUpdates() {
|
||||
final DistributionSet permitted = testdataFactory.createDistributionSet();
|
||||
final String mdPresetKey = "metadata.preset";
|
||||
final String mdPresetValue = "presetValue";
|
||||
distributionSetManagement.createMetadata(permitted.getId(), Map.of(mdPresetKey, mdPresetValue));
|
||||
final DistributionSet readOnly = testdataFactory.createDistributionSet();
|
||||
distributionSetManagement.createMetadata(readOnly.getId(), Map.of(mdPresetKey, mdPresetValue));
|
||||
final DistributionSet hidden = testdataFactory.createDistributionSet();
|
||||
distributionSetManagement.createMetadata(hidden.getId(), Map.of(mdPresetKey, mdPresetValue));
|
||||
|
||||
final SoftwareModule swModule = testdataFactory.createSoftwareModuleOs();
|
||||
|
||||
runAs(withUser("user",
|
||||
READ_DISTRIBUTION_SET + "/id==" + permitted.getId() + " or id==" + readOnly.getId(),
|
||||
UPDATE_DISTRIBUTION_SET + "/id==" + permitted.getId()), () -> {
|
||||
// verify distributionSetManagement#assignSoftwareModules
|
||||
final List<Long> singleModuleIdList = Collections.singletonList(swModule.getId());
|
||||
assertThat(distributionSetManagement.assignSoftwareModules(permitted.getId(), singleModuleIdList))
|
||||
.satisfies(ds -> assertThat(ds.getModules().stream().map(Identifiable::getId).toList()).contains(swModule.getId()));
|
||||
final Long readOnlyId = readOnly.getId();
|
||||
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(readOnlyId, singleModuleIdList))
|
||||
.as("Distribution set not allowed to me modified.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
final Long hiddenId = hidden.getId();
|
||||
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(hiddenId, singleModuleIdList))
|
||||
.as("Distribution set should not be visible.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
final Map<String, String> metadata = Map.of("test.create", mdPresetValue);
|
||||
|
||||
// verify distributionSetManagement#createMetaData
|
||||
distributionSetManagement.createMetadata(permitted.getId(), metadata);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, metadata))
|
||||
.as("Distribution set not allowed to be modified.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, metadata))
|
||||
.as("Distribution set should not be visible.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
// verify distributionSetManagement#updateMetaData
|
||||
final String newValue = "newValue";
|
||||
distributionSetManagement.createMetadata(permitted.getId(), mdPresetKey, newValue);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, mdPresetKey, newValue))
|
||||
.as("Distribution set not allowed to me modified.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, mdPresetKey, newValue))
|
||||
.as("Distribution set should not be visible.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
// verify distributionSetManagement#deleteMetaData
|
||||
final String metadataKey = metadata.entrySet().stream().findAny().get().getKey();
|
||||
distributionSetManagement.deleteMetadata(permitted.getId(), metadataKey);
|
||||
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(readOnlyId, mdPresetKey))
|
||||
.as("Distribution set not allowed to me modified.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(hiddenId, mdPresetKey))
|
||||
.as("Distribution set should not be visible.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyTagFilteringAndManagement() {
|
||||
final DistributionSet permitted = testdataFactory.createDistributionSet();
|
||||
final DistributionSet readOnly = testdataFactory.createDistributionSet();
|
||||
final DistributionSet hidden = testdataFactory.createDistributionSet();
|
||||
final Long dsTagId = distributionSetTagManagement.create(
|
||||
DistributionSetTagManagement.Create.builder().name("dsTag").build()).getId();
|
||||
final Long dsTag2Id = distributionSetTagManagement.create(
|
||||
DistributionSetTagManagement.Create.builder().name("dsTag2").build()).getId();
|
||||
|
||||
// perform tag assignment before setting access rules
|
||||
distributionSetManagement.assignTag(Arrays.asList(permitted.getId(), readOnly.getId(), hidden.getId()), dsTagId);
|
||||
|
||||
runAs(withUser("user",
|
||||
READ_DISTRIBUTION_SET + "/id==" + permitted.getId() + " or id==" + readOnly.getId(),
|
||||
UPDATE_DISTRIBUTION_SET + "/id==" + permitted.getId()), () -> {
|
||||
assertThat(distributionSetManagement.findByTag(dsTagId, Pageable.unpaged()).get().map(Identifiable::getId)
|
||||
.toList()).containsOnly(permitted.getId(), readOnly.getId());
|
||||
|
||||
assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTagId, Pageable.unpaged()).get()
|
||||
.map(Identifiable::getId).toList()).containsOnly(permitted.getId(), readOnly.getId());
|
||||
|
||||
// verify distributionSetManagement#unassignTag on permitted target
|
||||
assertThat(distributionSetManagement
|
||||
.unassignTag(Collections.singletonList(permitted.getId()), dsTagId))
|
||||
.size()
|
||||
.isEqualTo(1);
|
||||
// verify distributionSetManagement#assignTag on permitted target
|
||||
assertThat(distributionSetManagement.assignTag(Collections.singletonList(permitted.getId()), dsTagId))
|
||||
.hasSize(1);
|
||||
// verify distributionSetManagement#unAssignTag on permitted target
|
||||
assertThat(distributionSetManagement.unassignTag(List.of(permitted.getId()), dsTagId)
|
||||
.get(0).getId())
|
||||
.isEqualTo(permitted.getId());
|
||||
|
||||
// assignment is denied for readOnlyTarget (read, but no update permissions)
|
||||
final List<Long> readOblyList = Collections.singletonList(readOnly.getId());
|
||||
assertThatThrownBy(() ->
|
||||
distributionSetManagement.unassignTag(readOblyList, dsTagId))
|
||||
.as("Missing update permissions for target to toggle tag assignment.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
// assignment is denied for readOnlyTarget (read, but no update permissions)
|
||||
// dsTag2- since - it is tagged with dsTag and won't do anything if assigning dsTag
|
||||
assertThatThrownBy(() -> distributionSetManagement.assignTag(readOblyList, dsTag2Id))
|
||||
.as("Missing update permissions for target to toggle tag assignment.")
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
// assignment is denied for hiddenTarget since it's hidden
|
||||
final List<Long> hiddenList = Collections.singletonList(hidden.getId());
|
||||
assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenList, dsTagId))
|
||||
.as("Missing update permissions for target to toggle tag assignment.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
// assignment is denied for hiddenTarget since it's hidden
|
||||
assertThatThrownBy(() -> distributionSetManagement.assignTag(hiddenList, dsTagId))
|
||||
.as("Missing update permissions for target to toggle tag assignment.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
// assignment is denied for hiddenTarget since it's hidden
|
||||
final List<Long> hiddenIdList = List.of(hidden.getId());
|
||||
assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenIdList, dsTagId))
|
||||
.as("Missing update permissions for target to toggle tag assignment.")
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyAutoAssignmentUsage() {
|
||||
final DistributionSet permitted = testdataFactory.createDistributionSet();
|
||||
final DistributionSet readOnly = testdataFactory.createDistributionSet();
|
||||
final DistributionSet hidden = testdataFactory.createDistributionSet();
|
||||
// has to lock them, otherwise implicit lock shall be made which require DistributionSet update permissions
|
||||
distributionSetManagement.lock(permitted);
|
||||
distributionSetManagement.lock(readOnly);
|
||||
distributionSetManagement.lock(hidden);
|
||||
|
||||
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
|
||||
.create(TargetFilterQueryManagement.Create.builder().name("test").query("id==*").build());
|
||||
|
||||
runAs(withUser("user",
|
||||
READ_DISTRIBUTION_SET + "/id==" + permitted.getId() + " or id==" + readOnly.getId(),
|
||||
UPDATE_DISTRIBUTION_SET + "/id==" + permitted.getId(),
|
||||
// read / update target needed to update target filter query
|
||||
READ_TARGET, UPDATE_TARGET), () -> {
|
||||
assertThat(targetFilterQueryManagement
|
||||
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(permitted.getId())
|
||||
.actionType(Action.ActionType.FORCED).confirmationRequired(false))
|
||||
.getAutoAssignDistributionSet().getId()).isEqualTo(permitted.getId());
|
||||
targetFilterQueryManagement
|
||||
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
|
||||
.ds(readOnly.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false))
|
||||
.getAutoAssignDistributionSet().getId();
|
||||
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate =
|
||||
new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
|
||||
.ds(hidden.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false);
|
||||
assertThatThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate))
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* 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.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_PREFIX;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetManagement.Create;
|
||||
import org.eclipse.hawkbit.repository.Identifiable;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.NamedEntity;
|
||||
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DistributionSetManagementTest extends AbstractAccessControllerTest {
|
||||
|
||||
@Test
|
||||
void verifyCreate() {
|
||||
// permissions to read all and create only type1 ds
|
||||
runAs(withAuthorities(READ_DISTRIBUTION_SET, CREATE_PREFIX + DISTRIBUTION_SET + "/type.id==" + dsType1.getId()), () -> {
|
||||
final Create dsType1Create = Create.builder().type(dsType1).name(randomString(16)).version("1.0").build();
|
||||
final Create dsType2Create = Create.builder().type(dsType2).name(randomString(16)).version("1.0").build();
|
||||
|
||||
assertThat(distributionSetManagement.create(dsType1Create)).isNotNull();
|
||||
assertThatThrownBy(() -> distributionSetManagement.create(dsType2Create)).isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
final List<Create> dsType1CreateList = List.of(
|
||||
Create.builder().type(dsType1).name(randomString(16)).version("1.0").build(),
|
||||
Create.builder().type(dsType1).name(randomString(16)).version("1.0").build());
|
||||
final List<Create> dsType2CreateList = List.of(
|
||||
Create.builder().type(dsType2).name(randomString(16)).version("1.0").build(),
|
||||
Create.builder().type(dsType2).name(randomString(16)).version("1.0").build());
|
||||
assertThat(distributionSetManagement.create(dsType1CreateList)).hasSize(2);
|
||||
assertThatThrownBy(() -> distributionSetManagement.create(dsType2CreateList)).isInstanceOf(InsufficientPermissionException.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyRead() {
|
||||
// permissions to read only type1 ds
|
||||
runAs(withAuthorities(READ_DISTRIBUTION_SET + "/type.id==" + dsType1.getId()), () -> {
|
||||
Assertions.<DistributionSet> assertThat(distributionSetManagement.findAll(UNPAGED)).hasSize(1).containsExactlyInAnyOrder(ds1Type1);
|
||||
assertThat(distributionSetManagement.count()).isEqualTo(1);
|
||||
|
||||
Assertions.<DistributionSet> assertThat(distributionSetManagement.findByRsql("name==*", UNPAGED)).hasSize(1).hasSize(1)
|
||||
.containsExactly(ds1Type1);
|
||||
assertThat(distributionSetManagement.countByRsql("name==*")).isEqualTo(1);
|
||||
|
||||
Assertions.<DistributionSet> assertThat(distributionSetManagement.findByTag(dsTag1.getId(), UNPAGED)).hasSize(1)
|
||||
.containsExactly(ds1Type1);
|
||||
assertThat(distributionSetManagement.findByTag(dsTag2.getId(), UNPAGED)).isEmpty();
|
||||
|
||||
Assertions.<DistributionSet> assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTag1.getId(), UNPAGED)).hasSize(1)
|
||||
.containsExactly(ds1Type1);
|
||||
assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTag2.getId(), UNPAGED)).isEmpty();
|
||||
|
||||
// perform distributionSetManagement#get and verify
|
||||
final var ds1Type1Id = ds1Type1.getId();
|
||||
final Long ds2Type2Id = ds2Type2.getId();
|
||||
assertThat(distributionSetManagement.get(ds1Type1Id)).isEqualTo(ds1Type1);
|
||||
assertThat(distributionSetManagement.getWithDetails(ds1Type1Id)).isEqualTo(ds1Type1);
|
||||
assertThat(distributionSetManagement.find(ds1Type1Id).map(DistributionSet.class::cast)).hasValue(ds1Type1);
|
||||
assertThatThrownBy(() -> distributionSetManagement.get(ds2Type2Id)).isInstanceOf(EntityNotFoundException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.getWithDetails(ds2Type2Id)).isInstanceOf(EntityNotFoundException.class);
|
||||
assertThat(distributionSetManagement.find(ds2Type2Id)).isEmpty();
|
||||
|
||||
Assertions.<DistributionSet> assertThat(distributionSetManagement.get(List.of(ds1Type1Id))).hasSize(1).contains(ds1Type1);
|
||||
final List<Long> noPermissionsTestDataDsIdList = List.of(ds2Type2Id);
|
||||
assertThatThrownBy(() -> distributionSetManagement.get(noPermissionsTestDataDsIdList)).isInstanceOf(EntityNotFoundException.class);
|
||||
final List<Long> containingNoPermissionTestDataDsIsList = List.of(ds1Type1Id, ds2Type2Id);
|
||||
assertThatThrownBy(() -> distributionSetManagement.get(containingNoPermissionTestDataDsIsList))
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
// perform distributionSetManagement#getByNameAndVersion and verify
|
||||
assertThat(distributionSetManagement.findByNameAndVersion(ds1Type1.getName(), ds1Type1.getVersion())).isEqualTo(ds1Type1);
|
||||
final String ds2Type2Name = ds2Type2.getName();
|
||||
final String ds2Type2Version = ds2Type2.getVersion();
|
||||
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(
|
||||
() -> distributionSetManagement.findByNameAndVersion(ds2Type2Name, ds2Type2Version));
|
||||
|
||||
assertThat(distributionSetManagement.getValidAndComplete(ds1Type1Id)).isEqualTo(ds1Type1);
|
||||
assertThatThrownBy(() -> distributionSetManagement.getValidAndComplete(ds2Type2Id)).isInstanceOf(EntityNotFoundException.class);
|
||||
|
||||
assertThat(distributionSetManagement.getMetadata(ds1Type1Id)).isEmpty();
|
||||
assertThatThrownBy(() -> distributionSetManagement.getMetadata(ds2Type2Id)).isInstanceOf(EntityNotFoundException.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyUpdate() {
|
||||
final String ds2MdKey = "ds2MdKey";
|
||||
distributionSetManagement.createMetadata(ds2Type2.getId(), ds2MdKey, "ds2MdValue");
|
||||
// override with updated
|
||||
final DistributionSet ds2Type2 = distributionSetManagement.get(this.ds2Type2.getId());
|
||||
// permissions to read all and update only type1 ds
|
||||
runAs(withAuthorities(READ_DISTRIBUTION_SET, UPDATE_DISTRIBUTION_SET + "/type.id==" + dsType1.getId()), () -> {
|
||||
assertThat(distributionSetManagement.assignTag(List.of(ds1Type1.getId()), dsTag2.getId())).hasSize(1);
|
||||
final List<Long> noPermissionsTestDataDsId = List.of(ds3Type2.getId());
|
||||
final Long tagToAssignId = dsTag1.getId();
|
||||
assertThatThrownBy(() -> distributionSetManagement.assignTag(noPermissionsTestDataDsId, tagToAssignId))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
assertThat(distributionSetManagement.unassignTag(List.of(ds1Type1.getId()), dsTag2.getId())).hasSize(1);
|
||||
final Long tagToRemoveId = dsTag2.getId();
|
||||
assertThatThrownBy(() -> distributionSetManagement.unassignTag(noPermissionsTestDataDsId, tagToRemoveId))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
final String metadataKey = "key.dot";
|
||||
final Map<String, String> metaData = Map.of(metadataKey, "value.dot");
|
||||
|
||||
final Long ds2Type2Id = ds2Type2.getId();
|
||||
distributionSetManagement.createMetadata(ds1Type1.getId(), metaData);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(ds2Type2Id, metaData))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
final String metadataNewValue = "newValue.dot";
|
||||
distributionSetManagement.createMetadata(ds1Type1.getId(), metadataKey, metadataNewValue);
|
||||
assertThatThrownBy(() -> distributionSetManagement.createMetadata(ds2Type2Id, metadataKey, metadataNewValue))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
distributionSetManagement.deleteMetadata(ds1Type1.getId(), metadataKey);
|
||||
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(ds2Type2Id, metadataKey))
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(ds2Type2Id, ds2MdKey))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
final DistributionSetManagement.Update distributionSet1Update = DistributionSetManagement.Update.builder()
|
||||
.id(ds1Type1.getId()).description(randomString(16)).build();
|
||||
final DistributionSetManagement.Update distributionSet2Update = DistributionSetManagement.Update.builder()
|
||||
.id(ds2Type2Id).description(randomString(16)).build();
|
||||
DistributionSet ds1Type1 = distributionSetManagement.update(distributionSet1Update);
|
||||
assertThat(ds1Type1).extracting(NamedEntity::getDescription).isEqualTo(distributionSet1Update.getDescription());
|
||||
assertThatThrownBy(() -> distributionSetManagement.update(distributionSet2Update))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
distributionSetManagement.unlock(ds1Type1);
|
||||
ds1Type1 = distributionSetManagement.unassignSoftwareModule(ds1Type1.getId(), sm1Type1.getId());
|
||||
assertThat(ds1Type1).matches(
|
||||
updated -> updated.getModules().isEmpty());
|
||||
ds1Type1 = distributionSetManagement.assignSoftwareModules(ds1Type1.getId(), List.of(sm1Type1.getId()));
|
||||
assertThat(ds1Type1).matches(updated -> updated.getModules().stream()
|
||||
.map(Identifiable::getId).anyMatch(sm1Type1.getId()::equals));
|
||||
try {
|
||||
SecurityContextSwitch.callAsPrivileged(() -> distributionSetManagement.unlock(ds2Type2));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
final Long softwareModuleAppId = sm1Type1.getId();
|
||||
final List<Long> softwareModuleAppIdList = List.of(softwareModuleAppId);
|
||||
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(ds2Type2Id, softwareModuleAppIdList))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
assertThatThrownBy(() -> distributionSetManagement.unassignSoftwareModule(ds2Type2Id, softwareModuleAppId))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
distributionSetManagement.invalidate(ds1Type1);
|
||||
assertThatThrownBy(() -> distributionSetManagement.invalidate(ds2Type2)).isInstanceOf(InsufficientPermissionException.class);
|
||||
});
|
||||
}
|
||||
//
|
||||
// private TestData createCompleteDistributionSetWithNewType() {
|
||||
// // create type
|
||||
// final DistributionSetType distributionSetType = testdataFactory.findOrCreateDistributionSetType(randomString(16), randomString(16),
|
||||
// List.of(osType), List.of(appType));
|
||||
//
|
||||
// final DistributionSetTag distributionSetTag = distributionSetTagManagement.create(
|
||||
// DistributionSetTagManagement.Create.builder().name(randomString(16)).build());
|
||||
//
|
||||
// final SoftwareModule moduleOs = testdataFactory.createSoftwareModuleOs();
|
||||
//
|
||||
// final Long distributionSetId = distributionSetManagement.create(
|
||||
// Create.builder().type(distributionSetType).name(randomString(16)).version("1.0")
|
||||
// .modules(Set.of(moduleOs)).build()).getId();
|
||||
//
|
||||
// final DistributionSetAssignmentResult assignmentResult = assignDistributionSet(distributionSetId, createTarget().getControllerId());
|
||||
// assertThat(assignmentResult.getAssignedEntity()).hasSize(1);
|
||||
//
|
||||
// return new TestData(distributionSetType,
|
||||
// distributionSetManagement.assignTag(Collections.singletonList(distributionSetId), distributionSetTag.getId()).get(0),
|
||||
// distributionSetTag, assignmentResult.getAssignedEntity().get(0));
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_PREFIX;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_PREFIX;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.DISTRIBUTION_SET_TYPE;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_PREFIX;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_PREFIX;
|
||||
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.NamedEntity;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class DistributionSetTypeManagementTest extends AbstractAccessControllerTest {
|
||||
|
||||
@Test
|
||||
void verifyCreate() {
|
||||
// permissions to read only type1 ds types and create new type with name 'permitted'
|
||||
runAs(withAuthorities(READ_PREFIX + DISTRIBUTION_SET_TYPE, CREATE_PREFIX + DISTRIBUTION_SET_TYPE + "/name==permitted"), () -> {
|
||||
assertThat(testdataFactory.findOrCreateDistributionSetType("newType", "permitted")).isNotNull();
|
||||
assertThatThrownBy(() -> testdataFactory.findOrCreateDistributionSetType("newType_2", "not_permitted_2"))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyRead() {
|
||||
// permissions to read only type1 ds types
|
||||
runAs(withAuthorities(READ_PREFIX + DISTRIBUTION_SET_TYPE + "/id==" + dsType1.getId()), () -> {
|
||||
// perform distributionSetTypeManagement#findAll and verify
|
||||
Assertions.<DistributionSetType> assertThat(distributionSetTypeManagement.findAll(UNPAGED))
|
||||
.hasSize(1).containsExactly(dsType1);
|
||||
assertThat(distributionSetTypeManagement.count()).isEqualTo(1);
|
||||
|
||||
Assertions.<DistributionSetType> assertThat(distributionSetTypeManagement.findByRsql("name==*", UNPAGED))
|
||||
.hasSize(1).containsExactly(dsType1);
|
||||
assertThat(distributionSetTypeManagement.countByRsql("name==*")).isEqualTo(1);
|
||||
|
||||
assertThat(distributionSetTypeManagement.exists(dsType1.getId())).isTrue();
|
||||
assertThat(distributionSetTypeManagement.exists(dsType2.getId())).isFalse();
|
||||
|
||||
assertThat(distributionSetTypeManagement.find(dsType1.getId()).map(DistributionSetType.class::cast)).hasValue(dsType1);
|
||||
assertThat(distributionSetTypeManagement.find(dsType2.getId())).isEmpty();
|
||||
|
||||
Assertions.<DistributionSetType> assertThat(distributionSetTypeManagement.get(List.of(dsType1.getId())))
|
||||
.hasSize(1).contains(dsType1);
|
||||
final List<Long> noPermissionIdList = List.of(dsType2.getId());
|
||||
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> distributionSetTypeManagement.get(noPermissionIdList));
|
||||
final List<Long> allPermissionsIdList = List.of(dsType1.getId(), dsType2.getId());
|
||||
assertThatExceptionOfType(EntityNotFoundException.class).isThrownBy(() -> distributionSetTypeManagement.get(allPermissionsIdList));
|
||||
|
||||
assertThat(distributionSetTypeManagement.findByKey(dsType1.getKey()).map(DistributionSetType.class::cast)).hasValue(dsType1);
|
||||
assertThat(distributionSetTypeManagement.findByKey(dsType2.getKey())).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyUpdate() {
|
||||
// permissions to read all and update only type1 ds types
|
||||
runAs(withAuthorities(READ_PREFIX + DISTRIBUTION_SET_TYPE, UPDATE_PREFIX + DISTRIBUTION_SET_TYPE + "/id==" + dsType1.getId()), () -> {
|
||||
final String newDescription = randomString(16);
|
||||
|
||||
final Long dsType2Id = dsType2.getId();
|
||||
assertThat(distributionSetTypeManagement.update(
|
||||
DistributionSetTypeManagement.Update.builder().id(dsType1.getId()).description(newDescription).build()))
|
||||
.extracting(NamedEntity::getDescription).isEqualTo(newDescription);
|
||||
|
||||
final DistributionSetTypeManagement.Update descriptionUpdate = DistributionSetTypeManagement.Update.builder()
|
||||
.id(dsType2Id).description(newDescription).build();
|
||||
assertThatThrownBy(() -> distributionSetTypeManagement.update(descriptionUpdate))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
});
|
||||
|
||||
// override types with unassigned in order to have modifiable types
|
||||
final DistributionSetType dsType1 = testdataFactory.findOrCreateDistributionSetType(
|
||||
"DsType1_override", "DistributionSetType-1-override", List.of(smType1), List.of());
|
||||
final DistributionSetType dsType2 = testdataFactory.findOrCreateDistributionSetType(
|
||||
"DsType2_override", "DistributionSetType-2-override", List.of(smType2), List.of(smType1));
|
||||
runAs(withAuthorities(READ_PREFIX + DISTRIBUTION_SET_TYPE, UPDATE_PREFIX + DISTRIBUTION_SET_TYPE + "/id==" + dsType1.getId()), () -> {
|
||||
final List<Long> osAndAppTypeIds = List.of(osType.getId(), appType.getId());
|
||||
// verify distributionSetTypeManagement#assignCompatibleDistributionSetTypes
|
||||
DistributionSetType dsType1Up = distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType1.getId(), osAndAppTypeIds);
|
||||
assertThat(dsType1Up).satisfies(
|
||||
updatedType -> assertThat(Stream.of(osType, appType).allMatch(updatedType::containsModuleType)).isTrue());
|
||||
assertThat(dsType1Up.containsModuleType(osType)).isTrue();
|
||||
assertThat(dsType1Up.containsModuleType(appType)).isTrue();
|
||||
final Long dsType2Id = dsType2.getId();
|
||||
assertThatThrownBy(() -> distributionSetTypeManagement.assignMandatorySoftwareModuleTypes(dsType2Id, osAndAppTypeIds))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
|
||||
assertThat(distributionSetTypeManagement
|
||||
.unassignSoftwareModuleType(dsType1.getId(), osType.getId()))
|
||||
.satisfies(updatedType -> assertThat(updatedType.containsModuleType(osType)).isFalse());
|
||||
assertThatThrownBy(
|
||||
() -> distributionSetTypeManagement.unassignSoftwareModuleType(dsType2Id, osType.getId()))
|
||||
.isInstanceOf(InsufficientPermissionException.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyDelete() {
|
||||
// permissions to read all and update only type1 ds types
|
||||
runAs(withAuthorities(READ_PREFIX + DISTRIBUTION_SET_TYPE, DELETE_PREFIX + DISTRIBUTION_SET_TYPE + "/id==" + dsType1.getId()), () -> {
|
||||
assertThat(distributionSetTypeManagement.find(dsType1.getId())).isPresent();
|
||||
distributionSetTypeManagement.delete(dsType1.getId());
|
||||
// soft delete since dsType1 is assigned to ds1Type1
|
||||
assertThat(distributionSetTypeManagement.find(dsType1.getId())).hasValueSatisfying(DistributionSetType::isDeleted);
|
||||
|
||||
final Long ds2Type2Id = dsType2.getId();
|
||||
assertThatThrownBy(() -> distributionSetTypeManagement.delete(ds2Type2Id)).isInstanceOf(InsufficientPermissionException.class);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 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.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_DISTRIBUTION_SET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
|
||||
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
|
||||
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.AutoAssignDistributionSetUpdate;
|
||||
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Note: Still all test gets READ_REPOSITORY since find methods are inherited with request for READ_REPOSITORY. However,
|
||||
* using READ_DISTRIBUTION_SET scoping - the scopes still work.
|
||||
* <p/>
|
||||
* Feature: Component Tests - Access Control<br/>
|
||||
* Story: Test Distribution Set Access Controller
|
||||
*/
|
||||
class TargetTypeQueryManagementTest extends AbstractAccessControllerTest {
|
||||
|
||||
@Test
|
||||
void verifyAutoAssignmentRestrictionByDs() {
|
||||
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
|
||||
.create(TargetFilterQueryManagement.Create.builder().name("test").query("id==*").build());
|
||||
|
||||
runAs(withAuthorities(
|
||||
READ_DISTRIBUTION_SET + "/type.id==" + dsType1.getId() + " or id==" + ds2Type2.getId(),
|
||||
UPDATE_DISTRIBUTION_SET + "/type.id==" + dsType1.getId(),
|
||||
// read / update target needed to update target filter query
|
||||
READ_TARGET, UPDATE_TARGET), () -> {
|
||||
assertThat(targetFilterQueryManagement
|
||||
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(ds1Type1.getId())
|
||||
.actionType(Action.ActionType.FORCED).confirmationRequired(false))
|
||||
.getAutoAssignDistributionSet().getId()).isEqualTo(ds1Type1.getId());
|
||||
targetFilterQueryManagement
|
||||
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
|
||||
.ds(ds2Type2.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false))
|
||||
.getAutoAssignDistributionSet().getId();
|
||||
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(
|
||||
targetFilterQuery.getId())
|
||||
.ds(ds3Type2.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false);
|
||||
assertThatThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate))
|
||||
.isInstanceOf(EntityNotFoundException.class);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user