1
0

add-unix-user-hosting-asset-validation (#66)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/66
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-06-27 12:39:44 +02:00
parent de88f1d842
commit 6167ef2221
28 changed files with 895 additions and 233 deletions

View File

@@ -53,7 +53,7 @@ class HsBookingItemEntityUnitTest {
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
final var result = givenBookingItem.toString();
assertThat(result).isEqualTo("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
assertThat(result).isEqualToIgnoringWhitespace("HsBookingItemEntity(D-1234500:test project, CLOUD_SERVER, [2020-01-01,2031-01-01), some caption, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
}
@Test

View File

@@ -170,9 +170,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
// then
allTheseBookingItemsAreReturned(
result,
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })",
"HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )",
"HsBookingItemEntity(D-1000212:D-1000212 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )",
"HsBookingItemEntity(D-1000212:D-1000212 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )");
assertThat(result.stream().filter(bi -> bi.getRelatedHostingAsset()!=null).findAny())
.as("at least one relatedProject expected, but none found => fetching relatedProject does not work")
.isNotEmpty();
@@ -193,9 +193,9 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
// then:
exactlyTheseBookingItemsAreReturned(
result,
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 })",
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 })",
"HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 })");
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_WEBSPACE, [2022-10-01,), separate ManagedWebspace, { Daemons: 0, Multi: 1, SSD: 100, Traffic: 50 } )",
"HsBookingItemEntity(D-1000111:D-1000111 default project, MANAGED_SERVER, [2022-10-01,), separate ManagedServer, { CPUs: 2, RAM: 8, SSD: 500, Traffic: 500 } )",
"HsBookingItemEntity(D-1000111:D-1000111 default project, PRIVATE_CLOUD, [2024-04-01,), some PrivateCloud, { CPUs: 10, HDD: 10000, RAM: 32, SSD: 4000, Traffic: 2000 } )");
}
}
@@ -348,13 +348,17 @@ class HsBookingItemRepositoryIntegrationTest extends ContextBasedTestWithCleanup
final List<HsBookingItemEntity> actualResult,
final String... bookingItemNames) {
assertThat(actualResult)
.extracting(bookingItemEntity -> bookingItemEntity.toString())
.extracting(HsBookingItemEntity::toString)
.extracting(string-> string.replaceAll("\\s+", " "))
.extracting(string-> string.replaceAll("\"", ""))
.containsExactlyInAnyOrder(bookingItemNames);
}
void allTheseBookingItemsAreReturned(final List<HsBookingItemEntity> actualResult, final String... bookingItemNames) {
assertThat(actualResult)
.extracting(bookingItemEntity -> bookingItemEntity.toString())
.extracting(HsBookingItemEntity::toString)
.extracting(string -> string.replaceAll("\\s+", " "))
.extracting(string -> string.replaceAll("\"", ""))
.contains(bookingItemNames);
}
}

View File

@@ -12,21 +12,35 @@ import static net.hostsharing.hsadminng.hs.booking.project.TestHsBookingProject.
@UtilityClass
public class TestHsBookingItem {
public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
.project(TEST_PROJECT)
.type(HsBookingItemType.MANAGED_SERVER)
.caption("test project booking item")
.resources(Map.ofEntries(
entry("someThing", 1),
entry("anotherThing", "blue")
))
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build();
public static final HsBookingItemEntity TEST_CLOUD_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
.project(TEST_PROJECT)
.type(HsBookingItemType.CLOUD_SERVER)
.caption("test cloud server booking item")
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build();
public static final HsBookingItemEntity TEST_MANAGED_SERVER_BOOKING_ITEM = HsBookingItemEntity.builder()
.project(TEST_PROJECT)
.type(HsBookingItemType.MANAGED_SERVER)
.caption("test project booking item")
.resources(Map.ofEntries(
entry("CPUs", 2),
entry("RAM", 4),
entry("SSD", 50),
entry("Traffic", 250)
))
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build();
public static final HsBookingItemEntity TEST_MANAGED_WEBSPACE_BOOKING_ITEM = HsBookingItemEntity.builder()
.parentItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
.type(HsBookingItemType.MANAGED_WEBSPACE)
.caption("test managed webspace item")
.resources(Map.ofEntries(
entry("SSD", 50),
entry("Traffic", 250)
))
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build();
}

