Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2026 Alliander N.V.
//
// SPDX-License-Identifier: Apache-2.0
package org.lfenergy.compas.scl.data.rest;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;

import static org.lfenergy.compas.scl.data.SclDataServiceConstants.GLOBAL_TENANT;

/**
* Service responsible for resolving the tenant name for the current request.
* <p>
* Tenant resolution rules:
* <ul>
* <li>When authentication is <b>disabled</b> (no JWT issuer), the hardcoded
* tenant {@code "global"} is used.</li>
* <li>When <b>OIDC authentication is enabled</b>, the tenant name is derived
* from the realm segment of the JWT issuer URL
* (e.g. {@code http://host/auth/realms/compas} → tenant {@code "compas"}).</li>
* </ul>
*/
@ApplicationScoped
public class TenantService {

private final JsonWebToken jsonWebToken;

@Inject
public TenantService(JsonWebToken jsonWebToken) {
this.jsonWebToken = jsonWebToken;
}

/**
* Resolve the tenant name for the active request.
*
* @return The resolved tenant name; never {@code null}.
*/
public String resolveTenant() {
try {
String issuer = jsonWebToken.getIssuer();
if (issuer == null || issuer.isBlank()) {
return GLOBAL_TENANT;
}
// Issuer URL format (Keycloak): http://<host>/auth/realms/<realm>
// Strip any trailing slash so that "http://host/realms/compas/" is treated
// the same as "http://host/realms/compas".
String normalized = issuer.endsWith("/") ? issuer.substring(0, issuer.length() - 1) : issuer;
int lastSlash = normalized.lastIndexOf('/');
// Guard against URLs with no path (e.g. "http://host") by ensuring the
// character before lastSlash is not also a slash (i.e. not the "://" part).
if (lastSlash > 0 && normalized.charAt(lastSlash - 1) != '/') {
String segment = normalized.substring(lastSlash + 1);
if (!segment.isBlank()) {
return segment;
}
}
} catch (Exception e) {
// No token available – fall back to global tenant.
}
return GLOBAL_TENANT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.lfenergy.compas.scl.data.exception.CompasInvalidInputException;
import org.lfenergy.compas.scl.data.model.PluginsCustomResource;
import org.lfenergy.compas.scl.data.rest.PluginsCustomResourcesApi;
import org.lfenergy.compas.scl.data.rest.TenantService;
import org.lfenergy.compas.scl.data.rest.dto.DataEntry;
import org.lfenergy.compas.scl.data.rest.dto.DataEntryWithContent;
import org.lfenergy.compas.scl.data.rest.dto.PagedDataEntryResponse;
Expand All @@ -32,10 +33,12 @@ public class CompasPluginsResource implements PluginsCustomResourcesApi {
private static final Logger LOGGER = LogManager.getLogger(CompasPluginsResource.class);

private final CompasPluginsResourceService service;
private final TenantService tenantService;

@Inject
public CompasPluginsResource(CompasPluginsResourceService service) {
public CompasPluginsResource(CompasPluginsResourceService service, TenantService tenantService) {
this.service = service;
this.tenantService = tenantService;
}

@Override
Expand All @@ -46,9 +49,10 @@ public PagedDataEntryResponse getAllData(String type,
Integer page,
Integer size) {
LOGGER.info("Listing plugins custom resources for type '{}'", type);
String tenant = tenantService.resolveTenant();

var entities = service.list(type, uploadedAfter, uploadedBefore, name, page, size);
long totalElements = service.count(type, uploadedAfter, uploadedBefore, name);
var entities = service.list(tenant, type, uploadedAfter, uploadedBefore, name, page, size);
long totalElements = service.count(tenant, type, uploadedAfter, uploadedBefore, name);

var entries = entities.stream()
.map(this::toDataEntry)
Expand Down Expand Up @@ -89,7 +93,8 @@ public UploadDataResponse uploadData(String type,
throw new CompasInvalidInputException("Failed to read content from upload");
}

var entity = service.upload(new UploadCustomPluginsResourceData(type, name, contentType, contentText,
String tenant = tenantService.resolveTenant();
var entity = service.upload(tenant, new UploadCustomPluginsResourceData(type, name, contentType, contentText,
dataCompatibilityVersion, description, version, nextVersionType));

var response = new UploadDataResponse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.apache.logging.log4j.Logger;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.lfenergy.compas.scl.data.model.Version;
import org.lfenergy.compas.scl.data.rest.TenantService;
import org.lfenergy.compas.scl.data.rest.UserInfoProperties;
import org.lfenergy.compas.scl.data.rest.v1.model.*;
import org.lfenergy.compas.scl.data.service.CompasSclDataService;
Expand Down Expand Up @@ -38,6 +39,9 @@ public class CompasSclDataResource {
@Inject
UserInfoProperties userInfoProperties;

@Inject
TenantService tenantService;

@Inject
public CompasSclDataResource(CompasSclDataService compasSclDataService) {
this.compasSclDataService = compasSclDataService;
Expand All @@ -52,9 +56,11 @@ public Uni<CreateResponse> create(@PathParam(TYPE_PATH_PARAM) SclFileType type,
LOGGER.info("Adding new SCL File for type {} to storage.", type);
String who = jsonWebToken.getClaim(userInfoProperties.who());
LOGGER.trace("Username used for Who {}", who);
String tenant = tenantService.resolveTenant();
LOGGER.trace("Tenant resolved as {}", tenant);

var response = new CreateResponse();
response.setSclData(compasSclDataService.create(type, request.getName(), who, request.getComment(),
response.setSclData(compasSclDataService.create(tenant, type, request.getName(), who, request.getComment(),
request.getSclData()));
return Uni.createFrom().item(response);
}
Expand All @@ -64,8 +70,9 @@ public Uni<CreateResponse> create(@PathParam(TYPE_PATH_PARAM) SclFileType type,
@Produces(MediaType.APPLICATION_XML)
public Uni<ListResponse> list(@PathParam(TYPE_PATH_PARAM) SclFileType type) {
LOGGER.info("Listing SCL Files for type {} from storage.", type);
String tenant = tenantService.resolveTenant();
var response = new ListResponse();
response.setItems(compasSclDataService.list(type));
response.setItems(compasSclDataService.list(tenant, type));
return Uni.createFrom().item(response);
}

Expand All @@ -75,8 +82,9 @@ public Uni<ListResponse> list(@PathParam(TYPE_PATH_PARAM) SclFileType type) {
public Uni<VersionsResponse> listVersionsByUUID(@PathParam(TYPE_PATH_PARAM) SclFileType type,
@PathParam(ID_PATH_PARAM) UUID id) {
LOGGER.info("Listing versions of SCL File {} for type {} from storage.", id, type);
String tenant = tenantService.resolveTenant();
var response = new VersionsResponse();
response.setItems(compasSclDataService.listVersionsByUUID(type, id));
response.setItems(compasSclDataService.listVersionsByUUID(tenant, type, id));
return Uni.createFrom().item(response);
}

Expand All @@ -86,8 +94,9 @@ public Uni<VersionsResponse> listVersionsByUUID(@PathParam(TYPE_PATH_PARAM) SclF
public Uni<GetResponse> findByUUID(@PathParam(TYPE_PATH_PARAM) SclFileType type,
@PathParam(ID_PATH_PARAM) UUID id) {
LOGGER.info("Retrieving latest version of SCL File {} for type {} from storage.", id, type);
String tenant = tenantService.resolveTenant();
var response = new GetResponse();
response.setSclData(compasSclDataService.findByUUID(type, id));
response.setSclData(compasSclDataService.findByUUID(tenant, type, id));
return Uni.createFrom().item(response);
}

Expand All @@ -98,8 +107,9 @@ public Uni<GetResponse> findByUUIDAndVersion(@PathParam(TYPE_PATH_PARAM) SclFile
@PathParam(ID_PATH_PARAM) UUID id,
@PathParam(VERSION_PATH_PARAM) Version version) {
LOGGER.info("Retrieving version {} of SCL File {} for type {} from storage.", version, id, type);
String tenant = tenantService.resolveTenant();
var response = new GetResponse();
response.setSclData(compasSclDataService.findByUUID(type, id, version));
response.setSclData(compasSclDataService.findByUUID(tenant, type, id, version));
return Uni.createFrom().item(response);
}

Expand All @@ -114,9 +124,11 @@ public Uni<UpdateResponse> update(@PathParam(TYPE_PATH_PARAM) SclFileType type,
LOGGER.info("Updating SCL File {} for type {} to storage.", id, type);
String who = jsonWebToken.getClaim(userInfoProperties.who());
LOGGER.trace("Username used for Who {}", who);
String tenant = tenantService.resolveTenant();
LOGGER.trace("Tenant resolved as {}", tenant);

var response = new UpdateResponse();
response.setSclData(compasSclDataService.update(type, id, request.getChangeSetType(), who, request.getComment(),
response.setSclData(compasSclDataService.update(tenant, type, id, request.getChangeSetType(), who, request.getComment(),
request.getSclData()));
return Uni.createFrom().item(response);
}
Expand All @@ -128,7 +140,8 @@ public Uni<UpdateResponse> update(@PathParam(TYPE_PATH_PARAM) SclFileType type,
public Uni<Void> deleteAll(@PathParam(TYPE_PATH_PARAM) SclFileType type,
@PathParam(ID_PATH_PARAM) UUID id) {
LOGGER.info("Removing all versions of SCL File {} for type {} from storage.", id, type);
compasSclDataService.delete(type, id);
String tenant = tenantService.resolveTenant();
compasSclDataService.delete(tenant, type, id);
return Uni.createFrom().nullItem();
}

Expand All @@ -140,7 +153,8 @@ public Uni<Void> deleteVersion(@PathParam(TYPE_PATH_PARAM) SclFileType type,
@PathParam(ID_PATH_PARAM) UUID id,
@PathParam(VERSION_PATH_PARAM) Version version) {
LOGGER.info("Removing version {} of SCL File {} for type {} from storage.", version, id, type);
compasSclDataService.delete(type, id, version);
String tenant = tenantService.resolveTenant();
compasSclDataService.delete(tenant, type, id, version);
return Uni.createFrom().nullItem();
}

Expand All @@ -151,9 +165,10 @@ public Uni<Void> deleteVersion(@PathParam(TYPE_PATH_PARAM) SclFileType type,
public Uni<DuplicateNameCheckResponse> checkDuplicateName(@PathParam(TYPE_PATH_PARAM) SclFileType type,
@Valid DuplicateNameCheckRequest request) {
LOGGER.info("Checking for duplicate SCL File name.");
String tenant = tenantService.resolveTenant();

var response = new DuplicateNameCheckResponse();
response.setDuplicate(compasSclDataService.hasDuplicateSclName(type, request.getName()));
response.setDuplicate(compasSclDataService.hasDuplicateSclName(tenant, type, request.getName()));
return Uni.createFrom().item(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
public class CompasPluginsResourceService {

private static final Logger LOGGER = LogManager.getLogger(CompasPluginsResourceService.class);
private static final String DEFAULT_TENANT = "default";


private static final List<String> ALLOWED_CONTENT_TYPES = List.of(
Expand All @@ -44,11 +43,12 @@ public CompasPluginsResourceService(EntityManager entityManager) {
}

@Transactional(SUPPORTS)
public List<PluginsCustomResource> list(String type, Date uploadedAfter, Date uploadedBefore,
public List<PluginsCustomResource> list(String tenant, String type, Date uploadedAfter, Date uploadedBefore,
String name, int page, int size) {
var queryBuilder = new StringBuilder("SELECT e FROM PluginsCustomResource e WHERE e.type = :type");
var queryBuilder = new StringBuilder("SELECT e FROM PluginsCustomResource e WHERE e.type = :type AND e.tenant = :tenant");
var params = new HashMap<String, Object>();
params.put("type", type);
params.put("tenant", tenant);

appendFilters(queryBuilder, params, uploadedAfter, uploadedBefore, name);
queryBuilder.append(" ORDER BY e.uploadedAt DESC");
Expand All @@ -61,10 +61,11 @@ public List<PluginsCustomResource> list(String type, Date uploadedAfter, Date up
}

@Transactional(SUPPORTS)
public long count(String type, Date uploadedAfter, Date uploadedBefore, String name) {
var queryBuilder = new StringBuilder("SELECT COUNT(e) FROM PluginsCustomResource e WHERE e.type = :type");
public long count(String tenant, String type, Date uploadedAfter, Date uploadedBefore, String name) {
var queryBuilder = new StringBuilder("SELECT COUNT(e) FROM PluginsCustomResource e WHERE e.type = :type AND e.tenant = :tenant");
var params = new HashMap<String, Object>();
params.put("type", type);
params.put("tenant", tenant);

appendFilters(queryBuilder, params, uploadedAfter, uploadedBefore, name);

Expand All @@ -84,20 +85,20 @@ public PluginsCustomResource findById(UUID id) {
}

@Transactional(REQUIRED)
public PluginsCustomResource upload(UploadCustomPluginsResourceData request) {
public PluginsCustomResource upload(String tenant, UploadCustomPluginsResourceData request) {
LOGGER.info("Uploading plugins custom resource type='{}', name='{}'", request.type(), request.name());

validateContentType(request.contentType());
validateSemver(request.dataCompatibilityVersion(), "data-compatibility-version");

String resolvedVersion = resolveVersion(request.type(), request.name(), request.version(), request.nextVersionType());
String resolvedVersion = resolveVersion(tenant, request.type(), request.name(), request.version(), request.nextVersionType());

Long duplicateCount = entityManager.createQuery(
"SELECT COUNT(e) FROM PluginsCustomResource e " +
"WHERE e.type = :type AND e.tenant = :tenant AND e.name = :name AND e.version = :version",
Long.class)
.setParameter("type", request.type())
.setParameter("tenant", DEFAULT_TENANT)
.setParameter("tenant", tenant)
.setParameter("name", request.name())
.setParameter("version", resolvedVersion)
.getSingleResult();
Expand All @@ -110,7 +111,7 @@ public PluginsCustomResource upload(UploadCustomPluginsResourceData request) {

var entity = new PluginsCustomResource();
entity.type = request.type();
entity.tenant = DEFAULT_TENANT;
entity.tenant = tenant;
entity.name = request.name();
entity.description = request.description();
entity.contentType = request.contentType();
Expand All @@ -123,7 +124,7 @@ public PluginsCustomResource upload(UploadCustomPluginsResourceData request) {
return entity;
}

private String resolveVersion(String type, String name,
private String resolveVersion(String tenant, String type, String name,
String explicitVersion, String nextVersionType) {
if (explicitVersion != null && !explicitVersion.isBlank()) {
validateSemver(explicitVersion, "version");
Expand All @@ -137,20 +138,20 @@ private String resolveVersion(String type, String name,
throw new CompasInvalidInputException(
"Invalid nextVersionType: must be 'major', 'minor', or 'patch'");
}
return findLatestVersionAndIncrement(type, name, changeSetType);
return findLatestVersionAndIncrement(tenant, type, name, changeSetType);
}
throw new CompasInvalidInputException(
"Either 'version' or 'nextVersionType' must be provided");
}

private String findLatestVersionAndIncrement(String type, String name,
private String findLatestVersionAndIncrement(String tenant, String type, String name,
ChangeSetType changeSetType) {
List<PluginsCustomResource> existing = entityManager.createQuery(
"SELECT e FROM PluginsCustomResource e " +
"WHERE e.type = :type AND e.tenant = :tenant AND e.name = :name",
PluginsCustomResource.class)
.setParameter("type", type)
.setParameter("tenant", DEFAULT_TENANT)
.setParameter("tenant", tenant)
.setParameter("name", name)
.getResultList();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public CompasSclDataEventHandler(CompasSclDataService compasSclDataService) {
public void createWebsocketsEvent(CreateEventRequest request) {
new WebsocketHandler<CreateWsResponse>().execute(request.getSession(), () -> {
var response = new CreateWsResponse();
response.setSclData(compasSclDataService.create(request.getType(), request.getName(), request.getWho(),
response.setSclData(compasSclDataService.create(request.getTenant(), request.getType(), request.getName(), request.getWho(),
request.getComment(), request.getSclData()));
return response;
});
Expand All @@ -43,7 +43,7 @@ public void createWebsocketsEvent(CreateEventRequest request) {
public void getWebsocketsEvent(GetEventRequest request) {
new WebsocketHandler<GetWsResponse>().execute(request.getSession(), () -> {
var response = new GetWsResponse();
response.setSclData(compasSclDataService.findByUUID(request.getType(), request.getId()));
response.setSclData(compasSclDataService.findByUUID(request.getTenant(), request.getType(), request.getId()));
return response;
});
}
Expand All @@ -52,7 +52,7 @@ public void getWebsocketsEvent(GetEventRequest request) {
public void getVersionWebsocketsEvent(GetVersionEventRequest request) {
new WebsocketHandler<GetWsResponse>().execute(request.getSession(), () -> {
var response = new GetWsResponse();
response.setSclData(compasSclDataService.findByUUID(request.getType(), request.getId(), request.getVersion()));
response.setSclData(compasSclDataService.findByUUID(request.getTenant(), request.getType(), request.getId(), request.getVersion()));
return response;
});
}
Expand All @@ -61,7 +61,7 @@ public void getVersionWebsocketsEvent(GetVersionEventRequest request) {
public void updateWebsocketsEvent(UpdateEventRequest request) {
new WebsocketHandler<UpdateWsResponse>().execute(request.getSession(), () -> {
var response = new UpdateWsResponse();
response.setSclData(compasSclDataService.update(request.getType(), request.getId(), request.getChangeSetType(),
response.setSclData(compasSclDataService.update(request.getTenant(), request.getType(), request.getId(), request.getChangeSetType(),
request.getWho(), request.getComment(), request.getSclData()));
return response;
});
Expand Down
Loading