/* * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. */ package net.curisit.securis.services; import java.security.Principal; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import jakarta.annotation.security.RolesAllowed; 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.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.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.UriInfo; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import net.curisit.integrity.commons.Utils; import net.curisit.securis.DefaultExceptionHandler; import net.curisit.securis.SeCurisException; import net.curisit.securis.db.License; import net.curisit.securis.db.LicenseStatus; import net.curisit.securis.db.LicenseType; import net.curisit.securis.db.Organization; import net.curisit.securis.db.Pack; import net.curisit.securis.db.PackMetadata; 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.MetadataHelper; import net.curisit.securis.utils.LicUtils; import net.curisit.securis.utils.TokenHelper; /** * PackResource *
* Manages Packs (group of licenses bound to an organization, application/type, * and configuration/metadata). Provides list/filter, get, create, modify, * state transitions (activate/hold/cancel) and deletion. * * @author JRA * Last reviewed by JRA on Oct 5, 2025. */ @Path("/pack") public class PackResource { private static final Logger LOG = LogManager.getLogger(PackResource.class); @Inject TokenHelper tokenHelper; @Inject MetadataHelper metadataHelper; @Inject private LicenseHelper licenseHelper; @Context EntityManager em; /** * index *
* List packs with optional filters (organizationId, applicationId, licenseTypeId).
* For non-admins, results are scoped by both apps and orgs from {@link BasicSecurityContext}.
*
* @param uriInfo supplies query parameters.
* @param bsc security scope/roles.
* @return 200 OK with the list (possibly empty).
*/
@GET
@Path("/")
@Securable
@Produces({ MediaType.APPLICATION_JSON })
public Response index(@Context UriInfo uriInfo, @Context BasicSecurityContext bsc) {
LOG.info("Getting packs list ");
MultivaluedMap
* Fetch a pack by id. If the caller is an ADVANCE user, validates
* the organization scope.
*
* @param packId pack id.
* @param bsc security context.
* @return 200 OK with entity, or 404/401 accordingly.
*/
@GET
@Path("/{packId}")
@Securable
@Produces({ MediaType.APPLICATION_JSON })
public Response get(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) {
LOG.info("Getting pack data for id: {}: ", packId);
if (packId == null || "".equals(Integer.toString(packId))) {
LOG.error("Pack ID is mandatory");
return Response.status(Status.NOT_FOUND).build();
}
em.clear();
Pack pack = em.find(Pack.class, packId);
if (pack == null) {
LOG.error("Pack with id {} not found in DB", packId);
return Response.status(Status.NOT_FOUND).build();
}
if (bsc.isUserInRole(BasicSecurityContext.ROL_ADVANCE) && (bsc.getOrganizationsIds() == null || !bsc.getOrganizationsIds().contains(pack.getOrgId()))) {
return generateErrorUnathorizedAccess(pack, bsc.getUserPrincipal());
}
return Response.ok(pack).build();
}
/**
* create
*
* Create a new pack. Validates code uniqueness, sets organization and
* license type references, stamps creator and timestamps, and persists
* metadata entries.
*
* @param pack payload.
* @param bsc security context (for createdBy).
* @return 200 OK with created pack or 404 when references not found.
* @throws SeCurisServiceException on duplicated code.
*/
@POST
@Path("/")
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON })
@EnsureTransaction
public Response create(Pack pack, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
LOG.info("Creating new pack");
if (checkIfCodeExists(pack.getCode(), em)) {
throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is already used in an existing pack");
}
try {
setPackOrganization(pack, pack.getOrgId(), em);
} catch (SeCurisException e) {
return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
}
try {
setPackLicenseType(pack, pack.getLicTypeId(), em);
} catch (SeCurisException e) {
return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
}
User user = em.find(User.class, bsc.getUserPrincipal().getName());
pack.setStatus(PackStatus.CREATED);
pack.setCreatedBy(user);
pack.setCreationTimestamp(new Date());
em.persist(pack);
Set
* Update a pack basic fields and reconcile metadata (remove/merge/persist).
* If metadata keys changed, marks dependent licenses metadata as obsolete via
* {@link MetadataHelper#markObsoleteMetadata}.
*
* @param pack payload values.
* @param packId target id.
* @return 200 OK with updated pack or 404 on ref errors.
*/
@PUT
@POST
@Path("/{packId}")
@EnsureTransaction
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON })
public Response modify(Pack pack, @PathParam("packId") Integer packId) {
LOG.info("Modifying pack with id: {}", packId);
Pack currentPack = em.find(Pack.class, packId);
try {
setPackOrganization(currentPack, pack.getOrgId(), em);
} catch (SeCurisException e) {
return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
}
try {
setPackLicenseType(currentPack, pack.getLicTypeId(), em);
} catch (SeCurisException e) {
return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, e.getMessage()).build();
}
currentPack.setLicensePreactivation(pack.isLicensePreactivation());
currentPack.setCode(pack.getCode());
currentPack.setComments(pack.getComments());
currentPack.setNumLicenses(pack.getNumLicenses());
currentPack.setPreactivationValidPeriod(pack.getPreactivationValidPeriod());
currentPack.setRenewValidPeriod(pack.getRenewValidPeriod());
currentPack.setInitValidDate(pack.getInitValidDate());
currentPack.setEndValidDate(pack.getEndValidDate());
Set
* Move a pack to ACTIVE (only from allowed states).
*
* @param packId target pack id.
* @return 200 OK with updated pack or error when invalid transition.
* @throws SeCurisServiceException when invalid state transition.
*/
@POST
@Path("/{packId}/activate")
@EnsureTransaction
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON })
public Response activate(@PathParam("packId") Integer packId) throws SeCurisServiceException {
LOG.info("Activating pack with id: {}", packId);
Pack currentPack = em.find(Pack.class, packId);
if (!Pack.Status.isActionValid(Pack.Action.ACTIVATION, currentPack.getStatus())) {
LOG.error("Pack with id {} cannot be activaed from status {}", packId, currentPack.getStatus().name());
throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "Pack cannot be activated in status: " + currentPack.getStatus().name());
}
currentPack.setStatus(PackStatus.ACTIVE);
em.persist(currentPack);
return Response.ok(currentPack).build();
}
/**
* onhold
*
* Put a pack ON_HOLD from allowed states.
*
* @param packId id.
* @return 200 OK with updated pack or error on invalid state.
* @throws SeCurisServiceException on invalid state.
*/
@POST
@Path("/{packId}/putonhold}")
@EnsureTransaction
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON })
public Response onhold(@PathParam("packId") Integer packId) throws SeCurisServiceException {
LOG.info("Putting On hold pack with id: {}", packId);
Pack currentPack = em.find(Pack.class, packId);
if (!Pack.Status.isActionValid(Pack.Action.PUT_ONHOLD, currentPack.getStatus())) {
LOG.error("Pack with id {} cannot be put on hold from status {}", packId, currentPack.getStatus().name());
throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "Pack cannot be put on hold in status: " + currentPack.getStatus().name());
}
currentPack.setStatus(PackStatus.ON_HOLD);
em.persist(currentPack);
return Response.ok(currentPack).build();
}
/**
* cancel
*
* Cancel a pack. Cascades cancel to ACTIVE/PRE_ACTIVE licenses in the pack
* via {@link LicenseHelper#cancelLicense}.
*
* @param packId id.
* @param reason cancellation reason.
* @param bsc actor for history entries.
* @return 200 OK with updated pack.
* @throws SeCurisServiceException on invalid state.
*/
@POST
@Path("/{packId}/cancel")
@EnsureTransaction
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@Consumes(MediaType.APPLICATION_JSON)
@Produces({ MediaType.APPLICATION_JSON })
public Response cancel(@PathParam("packId") Integer packId, @FormParam("reason") String reason, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
LOG.info("Cancelling pack with id: {}", packId);
Pack currentPack = em.find(Pack.class, packId);
if (!Pack.Status.isActionValid(Pack.Action.CANCEL, currentPack.getStatus())) {
LOG.error("Pack with id {} cannot cancelled from status {}", packId, currentPack.getStatus().name());
throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "Pack cannot be cancelled in status: " + currentPack.getStatus().name());
}
Set
* Compute the next available license code for a pack, by asking the helper
* for the next numeric suffix and composing with {@link LicUtils}.
*
* @param packId id.
* @param bsc (unused) security context.
* @return 200 OK with the full code text.
* @throws SeCurisServiceException if packId missing.
*/
@GET
@Path("/{packId}/next_license_code")
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@Produces({ MediaType.TEXT_PLAIN })
public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
if (packId == null) {
throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
}
Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
Pack pack = em.find(Pack.class, packId);
String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
return Response.ok(licCode).build();
}
/**
* delete
*
* Delete a pack after ensuring there are no ACTIVE/PRE_ACTIVE licenses.
* Removes remaining licenses then the pack itself.
*
* @param packId String id.
* @return 200 OK with success map, 404 if missing, or 409 if active license exists.
* @throws SeCurisServiceException on constraint errors.
*/
@DELETE
@Path("/{packId}")
@Securable(roles = Rol.ADMIN | Rol.ADVANCE)
@RolesAllowed(BasicSecurityContext.ROL_ADMIN)
@EnsureTransaction
@Produces({ MediaType.APPLICATION_JSON })
public Response delete(@PathParam("packId") String packId) throws SeCurisServiceException {
LOG.info("Deleting pack with id: {}", packId);
Pack pack = em.find(Pack.class, Integer.parseInt(packId));
if (pack == null) {
LOG.error("Pack with id {} can not be deleted, It was not found in DB", packId);
return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Pack was not found, ID: " + packId).build();
}
Set
* Generate where clause to include to a query
*
* @param addWhere
* @param queryParams
* @return whereClause
*/
private String generateWhereFromParams(boolean addWhere, MultivaluedMap
* Build a typed query considering role-based scopes and filters.
*
* @param queryParams
* @param basicSecurityContext
*/
private TypedQuery
* Convenience 401 generator with log.
*
* @param pack
* @param user
*/
private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
LOG.error("Pack with id {} not accesible by user {}", pack, user);
return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
}
/**
* setPackLicenseType
* Set the pack type
*
* @param pack
* @param licTypeId
* @param em
* @throws SeCurisException
*/
private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
LicenseType lt = null;
if (licTypeId != null) {
lt = em.find(LicenseType.class, pack.getLicTypeId());
if (lt == null) {
LOG.error("Pack license type with id {} not found in DB", licTypeId);
throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
}
}
pack.setLicenseType(lt);
}
/**
* getMdKeys
* Get the MD keys
*
* @param mds
* @return mdKeys
*/
private Set
* Check if the code already exist
*
* @param code
* @param em
* @return codeExist
*/
private boolean checkIfCodeExists(String code, EntityManager em) {
TypedQuery
* Set the organization of the pack
*
* @param currentPack
* @param orgId
* @param em
* @throws SeCurisException
*/
private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
Organization org = null;
if (orgId != null) {
org = em.find(Organization.class, orgId);
if (org == null) {
LOG.error("Organization pack with id {} not found in DB", orgId);
throw new SeCurisException("Pack organization not found with ID: " + orgId);
}
}
currentPack.setOrganization(org);
}
}