/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.services; import java.io.File; import java.io.IOException; import java.text.MessageFormat; import java.util.Date; import java.util.List; import jakarta.inject.Inject; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.FormParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.PUT; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import net.curisit.integrity.commons.Utils; import net.curisit.securis.DefaultExceptionHandler; import net.curisit.securis.LicenseGenerator; import net.curisit.securis.SeCurisException; import net.curisit.securis.beans.LicenseBean; import net.curisit.securis.beans.RequestBean; import net.curisit.securis.beans.SignedLicenseBean; import net.curisit.securis.db.Application; import net.curisit.securis.db.BlockedRequest; import net.curisit.securis.db.License; import net.curisit.securis.db.LicenseHistory; import net.curisit.securis.db.LicenseStatus; import net.curisit.securis.db.Pack; import net.curisit.securis.db.PackStatus; import net.curisit.securis.db.User; import net.curisit.securis.db.User.Rol; import net.curisit.securis.ioc.EnsureTransaction; import net.curisit.securis.security.BasicSecurityContext; import net.curisit.securis.security.Securable; import net.curisit.securis.services.exception.SeCurisServiceException; import net.curisit.securis.services.exception.SeCurisServiceException.ErrorCodes; import net.curisit.securis.services.helpers.LicenseHelper; import net.curisit.securis.services.helpers.UserHelper; import net.curisit.securis.utils.Config; import net.curisit.securis.utils.EmailManager; import net.curisit.securis.utils.JsonUtils; import net.curisit.securis.utils.LicUtils; /** * LicenseResource *

* REST resource in charge of managing licenses: list, fetch, create, activate, * email delivery, cancel, block/unblock, modify and delete. It relies on * {@link BasicSecurityContext} to scope access (organizations/apps) and * on {@link EnsureTransaction} for mutating endpoints that need a TX. *

* Key rules: *

* * @author roberto * Last reviewed by JRA on Oct 5, 2025. */ @Path("/license") public class LicenseResource { private static final Logger LOG = LogManager.getLogger(LicenseResource.class); @Inject private EmailManager emailManager; @Inject private UserHelper userHelper; @Inject private LicenseHelper licenseHelper; @Inject private LicenseGenerator licenseGenerator; @Context EntityManager em; /** * index *

* List all licenses for a given pack. If the caller is not admin, * verifies the pack belongs to an accessible organization. * * @param packId Pack identifier to filter licenses (required). * @param bsc Security context to evaluate roles and scoping. * @return 200 OK with a list (possibly empty), or 401 if unauthorized. */ @GET @Path("/") @Securable @Produces({ MediaType.APPLICATION_JSON }) public Response index(@QueryParam("packId") Integer packId, @Context BasicSecurityContext bsc) { LOG.info("Getting licenses list "); em.clear(); if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) { Pack pack = em.find(Pack.class, packId); if (pack == null) return Response.ok().build(); if (!bsc.getOrganizationsIds().contains(pack.getOrganization().getId())) { LOG.error("Pack with id {} not accesible by user {}", pack, bsc.getUserPrincipal()); return Response.status(Status.UNAUTHORIZED) .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses") .build(); } } TypedQuery q = em.createNamedQuery("list-licenses-by-pack", License.class); q.setParameter("packId", packId); List list = q.getResultList(); return Response.ok(list).build(); } /** * get *

* Fetch a single license by id, enforcing access scope for non-admin users. * * @param licId License id (required). * @param bsc Security context. * @return 200 OK with the license. * @throws SeCurisServiceException 404 if not found, 401 if out of scope. */ @GET @Path("/{licId}") @Securable @Produces({ MediaType.APPLICATION_JSON }) public Response get(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { LOG.info("Getting organization data for id: {}: ", licId); em.clear(); License lic = getCurrentLicense(licId, bsc, em); return Response.ok(lic).build(); } /** * download *

* Download the license file. Only allowed when the license is ACTIVE * and license data exists. Adds a DOWNLOAD entry in history. * * @param licId License id. * @param bsc Security context. * @return 200 OK with the binary as application/octet-stream and a * Content-Disposition header; otherwise specific error codes. * @throws SeCurisServiceException if state or data is invalid. */ @GET @Path("/{licId}/download") @Securable @Produces({ MediaType.APPLICATION_OCTET_STREAM }) @EnsureTransaction public Response download(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { License lic = getCurrentLicense(licId, bsc, em); if (lic.getLicenseData() == null) { LOG.error("License with id {} has not license file generated", licId, bsc.getUserPrincipal()); throw new SeCurisServiceException(Status.FORBIDDEN.getStatusCode(), "License has not contain data to generate license file"); } if (!License.Status.isActionValid(License.Action.DOWNLOAD, lic.getStatus())) { LOG.error("License with id {} is not active, so It can not downloaded", licId, bsc.getUserPrincipal()); throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License is not active, so It can not be downloaded"); } em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.DOWNLOAD)); return Response.ok(lic.getLicenseData()) .header("Content-Disposition", String.format("attachment; filename=\"%s\"", lic.getPack().getLicenseType().getApplication().getLicenseFilename())).build(); } /** * activate *

* Set license to ACTIVE provided status transition is valid, pack has * available units and request data passes validation/uniqueness. * Adds an ACTIVATE entry in history. * * @param licId License id. * @param bsc Security context. * @return 200 OK with updated license. * @throws SeCurisServiceException if invalid transition, no availability or invalid request data. */ @PUT @POST @Path("/{licId}/activate") @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @EnsureTransaction @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON }) public Response activate(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { License lic = getCurrentLicense(licId, bsc, em); if (!License.Status.isActionValid(License.Action.ACTIVATION, lic.getStatus())) { LOG.error("License with id {} can not be activated from current license status", licId); throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "License with id " + licId + " can not be activated from the current license status"); } if (lic.getPack().getNumAvailables() == 0) { throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "The pack has not available licenses"); } validateRequestData(lic.getPack(), lic.getRequestData(), lic.getActivationCode()); License existingLicense = License.findActiveLicenseByRequestData(lic.getRequestData(), em); if (existingLicense != null && existingLicense.getStatus() == LicenseStatus.ACTIVE) { throw new SeCurisServiceException(ErrorCodes.NO_AVAILABLE_LICENSES, "An active license already exists for the given request data"); } lic.setStatus(LicenseStatus.ACTIVE); lic.setModificationTimestamp(new Date()); em.persist(lic); User user = userHelper.getUser(bsc.getUserPrincipal().getName(), em); em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.ACTIVATE)); return Response.ok(lic).build(); } /** * send *

* Email the license file to the license owner. Builds a temporary file * using the application license filename and cleans it afterwards. * Adds a SEND entry in history. * * @param licId License id. * @param addCC whether to CC the current operator. * @param bsc Security context. * @return 200 OK with the license (no state change). * @throws SeCurisServiceException when no license file exists or user full name is missing. * @throws SeCurisException if JSON/signature process fails. */ @SuppressWarnings("deprecation") @PUT @POST @Path("/{licId}/send") @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @EnsureTransaction @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON }) public Response send(@PathParam("licId") Integer licId, @DefaultValue("false") @FormParam("add_cc") Boolean addCC, @Context BasicSecurityContext bsc) throws SeCurisServiceException, SeCurisException { License lic = getCurrentLicense(licId, bsc, em); Application app = lic.getPack().getLicenseType().getApplication(); File licFile = null; if (lic.getLicenseData() == null) { throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "There is no license file available"); } if (lic.getFullName() == null) { throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "Please add an user name in license data to send it the license file"); } User user = userHelper.getUser(bsc.getUserPrincipal().getName(), em); try { String subject = MessageFormat.format(Config.get(Config.KEYS.EMAIL_LIC_DEFAULT_SUBJECT), lic.getPack().getAppName()); String email_tpl = IOUtils.toString(this.getClass().getResourceAsStream("/lic_email.template.en")); String body = MessageFormat.format(email_tpl, lic.getFullName(), app.getName()); licFile = licenseHelper.createTemporaryLicenseFile(lic, app.getLicenseFilename()); emailManager.sendEmail(subject, body, lic.getEmail(), addCC ? user.getEmail() : null, licFile); } catch (IOException e) { LOG.error("Error creating temporary license file", e); throw new SeCurisServiceException(Status.NOT_FOUND.getStatusCode(), "There is no license file available"); } finally { if (licFile != null) { licFile.delete(); licFile.getParentFile().delete(); } } em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.SEND, "Email sent to: " + lic.getEmail())); return Response.ok(lic).build(); } /** * cancel *

* Cancel a license (requires valid state transition and a non-null reason). * Delegates to {@link LicenseHelper#cancelLicense}. * * @param licId License id. * @param actionData DTO carrying the cancellation reason. * @param bsc Security context. * @return 200 OK with updated license. * @throws SeCurisServiceException when state is invalid or reason is missing. */ @PUT @POST @Path("/{licId}/cancel") @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @EnsureTransaction @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON }) public Response cancel(@PathParam("licId") Integer licId, CancellationLicenseActionBean actionData, @Context BasicSecurityContext bsc) throws SeCurisServiceException { License lic = getCurrentLicense(licId, bsc, em); if (!License.Status.isActionValid(License.Action.CANCEL, lic.getStatus())) { LOG.error("License with id {} can not be canceled from current license status", licId); throw new SeCurisServiceException(Status.FORBIDDEN.getStatusCode(), "License with id " + licId + " can not be canceled from the current license status"); } if (actionData.reason == null) { LOG.error("To cancel an active License we need a reason, lic ID: {}, user: {}", lic.getId(), bsc.getUserPrincipal().getName()); throw new SeCurisServiceException(Status.FORBIDDEN.getStatusCode(), "Active license with id " + licId + " can not be canceled without a reason"); } licenseHelper.cancelLicense(lic, actionData.reason, bsc, em); return Response.ok(lic).build(); } /** * create *

* Create a license. Validates: *

* If request data is provided and the Pack has availability, the license is * generated and set to ACTIVE immediately. * * @param lic License payload. * @param bsc Security context. * @return 200 OK with created license. * @throws SeCurisServiceException on validation failures. */ @POST @Path("/") @Consumes(MediaType.APPLICATION_JSON) @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @Produces({ MediaType.APPLICATION_JSON }) @EnsureTransaction public Response create(License lic, @Context BasicSecurityContext bsc) throws SeCurisServiceException { if (checkIfCodeExists(lic.getCode(), em)) { throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is already used in an existing license"); } if (lic.getActivationCode() == null) { throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The activation code is mandatory"); } License existingLic = License.findLicenseByActivationCode(lic.getActivationCode(), em); if (existingLic != null) { throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The activation code is already used in: " + existingLic.getCode()); } if (!LicUtils.checkValidLicenseCodeCrc(lic.getCode())) { throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is not valid"); } if (!Utils.isValidEmail(lic.getEmail())) { throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The user email should be a valid email"); } Pack pack = null; if (lic.getPackId() != null) { pack = em.find(Pack.class, lic.getPackId()); } if (pack == null) { LOG.error("License pack with id {} not found in DB", lic.getPackId()); throw new SeCurisServiceException(ErrorCodes.NOT_FOUND, "License's pack not found with ID: " + lic.getPackId()); } else { if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN) && !bsc.getOrganizationsIds().contains(pack.getOrganization().getId())) { LOG.error("License for pack with id {} can not be created by user {}", pack.getId(), bsc.getUserPrincipal()); throw new SeCurisServiceException(ErrorCodes.UNAUTHORIZED_ACCESS, "Unathorized action on pack license"); } if (pack.getStatus() != PackStatus.ACTIVE) { LOG.error("Current pack, {}, is not active so licenses cannot be created", pack.getId()); throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "Current pack is not active so licenses cannot be created"); } } User createdBy = userHelper.getUser(bsc.getUserPrincipal().getName(), em); if (lic.getRequestData() != null) { License existingLicense = License.findActiveLicenseByRequestData(lic.getRequestData(), em); if (existingLicense != null) { throw new SeCurisServiceException(ErrorCodes.DUPLICATED_REQUEST_DATA, "There is already an active license for current request data"); } if (pack.getNumAvailables() > 0) { SignedLicenseBean signedLicense = generateLicense(lic, em); // Move directly to ACTIVE when request data is provided lic.setStatus(LicenseStatus.ACTIVE); try { lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class)); if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be activated"); } lic.setLicenseData(JsonUtils.toJSON(signedLicense)); } catch (SeCurisException e) { LOG.error("Error generating license JSON", e); throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generating license JSON"); } } } else { lic.setStatus(LicenseStatus.CREATED); } lic.setCodeSuffix(LicUtils.getLicenseCodeSuffix(lic.getCode())); lic.setCreatedBy(createdBy); lic.setCreationTimestamp(new Date()); lic.setModificationTimestamp(lic.getCreationTimestamp()); lic.setMetadataObsolete(false); em.persist(lic); em.persist(licenseHelper.createLicenseHistoryAction(lic, createdBy, LicenseHistory.Actions.CREATE)); if (lic.getStatus() == LicenseStatus.ACTIVE) { em.persist(licenseHelper.createLicenseHistoryAction(lic, createdBy, LicenseHistory.Actions.ACTIVATE, "Activated on creation")); } return Response.ok(lic).build(); } /** * modify *

* Update license basic fields (comments, fullName, email) and, when * status is CREATED and request payload changes, re-normalize/validate and * regenerate the signed license data. Adds a MODIFY history entry. * * @param lic New values. * @param licId License id. * @param bsc Security context. * @return 200 OK with updated license. * @throws SeCurisServiceException if validation fails. */ @SuppressWarnings("deprecation") @PUT @POST @Path("/{licId}") @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @EnsureTransaction @Consumes(MediaType.APPLICATION_JSON) @Produces({ MediaType.APPLICATION_JSON }) public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { LOG.info("Modifying license with id: {}", licId); License currentLicense = getCurrentLicense(licId, bsc, em); currentLicense.setComments(lic.getComments()); currentLicense.setFullName(lic.getFullName()); currentLicense.setEmail(lic.getEmail()); if (currentLicense.getActivationCode() == null) { currentLicense.setActivationCode(lic.getActivationCode()); } if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) { if (lic.getRequestData() != null) { SignedLicenseBean signedLicense = generateLicense(lic, em); try { // Normalize the request JSON and update signed license JSON currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class)); LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData()); if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) { throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again"); } currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense)); } catch (SeCurisException e) { LOG.error("Error generaing license JSON", e); throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON"); } } else { currentLicense.setRequestData(null); } } currentLicense.setModificationTimestamp(new Date()); em.persist(currentLicense); em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY)); return Response.ok(currentLicense).build(); } /** * delete *

* Delete the license when the current status allows it. If the license * was BLOCKED, removes the BlockedRequest entry to unblock the request. * * @param licId License id. * @param bsc Security context. * @return 200 OK with a success payload. * @throws SeCurisServiceException if status does not allow deletion. */ @DELETE @Path("/{licId}") @EnsureTransaction @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @Produces({ MediaType.APPLICATION_JSON }) public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { LOG.info("Deleting license with id: {}", licId); License lic = getCurrentLicense(licId, bsc, em); if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) { LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus()); throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name()); } if (lic.getStatus() == LicenseStatus.BLOCKED) { BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash()); if (blockedReq != null) { em.remove(blockedReq); } } em.remove(lic); return Response.ok(Utils.createMap("success", true, "id", licId)).build(); } /** * block *

* Block the license request data (allowed only from CANCELLED state). * Persists a {@link BlockedRequest} and transitions the license to BLOCKED. * * @param licId License id. * @param bsc Security context. * @return 200 OK with a success payload. * @throws SeCurisServiceException if state is not CANCELLED or already blocked. */ @POST @Path("/{licId}/block") @EnsureTransaction @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @Produces({ MediaType.APPLICATION_JSON }) public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { LOG.info("Blocking license with id: {}", licId); License lic = getCurrentLicense(licId, bsc, em); if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) { LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name()); throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status"); } if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked"); } BlockedRequest blockedReq = new BlockedRequest(); blockedReq.setCreationTimestamp(new Date()); blockedReq.setBlockedBy(userHelper.getUser(bsc, em)); blockedReq.setRequestData(lic.getRequestData()); em.persist(blockedReq); lic.setStatus(LicenseStatus.BLOCKED); lic.setModificationTimestamp(new Date()); em.merge(lic); em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK)); return Response.ok(Utils.createMap("success", true, "id", licId)).build(); } /** * unblock *

* Remove the block for the license request data (if present) and move * license back to CANCELLED. Adds an UNBLOCK history entry. * * @param licId License id. * @param bsc Security context. * @return 200 OK with a success payload. * @throws SeCurisServiceException never if not blocked (returns success anyway). */ @POST @Path("/{licId}/unblock") @EnsureTransaction @Securable(roles = Rol.ADMIN | Rol.ADVANCE) @Produces({ MediaType.APPLICATION_JSON }) public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { LOG.info("Unblocking license with id: {}", licId); License lic = getCurrentLicense(licId, bsc, em); if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash()); em.remove(blockedReq); lic.setStatus(LicenseStatus.CANCELLED); lic.setModificationTimestamp(new Date()); em.merge(lic); em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK)); } else { LOG.info("Request data for license {} is NOT blocked", licId); } return Response.ok(Utils.createMap("success", true, "id", licId)).build(); } // --------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------- /** * checkIfCodeExists

