1
0

add-email-alias-hosting-asset (#70)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/70
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-07-03 11:43:08 +02:00
parent c5722e494f
commit a77eaefb94
16 changed files with 524 additions and 46 deletions

View File

@@ -29,6 +29,7 @@ import java.util.UUID;
import java.util.function.Supplier;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ALIAS;
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;
@@ -101,7 +102,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
}
@Test
void globalAdmin_canViewAllAssetsByType() {
void webspaceAgent_canViewAllAssetsByType() {
// given
context("superuser-alex@hostsharing.net");
@@ -109,42 +110,25 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
RestAssured // @formatter:off
.given()
.header("current-user", "superuser-alex@hostsharing.net")
.header("assumed-roles", "hs_hosting_asset#fir01:AGENT")
.port(port)
.when()
. get("http://localhost/api/hs/hosting/assets?type=" + MANAGED_SERVER)
. get("http://localhost/api/hs/hosting/assets?type=" + EMAIL_ALIAS)
.then().log().all().assertThat()
.statusCode(200)
.contentType("application/json")
.body("", lenientlyEquals("""
[
{
"type": "MANAGED_SERVER",
"identifier": "vm1011",
"caption": "some ManagedServer",
"type": "EMAIL_ALIAS",
"identifier": "fir01-web",
"caption": "some E-Mail-Alias",
"alarmContact": null,
"config": {
"monit_max_cpu_usage": 90,
"monit_max_ram_usage": 80,
"monit_max_ssd_usage": 70
}
},
{
"type": "MANAGED_SERVER",
"identifier": "vm1012",
"caption": "some ManagedServer",
"config": {
"monit_max_cpu_usage": 90,
"monit_max_ram_usage": 80,
"monit_max_ssd_usage": 70
}
},
{
"type": "MANAGED_SERVER",
"identifier": "vm1013",
"caption": "some ManagedServer",
"config": {
"monit_max_cpu_usage": 90,
"monit_max_ram_usage": 80,
"monit_max_ssd_usage": 70
"target": [
"office@example.org",
"archive@example.com"
]
}
}
]

View File

@@ -0,0 +1,243 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.mapper.Array;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.SynchronizationType;
import java.util.List;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.office.contact.TestHsOfficeContact.TEST_CONTACT;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(HsHostingAssetController.class)
@Import(Mapper.class)
@RunWith(SpringRunner.class)
public class HsHostingAssetControllerRestTest {
@Autowired
MockMvc mockMvc;
@MockBean
Context contextMock;
@Autowired
Mapper mapper;
@Mock
private EntityManager em;
@MockBean
EntityManagerFactory emf;
@MockBean
@SuppressWarnings("unused") // bean needs to be present for HsHostingAssetController
private HsBookingItemRepository bookingItemRepo;
@MockBean
private HsHostingAssetRepository hostingAssetRepo;
enum ListTestCases {
CLOUD_SERVER(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.CLOUD_SERVER)
.bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM)
.identifier("vm1234")
.caption("some fake cloud-server")
.alarmContact(TEST_CONTACT)
.build()),
"""
[
{
"type": "CLOUD_SERVER",
"identifier": "vm1234",
"caption": "some fake cloud-server",
"alarmContact": {
"caption": "some contact",
"postalAddress": "address of some contact",
"emailAddresses": {
"main": "some-contact@example.com"
}
},
"config": {}
}
]
"""),
MANAGED_SERVER(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MANAGED_SERVER)
.bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
.identifier("vm1234")
.caption("some fake managed-server")
.alarmContact(TEST_CONTACT)
.config(Map.ofEntries(
entry("monit_max_ssd_usage", 70),
entry("monit_max_cpu_usage", 80),
entry("monit_max_ram_usage", 90)
))
.build()),
"""
[
{
"type": "MANAGED_SERVER",
"identifier": "vm1234",
"caption": "some fake managed-server",
"alarmContact": {
"caption": "some contact",
"postalAddress": "address of some contact",
"emailAddresses": {
"main": "some-contact@example.com"
}
},
"config": {
"monit_max_ssd_usage": 70,
"monit_max_cpu_usage": 80,
"monit_max_ram_usage": 90
}
}
]
"""),
UNIX_USER(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.UNIX_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("xyz00-office")
.caption("some fake Unix-User")
.config(Map.ofEntries(
entry("password", "$6$salt$hashed-salted-password"),
entry("totpKey", "0x0123456789abcdef"),
entry("shell", "/bin/bash"),
entry("SSD-soft-quota", 128),
entry("SSD-hard-quota", 256),
entry("HDD-soft-quota", 256),
entry("HDD-hard-quota", 512)))
.build()),
"""
[
{
"type": "UNIX_USER",
"identifier": "xyz00-office",
"caption": "some fake Unix-User",
"alarmContact": null,
"config": {
"SSD-soft-quota": 128,
"SSD-hard-quota": 256,
"HDD-soft-quota": 256,
"HDD-hard-quota": 512,
"shell": "/bin/bash",
"homedir": "/home/pacs/xyz00/users/office"
}
}
]
"""),
EMAIL_ALIAS(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.EMAIL_ALIAS)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("xyz00-office")
.caption("some fake EMail-Alias")
.config(Map.ofEntries(
entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com"))
))
.build()),
"""
[
{
"type": "EMAIL_ALIAS",
"identifier": "xyz00-office",
"caption": "some fake EMail-Alias",
"alarmContact": null,
"config": {
"target": ["xyz00","xyz00-abc","office@example.com"]
}
}
]
""");
final HsHostingAssetType assetType;
final List<HsHostingAssetEntity> givenHostingAssetsOfType;
final String expectedResponse;
final JsonNode expectedResponseJson;
@SneakyThrows
ListTestCases(
final List<HsHostingAssetEntity> givenHostingAssetsOfType,
final String expectedResponse) {
this.assetType = HsHostingAssetType.valueOf(name());
this.givenHostingAssetsOfType = givenHostingAssetsOfType;
this.expectedResponse = expectedResponse;
this.expectedResponseJson = new ObjectMapper().readTree(expectedResponse);
}
@SneakyThrows
JsonNode expectedConfig(final int n) {
return expectedResponseJson.get(n).path("config");
}
}
@BeforeEach
void init() {
when(emf.createEntityManager()).thenReturn(em);
when(emf.createEntityManager(any(Map.class))).thenReturn(em);
when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em);
when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em);
}
@ParameterizedTest
@EnumSource(HsHostingAssetControllerRestTest.ListTestCases.class)
void shouldListAssets(final HsHostingAssetControllerRestTest.ListTestCases testCase) throws Exception {
// given
when(hostingAssetRepo.findAllByCriteria(null, null, testCase.assetType))
.thenReturn(testCase.givenHostingAssetsOfType);
// when
final var result = mockMvc.perform(MockMvcRequestBuilders
.get("/api/hs/hosting/assets?type="+testCase.name())
.header("current-user", "superuser-alex@hostsharing.net")
.accept(MediaType.APPLICATION_JSON))
// then
.andExpect(status().is2xxSuccessful())
.andExpect(jsonPath("$", lenientlyEquals(testCase.expectedResponse)))
.andReturn();
// and the config properties do match not just leniently but even strictly
final var resultBody = new ObjectMapper().readTree(result.getResponse().getContentAsString());
for (int n = 0; n < resultBody.size(); ++n) {
assertThat(resultBody.get(n).path("config")).isEqualTo(testCase.expectedConfig(n));
}
}
}

View File

@@ -34,7 +34,8 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"MANAGED_SERVER",
"MANAGED_WEBSPACE",
"CLOUD_SERVER",
"UNIX_USER"
"UNIX_USER",
"EMAIL_ALIAS"
]
"""));
// @formatter:on

View File

@@ -0,0 +1,22 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
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;
public class TestHsHostingAssetEntities {
public static 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();
public static final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MANAGED_WEBSPACE)
.identifier("xyz00")
.caption("some managed webspace")
.bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM)
.build();
}