View File

@@ -25,12 +25,14 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Supplier;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.strictlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.matchesRegex;
@@ -73,7 +75,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
// given
context("superuser-alex@hostsharing.net");
final var givenProject = projectRepo.findByCaption("D-1000111 default project").stream()
.findAny().orElseThrow();
.findAny().orElseThrow();
RestAssured // @formatter:off
.given()
@@ -264,7 +266,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
void propertyValidationsArePerformend_whenAddingAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenBookingItem = givenSomeNewBookingItem("D-1000111 default project",
final var givenBookingItem = givenSomeNewBookingItem(
"D-1000111 default project",
HsBookingItemType.MANAGED_SERVER,
"some PrivateCloud");
@@ -292,14 +295,13 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"statusPhrase": "Bad Request",
"message": "[
<<<'MANAGED_SERVER:vm1400.config.extra' is not expected but is set to '42',
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be <= 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be >= 10 but is 0
<<<'MANAGED_SERVER:vm1400.config.monit_max_cpu_usage' is expected to be at most 100 but is 101,
<<<'MANAGED_SERVER:vm1400.config.monit_max_ssd_usage' is expected to be at least 10 but is 0
<<<]"
}
""".replaceAll(" +<<<", ""))); // @formatter:on
}
@Test
void totalsLimitValidationsArePerformend_whenAddingAsset() {
@@ -311,7 +313,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
for (int n = 0; n < 25; ++n ) {
for (int n = 0; n < 25; ++n) {
toCleanup(assetRepo.save(
HsHostingAssetEntity.builder()
.type(UNIX_USER)
@@ -358,8 +360,8 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
void globalAdmin_canGetArbitraryAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenAssetUuid = assetRepo.findByIdentifier("vm1011").stream()
.filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project"))
.findAny().orElseThrow().getUuid();
.filter(bi -> bi.getBookingItem().getProject().getCaption().equals("D-1000111 default project"))
.findAny().orElseThrow().getUuid();
RestAssured // @formatter:off
.given()
@@ -429,8 +431,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test
void globalAdmin_canPatchAllUpdatablePropertiesOfAsset() {
final var givenAsset = givenSomeTemporaryHostingAsset("2001", MANAGED_SERVER,
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID())
.bookingItem(givenSomeNewBookingItem(
"D-1000111 default project",
HsBookingItemType.MANAGED_SERVER,
"temp ManagedServer"))
.type(MANAGED_SERVER)
.identifier("vm2001")
.caption("some test-asset")
.config(Map.ofEntries(
Map.<String, Object>entry("monit_max_ssd_usage", 80),
Map.<String, Object>entry("monit_max_hdd_usage", 90),
Map.<String, Object>entry("monit_max_cpu_usage", 90),
Map.<String, Object>entry("monit_max_ram_usage", 70)
))
.build());
final var alarmContactUuid = givenContact().getUuid();
RestAssured // @formatter:off
@@ -459,9 +476,10 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"identifier": "vm2001",
"caption": "some test-asset",
"alarmContact": {
"uuid": "%s",
"caption": "second contact",
"emailAddresses": { "main": "contact-admin@secondcontact.example.com" }
"emailAddresses": {
"main": "contact-admin@secondcontact.example.com"
}
},
"config": {
"monit_max_cpu_usage": 90,
@@ -470,27 +488,101 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
"monit_min_free_ssd": 5
}
}
""".formatted(alarmContactUuid)));
"""));
// @formatter:on
// finally, the asset is actually updated
em.clear();
context.define("superuser-alex@hostsharing.net");
assertThat(assetRepo.findByUuid(givenAsset.getUuid())).isPresent().get()
.matches(asset -> {
assertThat(asset.getAlarmContact().toString()).isEqualTo(
"contact(caption='second contact', emailAddresses='{ main: contact-admin@secondcontact.example.com }')");
assertThat(asset.getConfig().toString()).isEqualTo(
"{ monit_max_cpu_usage: 90, monit_max_ram_usage: 70, monit_max_ssd_usage: 85, monit_min_free_ssd: 5 }");
assertThat(asset.getAlarmContact()).isNotNull()
.extracting(c -> c.getEmailAddresses().get("main"))
.isEqualTo("contact-admin@secondcontact.example.com");
assertThat(asset.getConfig().toString())
.isEqualToIgnoringWhitespace("""
{
"monit_max_cpu_usage": 90,
"monit_max_ram_usage": 70,
"monit_max_ssd_usage": 85,
"monit_min_free_ssd": 5
}
""");
return true;
});
}
}
private HsOfficeContactEntity givenContact() {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow();
}).returnedValue();
@Test
void assetAdmin_canPatchAllUpdatablePropertiesOfAsset() {
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID())
.type(UNIX_USER)
.parentAsset(givenHostingAsset(MANAGED_WEBSPACE, "fir01"))
.identifier("fir01-temp")
.caption("some test-unix-user")
.build());
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
//.header("assumed-roles", "hs_hosting_asset#vm2001:ADMIN")
.contentType(ContentType.JSON)
.body("""
{
"caption": "some patched test-unix-user",
"config": {
"shell": "/bin/bash",
"totpKey": "0x1234567890abcdef0123456789abcdef",
"password": "Ein Passwort mit 4 Zeichengruppen!"
}
}
""")
.port(port)
.when()
.patch("http://localhost/api/hs/hosting/assets/" + givenAsset.getUuid())
.then().log().all().assertThat()
.statusCode(200)
.contentType(ContentType.JSON)
.body("", lenientlyEquals("""
{
"type": "UNIX_USER",
"identifier": "fir01-temp",
"caption": "some patched test-unix-user",
"config": {
"homedir": "/home/pacs/fir01/users/temp",
"shell": "/bin/bash"
}
}
"""))
// the config separately but not-leniently to make sure that no write-only-properties are listed
.body("config", strictlyEquals("""
{
"homedir": "/home/pacs/fir01/users/temp",
"shell": "/bin/bash"
}
"""))
;
// @formatter:on
// finally, the asset is actually updated
assertThat(jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
return assetRepo.findByUuid(givenAsset.getUuid());
}).returnedValue()).isPresent().get()
.matches(asset -> {
assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user");
assertThat(asset.getConfig().toString()).isEqualTo("""
{
"password": "Ein Passwort mit 4 Zeichengruppen!",
"shell": "/bin/bash",
"totpKey": "0x1234567890abcdef0123456789abcdef"
}
""");
return true;
});
}
}
@Nested
@@ -500,9 +592,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test
void globalAdmin_canDeleteArbitraryAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenAsset = givenSomeTemporaryHostingAsset("1002", MANAGED_SERVER,
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID())
.bookingItem(givenSomeNewBookingItem(
"D-1000111 default project",
HsBookingItemType.MANAGED_SERVER,
"temp ManagedServer"))
.type(MANAGED_SERVER)
.identifier("vm1002")
.caption("some test-asset")
.config(Map.ofEntries(
Map.<String, Object>entry("monit_max_ssd_usage", 80),
Map.<String, Object>entry("monit_max_hdd_usage", 90),
Map.<String, Object>entry("monit_max_cpu_usage", 90),
Map.<String, Object>entry("monit_max_ram_usage", 70)
))
.build());
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
@@ -519,9 +625,23 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
@Test
void normalUser_canNotDeleteUnrelatedAsset() {
context.define("superuser-alex@hostsharing.net");
final var givenAsset = givenSomeTemporaryHostingAsset("1003", MANAGED_SERVER,
config("monit_max_ssd_usage", 80), config("monit_max_hdd_usage", 90), config("monit_max_cpu_usage", 90), config("monit_max_ram_usage", 70));
final var givenAsset = givenSomeTemporaryHostingAsset(() ->
HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID())
.bookingItem(givenSomeNewBookingItem(
"D-1000111 default project",
HsBookingItemType.MANAGED_SERVER,
"temp ManagedServer"))
.type(MANAGED_SERVER)
.identifier("vm1003")
.caption("some test-asset")
.config(Map.ofEntries(
Map.<String, Object>entry("monit_max_ssd_usage", 80),
Map.<String, Object>entry("monit_max_hdd_usage", 90),
Map.<String, Object>entry("monit_max_cpu_usage", 90),
Map.<String, Object>entry("monit_max_ram_usage", 70)
))
.build());
RestAssured // @formatter:off
.given()
.header("current-user", "selfregistered-user-drew@hostsharing.org")
@@ -538,7 +658,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
HsHostingAssetEntity givenHostingAsset(final HsHostingAssetType type, final String identifier) {
return assetRepo.findByIdentifier(identifier).stream()
.filter(ha -> ha.getType()==type)
.filter(ha -> ha.getType() == type)
.findAny().orElseThrow();
}
@@ -559,12 +679,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}).assertSuccessful().returnedValue();
}
HsBookingItemEntity givenSomeNewBookingItem(final String projectCaption, final HsBookingItemType bookingItemType, final String bookingItemCaption) {
HsBookingItemEntity givenSomeNewBookingItem(
final String projectCaption,
final HsBookingItemType bookingItemType,
final String bookingItemCaption) {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var project = projectRepo.findByCaption(projectCaption).getFirst();
final var resources = switch (bookingItemType) {
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1), entry("RAM", 20), entry("SSD", 25), entry("Traffic", 250));
case MANAGED_SERVER -> Map.<String, Object>ofEntries(entry("CPUs", 1),
entry("RAM", 20),
entry("SSD", 25),
entry("Traffic", 250));
default -> new HashMap<String, Object>();
};
final var newBookingItem = HsBookingItemEntity.builder()
@@ -584,33 +710,18 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
return givenAsset;
}
@SafeVarargs
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final String identifierSuffix,
final HsHostingAssetType hostingAssetType,
final Map.Entry<String, Object>... config) {
private HsHostingAssetEntity givenSomeTemporaryHostingAsset(final Supplier<HsHostingAssetEntity> newAsset) {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
final var bookingItemType = switch (hostingAssetType) {
case CLOUD_SERVER -> HsBookingItemType.CLOUD_SERVER;
case MANAGED_SERVER -> HsBookingItemType.MANAGED_SERVER;
case MANAGED_WEBSPACE -> HsBookingItemType.MANAGED_WEBSPACE;
default -> null;
};
final var newBookingItem = givenSomeNewBookingItem("D-1000111 default project", bookingItemType, "temp ManagedServer");
final var newAsset = HsHostingAssetEntity.builder()
.uuid(UUID.randomUUID())
.bookingItem(newBookingItem)
.type(hostingAssetType)
.identifier("vm" + identifierSuffix)
.caption("some test-asset")
.config(Map.ofEntries(config))
.build();
return assetRepo.save(newAsset);
return toCleanup(assetRepo.save(newAsset.get()));
}).assertSuccessful().returnedValue();
}
private Map.Entry<String, Object> config(final String key, final Object value) {
return entry(key, value);
private HsOfficeContactEntity givenContact() {
return jpaAttempt.transacted(() -> {
context.define("superuser-alex@hostsharing.net");
return contactRepo.findContactByOptionalCaptionLike("second").stream().findFirst().orElseThrow();
}).returnedValue();
}
}

View File

@@ -57,14 +57,14 @@ class HsHostingAssetEntityUnitTest {
@Test
void toStringContainsAllPropertiesAndResourcesSortedByKey() {
assertThat(givenWebspace.toString()).isEqualTo(
"HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { CPUs: 2, HDD-storage: 2048, SSD-storage: 512 })");
assertThat(givenWebspace.toString()).isEqualToIgnoringWhitespace(
"HsHostingAssetEntity(MANAGED_WEBSPACE, xyz00, some managed webspace, MANAGED_SERVER:vm1234, D-1234500:test project:test cloud server booking item, { \"CPUs\": 2, \"HDD-storage\": 2048, \"SSD-storage\": 512 })");
assertThat(givenUnixUser.toString()).isEqualTo(
"HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { HDD-hard-quota: 512, HDD-soft-quota: 256, SSD-hard-quota: 256, SSD-soft-quota: 128 })");
assertThat(givenUnixUser.toString()).isEqualToIgnoringWhitespace(
"HsHostingAssetEntity(UNIX_USER, xyz00-web, some unix-user, MANAGED_WEBSPACE:xyz00, { \"HDD-hard-quota\": 512, \"HDD-soft-quota\": 256, \"SSD-hard-quota\": 256, \"SSD-soft-quota\": 128 })");
assertThat(givenDomainHttpSetup.toString()).isEqualTo(
"HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { option-htdocsfallback: true, use-fcgiphpbin: /usr/lib/cgi-bin/php, validsubdomainnames: * })");
assertThat(givenDomainHttpSetup.toString()).isEqualToIgnoringWhitespace(
"HsHostingAssetEntity(DOMAIN_HTTP_SETUP, example.org, some domain setup, MANAGED_WEBSPACE:xyz00, UNIX_USER:xyz00-web, { \"option-htdocsfallback\": true, \"use-fcgiphpbin\": \"/usr/lib/cgi-bin/php\", \"validsubdomainnames\": \"*\" })");
}
@Test

View File

@@ -59,9 +59,7 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 92,
"isTotalsValidator": false
"defaultValue": 92
},
{
"type": "integer",
@@ -69,9 +67,7 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 92,
"isTotalsValidator": false
"defaultValue": 92
},
{
"type": "integer",
@@ -79,18 +75,14 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 98,
"isTotalsValidator": false
"defaultValue": 98
},
{
"type": "integer",
"propertyName": "monit_min_free_ssd",
"min": 1,
"max": 1000,
"required": false,
"defaultValue": 5,
"isTotalsValidator": false
"defaultValue": 5
},
{
"type": "integer",
@@ -98,32 +90,24 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"unit": "%",
"min": 10,
"max": 100,
"required": false,
"defaultValue": 95,
"isTotalsValidator": false
"defaultValue": 95
},
{
"type": "integer",
"propertyName": "monit_min_free_hdd",
"min": 1,
"max": 4000,
"required": false,
"defaultValue": 10,
"isTotalsValidator": false
"defaultValue": 10
},
{
"type": "boolean",
"propertyName": "software-pgsql",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
"defaultValue": true
},
{
"type": "boolean",
"propertyName": "software-mariadb",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
"defaultValue": true
},
{
"type": "enumeration",
@@ -139,114 +123,70 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"8.1",
"8.2"
],
"required": false,
"defaultValue": "8.2",
"isTotalsValidator": false
"defaultValue": "8.2"
},
{
"type": "boolean",
"propertyName": "software-php-5.6",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-5.6"
},
{
"type": "boolean",
"propertyName": "software-php-7.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-7.0"
},
{
"type": "boolean",
"propertyName": "software-php-7.1",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-7.1"
},
{
"type": "boolean",
"propertyName": "software-php-7.2",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-7.2"
},
{
"type": "boolean",
"propertyName": "software-php-7.3",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-7.3"
},
{
"type": "boolean",
"propertyName": "software-php-7.4",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
"defaultValue": true
},
{
"type": "boolean",
"propertyName": "software-php-8.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-8.0"
},
{
"type": "boolean",
"propertyName": "software-php-8.1",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-php-8.1"
},
{
"type": "boolean",
"propertyName": "software-php-8.2",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
"defaultValue": true
},
{
"type": "boolean",
"propertyName": "software-postfix-tls-1.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-postfix-tls-1.0"
},
{
"type": "boolean",
"propertyName": "software-dovecot-tls-1.0",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-dovecot-tls-1.0"
},
{
"type": "boolean",
"propertyName": "software-clamav",
"required": false,
"defaultValue": true,
"isTotalsValidator": false
"defaultValue": true
},
{
"type": "boolean",
"propertyName": "software-collabora",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-collabora"
},
{
"type": "boolean",
"propertyName": "software-libreoffice",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-libreoffice"
},
{
"type": "boolean",
"propertyName": "software-imagemagick-ghostscript",
"required": false,
"defaultValue": false,
"isTotalsValidator": false
"propertyName": "software-imagemagick-ghostscript"
}
]
"""));

