1
0

introduce-hosting-module (#46)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/46
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig
2024-04-23 11:14:48 +02:00
parent 9806bcd78f
commit 66332b6de2
28 changed files with 3308 additions and 5 deletions

View File

@ -0,0 +1,124 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetInsertResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@RestController
public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
private Context context;
@Autowired
private Mapper mapper;
@Autowired
private HsHostingAssetRepository assetRepo;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsHostingAssetResource>> listAssetsByDebitorUuid(
final String currentUser,
final String assumedRoles,
final UUID debitorUuid) {
context.define(currentUser, assumedRoles);
final var entities = assetRepo.findAllByDebitorUuid(debitorUuid);
final var resources = mapper.mapList(entities, HsHostingAssetResource.class);
return ResponseEntity.ok(resources);
}
@Override
@Transactional
public ResponseEntity<HsHostingAssetResource> addAsset(
final String currentUser,
final String assumedRoles,
final HsHostingAssetInsertResource body) {
context.define(currentUser, assumedRoles);
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = assetRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/hosting/assets/{id}")
.buildAndExpand(saved.getUuid())
.toUri();
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.created(uri).body(mapped);
}
@Override
@Transactional(readOnly = true)
public ResponseEntity<HsHostingAssetResource> getAssetByUuid(
final String currentUser,
final String assumedRoles,
final UUID serverUuid) {
context.define(currentUser, assumedRoles);
final var result = assetRepo.findByUuid(serverUuid);
return result
.map(serverEntity -> ResponseEntity.ok(
mapper.map(serverEntity, HsHostingAssetResource.class)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@Override
@Transactional
public ResponseEntity<Void> deleteAssetUuid(
final String currentUser,
final String assumedRoles,
final UUID serverUuid) {
context.define(currentUser, assumedRoles);
final var result = assetRepo.deleteByUuid(serverUuid);
return result == 0
? ResponseEntity.notFound().build()
: ResponseEntity.noContent().build();
}
@Override
@Transactional
public ResponseEntity<HsHostingAssetResource> patchAsset(
final String currentUser,
final String assumedRoles,
final UUID serverUuid,
final HsHostingAssetPatchResource body) {
context.define(currentUser, assumedRoles);
final var current = assetRepo.findByUuid(serverUuid).orElseThrow();
new HsHostingAssetEntityPatcher(current).apply(body);
final var saved = assetRepo.save(current);
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.ok(mapped);
}
@SuppressWarnings("unchecked")
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putConfig(KeyValueMap.from(resource.getConfig()));
};
}

View File

@ -0,0 +1,180 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import io.hypersistence.utils.hibernate.type.json.JsonType;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
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.rbac.rbacdef.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Builder
@Entity
@Table(name = "hs_hosting_asset_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getBookingItem)
.withProp(HsHostingAssetEntity::getType)
.withProp(HsHostingAssetEntity::getParentAsset)
.withProp(HsHostingAssetEntity::getIdentifier)
.withProp(HsHostingAssetEntity::getCaption)
.withProp(HsHostingAssetEntity::getConfig)
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@ManyToOne(optional = false)
@JoinColumn(name = "bookingitemuuid")
private HsBookingItemEntity bookingItem;
@ManyToOne(optional = true)
@JoinColumn(name = "parentassetuuid")
private HsHostingAssetEntity parentAsset;
@Column(name = "type")
@Enumerated(EnumType.STRING)
private HsHostingAssetType type;
@Column(name = "identifier")
private String identifier; // vm1234, xyz00, example.org, xyz00_abc
@Column(name = "caption")
private String caption;
@Builder.Default
@Setter(AccessLevel.NONE)
@Type(JsonType.class)
@Column(columnDefinition = "config")
private Map<String, Object> config = new HashMap<>();
@Transient
private PatchableMapWrapper configWrapper;
public PatchableMapWrapper getConfig() {
if ( configWrapper == null ) {
configWrapper = new PatchableMapWrapper(config);
}
return configWrapper;
}
public void putConfig(Map<String, Object> entries) {
if ( configWrapper == null ) {
configWrapper = new PatchableMapWrapper(config);
}
configWrapper.assign(entries);
}
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return ofNullable(bookingItem).map(HsBookingItemEntity::toShortString).orElse("D-???????:?") +
":" + identifier;
}
public static RbacView rbac() {
return rbacViewFor("asset", HsHostingAssetEntity.class)
.withIdentityView(SQL.query("""
SELECT asset.uuid as uuid, bookingItemIV.idName || '-' || cleanIdentifier(asset.identifier) as idName
FROM hs_hosting_asset asset
JOIN hs_booking_item_iv bookingItemIV ON bookingItemIV.uuid = asset.bookingItemUuid
"""))
.withRestrictedViewOrderBy(SQL.expression("identifier"))
.withUpdatableColumns("version", "caption", "config")
.importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(),
dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.switchOnColumn("type",
inCaseOf(CLOUD_SERVER.name(),
then -> then.toRole("bookingItem", AGENT).grantPermission(INSERT)),
inCaseOf(MANAGED_SERVER.name(),
then -> then.toRole("bookingItem", AGENT).grantPermission(INSERT)),
inCaseOf(MANAGED_WEBSPACE.name(), then ->
then.importEntityAlias("parentServer", HsHostingAssetEntity.class, usingCase(MANAGED_SERVER),
dependsOnColumn("parentAssetUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
// TODO.rbac: implement multiple INSERT-rules, e.g. for Asset.bookingItem + Asset.parentAsset
//.toRole("parentServer", AGENT).grantPermission(INSERT)
)
)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("bookingItem", ADMIN);
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("bookingItem", TENANT);
with.permission(SELECT);
});
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac");
}
}

View File

@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import java.util.Optional;
public class HsHostingAssetEntityPatcher implements EntityPatcher<HsHostingAssetPatchResource> {
private final HsHostingAssetEntity entity;
public HsHostingAssetEntityPatcher(final HsHostingAssetEntity entity) {
this.entity = entity;
}
@Override
public void apply(final HsHostingAssetPatchResource resource) {
OptionalFromJson.of(resource.getCaption())
.ifPresent(entity::setCaption);
Optional.ofNullable(resource.getConfig())
.ifPresent(r -> entity.getConfig().patch(KeyValueMap.from(resource.getConfig())));
}
}

View File

@ -0,0 +1,26 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsHostingAssetRepository extends Repository<HsHostingAssetEntity, UUID> {
List<HsHostingAssetEntity> findAll();
Optional<HsHostingAssetEntity> findByUuid(final UUID serverUuid);
@Query("""
SELECT s FROM HsHostingAssetEntity s
WHERE s.bookingItem.debitor.uuid = :debitorUuid
""")
List<HsHostingAssetEntity> findAllByDebitorUuid(final UUID debitorUuid);
HsHostingAssetEntity save(HsHostingAssetEntity current);
int deleteByUuid(final UUID uuid);
long count();
}

View File

@ -0,0 +1,28 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
public enum HsHostingAssetType {
CLOUD_SERVER, // named e.g. vm1234
MANAGED_SERVER, // named e.g. vm1234
MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00
UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc
DOMAIN_SETUP(UNIX_USER), // named e.g. example.org
// TODO.spec: SECURE_MX
EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc
EMAIL_ADDRESS(DOMAIN_SETUP), // named e.g. sample@example.org
PGSQL_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc
PGSQL_DATABASE(MANAGED_WEBSPACE), // named e.g. xyz00_abc, TODO.spec: or PGSQL_USER?
MARIADB_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc
MARIADB_DATABASE(MANAGED_WEBSPACE); // named e.g. xyz00_abc, TODO.spec: or MARIADB_USER?
public final HsHostingAssetType parentAssetType;
HsHostingAssetType(final HsHostingAssetType parentAssetType) {
this.parentAssetType = parentAssetType;
}
HsHostingAssetType() {
this(null);
}
}