View File

@@ -0,0 +1,114 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.mapper.Array;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ALIAS;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET;
import static org.assertj.core.api.Assertions.assertThat;
class HsEMailAliasHostingAssetValidatorUnitTest {
@Test
void containsAllValidations() {
// when
final var validator = HsHostingAssetEntityValidatorRegistry.forType(EMAIL_ALIAS);
// then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=string[], propertyName=target, elementsOf={type=string, propertyName=target, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$], maxLength=320}, required=true, minLength=1}");
}
@Test
void validatesValidEntity() {
// given
final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder()
.type(EMAIL_ALIAS)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("xyz00-office")
.config(Map.ofEntries(
entry("target", Array.of("xyz00", "xyz00-abc", "office@example.com"))
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(emailAliasHostingAssetEntity);
// then
assertThat(result).isEmpty();
}
@Test
void validatesProperties() {
// given
final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder()
.type(EMAIL_ALIAS)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("xyz00-office")
.config(Map.ofEntries(
entry("target", Array.of("xyz00", "xyz00-abc", "garbage", "office@example.com"))
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(emailAliasHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'EMAIL_ALIAS:xyz00-office.config.target' is expected to match any of [^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$, ^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$] but 'garbage' does not match any");
}
@Test
void validatesInvalidIdentifier() {
// given
final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder()
.type(EMAIL_ALIAS)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-office")
.config(Map.ofEntries(
entry("target", Array.of("office@example.com"))
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(emailAliasHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'identifier' expected to match '^xyz00$|^xyz00-[a-z0-9]+$', but is 'abc00-office'");
}
@Test
void validatesInvalidReferences() {
// given
final var emailAliasHostingAssetEntity = HsHostingAssetEntity.builder()
.type(EMAIL_ALIAS)
.bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.assignedToAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("abc00-office")
.config(Map.ofEntries(
entry("target", Array.of("office@example.com"))
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(emailAliasHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(emailAliasHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'EMAIL_ALIAS:abc00-office.bookingItem' must be null but is set to D-1234500:test project:test project booking item",
"'EMAIL_ALIAS:abc00-office.parentAsset' must be of type MANAGED_WEBSPACE but is of type MANAGED_SERVER",
"'EMAIL_ALIAS:abc00-office.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item");
}
}

View File

@@ -32,7 +32,8 @@ class HsHostingAssetEntityValidatorRegistryUnitTest {
HsHostingAssetType.CLOUD_SERVER,
HsHostingAssetType.MANAGED_SERVER,
HsHostingAssetType.MANAGED_WEBSPACE,
HsHostingAssetType.UNIX_USER
HsHostingAssetType.UNIX_USER,
HsHostingAssetType.EMAIL_ALIAS
);
}
}

View File

@@ -110,7 +110,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
"'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.totpKey' is expected to match any of [^0x([0-9A-Fa-f]{2})+$] but provided value does not match any",
"'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"
);
@@ -168,7 +168,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
"{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=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, computed=true, hashedUsing=SHA512, undisclosed=true}"
);
}