View File

@@ -195,7 +195,7 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
exactlyTheseAssetsAreReturned(
result,
"HsHostingAssetEntity(MANAGED_WEBSPACE, fir01, some Webspace, MANAGED_SERVER:vm1011, D-1000111:D-1000111 default project:separate ManagedWebspace)",
"HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 })");
"HsHostingAssetEntity(MANAGED_SERVER, vm1011, some ManagedServer, D-1000111:D-1000111 default project:separate ManagedServer, { monit_max_cpu_usage: 90, monit_max_ram_usage: 80, monit_max_ssd_usage: 70 } )");
}
@Test
@@ -407,6 +407,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
final String... serverNames) {
assertThat(actualResult)
.extracting(HsHostingAssetEntity::toString)
.extracting(input -> input.replaceAll("\\s+", " "))
.extracting(input -> input.replaceAll("\"", ""))
.containsExactlyInAnyOrder(serverNames);
}

View File

@@ -37,8 +37,8 @@ class HsManagedServerHostingAssetValidatorUnitTest {
assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be >= 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be <= 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
}

View File

@@ -1,14 +1,95 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
import static net.hostsharing.hsadminng.mapper.PatchMap.entry;
import static org.assertj.core.api.Assertions.assertThat;
class HsUnixUserHostingAssetValidatorUnitTest {
private final HsHostingAssetEntity TEST_MANAGED_SERVER_HOSTING_ASSET = HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MANAGED_SERVER)
.identifier("vm1234")
.caption("some managed server")
.bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
.build();
private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE)
.bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("abc00")
.build();;
@Test
void validatesValidUnixUser() {
// given
final var unixUserHostingAsset = HsHostingAssetEntity.builder()
.type(UNIX_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-temp")
.caption("some valid test UnixUser")
.config(Map.ofEntries(
entry("SSD hard quota", 50),
entry("SSD soft quota", 40),
entry("totpKey", "0x123456789abcdef01234"),
entry("password", "Hallo Computer, lass mich rein!")
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
final var result = validator.validate(unixUserHostingAsset);
// then
assertThat(result).isEmpty();
}
@Test
void validatesUnixUserProperties() {
// given
final var unixUserHostingAsset = HsHostingAssetEntity.builder()
.type(UNIX_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-temp")
.caption("some test UnixUser with invalid properties")
.config(Map.ofEntries(
entry("SSD hard quota", 100),
entry("SSD soft quota", 200),
entry("HDD hard quota", 100),
entry("HDD soft quota", 200),
entry("shell", "/is/invalid"),
entry("homedir", "/is/read-only"),
entry("totpKey", "should be a hex number"),
entry("password", "short")
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
final var result = validator.validate(unixUserHostingAsset);
// then
assertThat(result).containsExactlyInAnyOrder(
"'UNIX_USER:abc00-temp.config.SSD hard quota' is expected to be at most 50 but is 100",
"'UNIX_USER:abc00-temp.config.SSD soft quota' is expected to be at most 100 but is 200",
"'UNIX_USER:abc00-temp.config.HDD hard quota' is expected to be at most 0 but is 100",
"'UNIX_USER:abc00-temp.config.HDD soft quota' is expected to be at most 100 but is 200",
"'UNIX_USER:abc00-temp.config.shell' is expected to be one of [/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd] but is '/is/invalid'",
"'UNIX_USER:abc00-temp.config.homedir' is readonly but given as '/is/read-only'",
"'UNIX_USER:abc00-temp.config.totpKey' is expected to be match ^0x([0-9A-Fa-f]{2})+$ but provided value does not match",
"'UNIX_USER:abc00-temp.config.password' length is expected to be at min 8 but length of provided value is 5",
"'UNIX_USER:abc00-temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"
);
}
@Test
void validatesInvalidIdentifier() {
// given
@@ -19,7 +100,6 @@ class HsUnixUserHostingAssetValidatorUnitTest {
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
final var result = validator.validate(unixUserHostingAsset);
@@ -27,4 +107,25 @@ class HsUnixUserHostingAssetValidatorUnitTest {
assertThat(result).containsExactly(
"'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'");
}
@Test
void describesItsProperties() {
// given
final var validator = HsHostingAssetEntityValidatorRegistry.forType(UNIX_USER);
// when
final var props = validator.properties();
// then
assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=SSD hard quota, unit=GB, maxFrom=SSD}",
"{type=integer, propertyName=SSD soft quota, unit=GB, maxFrom=SSD hard quota}",
"{type=integer, propertyName=HDD hard quota, unit=GB, maxFrom=HDD}",
"{type=integer, propertyName=HDD soft quota, unit=GB, maxFrom=HDD hard quota}",
"{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}",
"{type=string, propertyName=homedir, readOnly=true, computed=true}",
"{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}",
"{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}"
);
}
}

View File

@@ -0,0 +1,92 @@
package net.hostsharing.hsadminng.hs.validation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.ArrayList;
import java.util.List;
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
import static org.assertj.core.api.Assertions.assertThat;
class PasswordPropertyUnitTest {
private final ValidatableProperty<String> passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly();
private final List<String> violations = new ArrayList<>();
@ParameterizedTest
@ValueSource(strings = {
"lowerUpperAndDigit1",
"lowerUpperAndSpecial!",
"digit1LowerAndSpecial!",
"digit1special!lower",
"DIGIT1SPECIAL!UPPER" })
void shouldValidateValidPassword(final String password) {
// when
passwordProp.validate(violations, password, null);
// then
assertThat(violations).isEmpty();
}
@ParameterizedTest
@ValueSource(strings = {
"noDigitNoSpecial",
"!!!!!!12345",
"nolower-nodigit",
"nolower1nospecial",
"NOLOWER-NODIGIT",
"NOLOWER1NOSPECIAL"
})
void shouldRecognizeMissingCharacterGroup(final String givenPassword) {
// when
passwordProp.validate(violations, givenPassword, null);
// then
assertThat(violations)
.contains("password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters")
.doesNotContain(givenPassword);
}
@Test
void shouldRecognizeTooShortPassword() {
// given
final String givenPassword = "0123456";
// when
passwordProp.validate(violations, givenPassword, null);
// then
assertThat(violations)
.contains("password' length is expected to be at min 8 but length of provided value is 7")
.doesNotContain(givenPassword);
}
@Test
void shouldRecognizeTooLongPassowrd() {
// given
final String givenPassword = "password' length is expected to be at max 40 but is 41";
// when
passwordProp.validate(violations, givenPassword, null);
// then
assertThat(violations).contains("password' length is expected to be at max 40 but length of provided value is 54")
.doesNotContain(givenPassword);
}
@Test
void shouldRecognizeColonInPassword() {
// given
final String givenPassword = "lowerUpper:1234";
// when
passwordProp.validate(violations, givenPassword, null);
// then
assertThat(violations)
.contains("password' must not contain colon (':')")
.doesNotContain(givenPassword);
}
}

View File

@@ -9,13 +9,15 @@ import org.json.JSONException;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
public class JsonMatcher extends BaseMatcher<CharSequence> {
private final String expected;
private final String expectedJson;
private JSONCompareMode compareMode;
public JsonMatcher(final String expected, final JSONCompareMode compareMode) {
this.expected = expected;
public JsonMatcher(final String expectedJson, final JSONCompareMode compareMode) {
this.expectedJson = expectedJson;
this.compareMode = compareMode;
}
@@ -47,8 +49,8 @@ public class JsonMatcher extends BaseMatcher<CharSequence> {
return false;
}
try {
final var actualJson = new ObjectMapper().writeValueAsString(actual);
JSONAssert.assertEquals(expected, actualJson, compareMode);
final var actualJson = new ObjectMapper().enable(INDENT_OUTPUT).writeValueAsString(actual);
JSONAssert.assertEquals(expectedJson, actualJson, compareMode);
return true;
} catch (final JSONException | JsonProcessingException e) {
throw new AssertionError(e);
@@ -59,5 +61,4 @@ public class JsonMatcher extends BaseMatcher<CharSequence> {
public void describeTo(final Description description) {
description.appendText("leniently matches JSON");
}
}