* Check if there is an existing license with the same code. * * @param code * @param entityManager */ private boolean checkIfCodeExists(String code, EntityManager em) { TypedQuery query = em.createNamedQuery("license-by-code", License.class); query.setParameter("code", code); int lics = query.getResultList().size(); return lics > 0; } /** * generateLicense

* Generate a signed license from request data and pack metadata/expiration. * * @param license License with requestData and packId populated. * @param em Entity manager. * @return Signed license bean. * @throws SeCurisServiceException if validation/generation fails. */ private SignedLicenseBean generateLicense(License license, EntityManager em) throws SeCurisServiceException { SignedLicenseBean sl = null; Pack pack = em.find(Pack.class, license.getPackId()); RequestBean rb = validateRequestData(pack, license.getRequestData(), license.getActivationCode()); try { LicenseBean lb = licenseGenerator.generateLicense(rb, licenseHelper.extractPackMetadata(pack.getMetadata()), licenseHelper.getExpirationDateFromPack(pack, false), license.getCode(), pack.getAppName()); sl = new SignedLicenseBean(lb); } catch (SeCurisException e) { throw new SeCurisServiceException(ErrorCodes.INVALID_LICENSE_REQUEST_DATA, "Error generating license: " + e.toString()); } return sl; } /** * validateRequestData

* Validate that requestData matches the Pack and is well-formed. * * @param pack Target pack (org/type constraints). * @param requestData Raw JSON string with the license request. * @param activationCode Activation code from the license payload. * @return Parsed {@link RequestBean}. * @throws SeCurisServiceException on format mismatch or wrong codes. */ private RequestBean validateRequestData(Pack pack, String requestData, String activationCode) throws SeCurisServiceException { if (requestData == null) { throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA, "Request data is empty"); } RequestBean rb = null; try { rb = JsonUtils.json2object(requestData, RequestBean.class); } catch (SeCurisException e) { throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data has not a valid format"); } if (rb.getActivationCode() != null && activationCode != null) { if (!rb.getActivationCode().equals(activationCode)) { throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Activation code mismatch"); } } else { if (!rb.getCustomerCode().equals(pack.getOrganization().getCode())) { throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data not valid, wrong Organization code"); } if (!rb.getLicenseTypeCode().equals(pack.getLicenseTypeCode())) { throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data not valid, wrong License type code"); } if (rb.getPackCode() != null && !rb.getPackCode().equals(pack.getCode())) { throw new SeCurisServiceException(ErrorCodes.INVALID_REQUEST_DATA_FORMAT, "Request data not valid, wrong Pack code"); } } return rb; } /** * getCurrentLicense

* Load a license and verify scope for non-admin users. * * @param licId License id. * @param bsc Security context. * @param em Entity manager. * @return License entity. * @throws SeCurisServiceException if id is missing, not found or unauthorized. */ private License getCurrentLicense(Integer licId, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException { if (licId == null || "".equals(Integer.toString(licId))) { LOG.error("License ID is mandatory"); throw new SeCurisServiceException(Status.NOT_FOUND.getStatusCode(), "Missing license ID"); } License lic = em.find(License.class, licId); if (lic == null) { LOG.error("License with id {} not found in DB", licId); throw new SeCurisServiceException(Status.NOT_FOUND.getStatusCode(), "License not found for ID: " + licId); } if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN) && !bsc.getOrganizationsIds().contains(lic.getPack().getOrganization().getId())) { LOG.error("License with id {} is not accesible by user {}", licId, bsc.getUserPrincipal()); throw new SeCurisServiceException(Status.UNAUTHORIZED.getStatusCode(), "Unathorized access to license data"); } return lic; } // --------------------------------------------------------------------- // DTOs // --------------------------------------------------------------------- /** * DTO used to carry a cancellation reason for the cancel endpoint. */ @JsonAutoDetect @JsonIgnoreProperties(ignoreUnknown = true) static class CancellationLicenseActionBean { @JsonProperty private String reason; } }