| .. | .. |
|---|
| 1 | +/* |
|---|
| 2 | + * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved. |
|---|
| 3 | + */ |
|---|
| 1 | 4 | package net.curisit.securis.services; |
|---|
| 2 | 5 | |
|---|
| 3 | 6 | import java.io.File; |
|---|
| .. | .. |
|---|
| 63 | 66 | import net.curisit.securis.utils.LicUtils; |
|---|
| 64 | 67 | |
|---|
| 65 | 68 | /** |
|---|
| 66 | | - * License resource, this service will provide methods to create, modify and |
|---|
| 67 | | - * delete licenses |
|---|
| 68 | | - * |
|---|
| 69 | | - * @author roberto <roberto.sanchez@curisit.net> |
|---|
| 69 | + * LicenseResource |
|---|
| 70 | + * <p> |
|---|
| 71 | + * REST resource in charge of managing licenses: list, fetch, create, activate, |
|---|
| 72 | + * email delivery, cancel, block/unblock, modify and delete. It relies on |
|---|
| 73 | + * {@link BasicSecurityContext} to scope access (organizations/apps) and |
|---|
| 74 | + * on {@link EnsureTransaction} for mutating endpoints that need a TX. |
|---|
| 75 | + * <p> |
|---|
| 76 | + * Key rules: |
|---|
| 77 | + * <ul> |
|---|
| 78 | + * <li>Non-admin users must belong to the license's organization.</li> |
|---|
| 79 | + * <li>License creation validates code CRC, activation code and email.</li> |
|---|
| 80 | + * <li>Request payload must match Pack constraints (org/type/pack codes).</li> |
|---|
| 81 | + * <li>History is recorded for key actions (CREATE/ACTIVATE/DOWNLOAD/etc.).</li> |
|---|
| 82 | + * </ul> |
|---|
| 83 | + * |
|---|
| 84 | + * @author roberto |
|---|
| 85 | + * Last reviewed by JRA on Oct 5, 2025. |
|---|
| 70 | 86 | */ |
|---|
| 71 | 87 | @Path("/license") |
|---|
| 72 | 88 | public class LicenseResource { |
|---|
| 73 | 89 | |
|---|
| 74 | 90 | private static final Logger LOG = LogManager.getLogger(LicenseResource.class); |
|---|
| 75 | 91 | |
|---|
| 76 | | - @Inject |
|---|
| 77 | | - private EmailManager emailManager; |
|---|
| 92 | + @Inject private EmailManager emailManager; |
|---|
| 93 | + @Inject private UserHelper userHelper; |
|---|
| 94 | + @Inject private LicenseHelper licenseHelper; |
|---|
| 95 | + @Inject private LicenseGenerator licenseGenerator; |
|---|
| 78 | 96 | |
|---|
| 79 | | - @Inject |
|---|
| 80 | | - private UserHelper userHelper; |
|---|
| 81 | | - |
|---|
| 82 | | - @Inject |
|---|
| 83 | | - private LicenseHelper licenseHelper; |
|---|
| 84 | | - |
|---|
| 85 | | - @Context |
|---|
| 86 | | - EntityManager em; |
|---|
| 87 | | - |
|---|
| 88 | | - @Inject |
|---|
| 89 | | - private LicenseGenerator licenseGenerator; |
|---|
| 97 | + @Context EntityManager em; |
|---|
| 90 | 98 | |
|---|
| 91 | 99 | /** |
|---|
| 92 | | - * |
|---|
| 93 | | - * @return the server version in format majorVersion.minorVersion |
|---|
| 100 | + * index |
|---|
| 101 | + * <p> |
|---|
| 102 | + * List all licenses for a given pack. If the caller is not admin, |
|---|
| 103 | + * verifies the pack belongs to an accessible organization. |
|---|
| 104 | + * |
|---|
| 105 | + * @param packId Pack identifier to filter licenses (required). |
|---|
| 106 | + * @param bsc Security context to evaluate roles and scoping. |
|---|
| 107 | + * @return 200 OK with a list (possibly empty), or 401 if unauthorized. |
|---|
| 94 | 108 | */ |
|---|
| 95 | 109 | @GET |
|---|
| 96 | 110 | @Path("/") |
|---|
| .. | .. |
|---|
| 98 | 112 | @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 99 | 113 | public Response index(@QueryParam("packId") Integer packId, @Context BasicSecurityContext bsc) { |
|---|
| 100 | 114 | LOG.info("Getting licenses list "); |
|---|
| 101 | | - |
|---|
| 102 | | - // EntityManager em = emProvider.get(); |
|---|
| 103 | 115 | em.clear(); |
|---|
| 104 | 116 | |
|---|
| 105 | 117 | if (!bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) { |
|---|
| 106 | 118 | Pack pack = em.find(Pack.class, packId); |
|---|
| 107 | | - if (pack == null) { |
|---|
| 108 | | - return Response.ok().build(); |
|---|
| 109 | | - } |
|---|
| 119 | + if (pack == null) return Response.ok().build(); |
|---|
| 110 | 120 | if (!bsc.getOrganizationsIds().contains(pack.getOrganization().getId())) { |
|---|
| 111 | 121 | LOG.error("Pack with id {} not accesible by user {}", pack, bsc.getUserPrincipal()); |
|---|
| 112 | | - return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses").build(); |
|---|
| 122 | + return Response.status(Status.UNAUTHORIZED) |
|---|
| 123 | + .header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack licenses") |
|---|
| 124 | + .build(); |
|---|
| 113 | 125 | } |
|---|
| 114 | 126 | } |
|---|
| 115 | 127 | TypedQuery<License> q = em.createNamedQuery("list-licenses-by-pack", License.class); |
|---|
| 116 | 128 | q.setParameter("packId", packId); |
|---|
| 117 | 129 | List<License> list = q.getResultList(); |
|---|
| 118 | | - |
|---|
| 119 | 130 | return Response.ok(list).build(); |
|---|
| 120 | 131 | } |
|---|
| 121 | 132 | |
|---|
| 122 | 133 | /** |
|---|
| 123 | | - * |
|---|
| 124 | | - * @return the server version in format majorVersion.minorVersion |
|---|
| 125 | | - * @throws SeCurisServiceException |
|---|
| 134 | + * get |
|---|
| 135 | + * <p> |
|---|
| 136 | + * Fetch a single license by id, enforcing access scope for non-admin users. |
|---|
| 137 | + * |
|---|
| 138 | + * @param licId License id (required). |
|---|
| 139 | + * @param bsc Security context. |
|---|
| 140 | + * @return 200 OK with the license. |
|---|
| 141 | + * @throws SeCurisServiceException 404 if not found, 401 if out of scope. |
|---|
| 126 | 142 | */ |
|---|
| 127 | 143 | @GET |
|---|
| 128 | 144 | @Path("/{licId}") |
|---|
| .. | .. |
|---|
| 130 | 146 | @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 131 | 147 | public Response get(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 132 | 148 | LOG.info("Getting organization data for id: {}: ", licId); |
|---|
| 133 | | - |
|---|
| 134 | | - // EntityManager em = emProvider.get(); |
|---|
| 135 | 149 | em.clear(); |
|---|
| 136 | 150 | License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 137 | 151 | return Response.ok(lic).build(); |
|---|
| 138 | 152 | } |
|---|
| 139 | 153 | |
|---|
| 140 | 154 | /** |
|---|
| 141 | | - * |
|---|
| 142 | | - * @return The license file, only of license is active |
|---|
| 143 | | - * @throws SeCurisServiceException |
|---|
| 155 | + * download |
|---|
| 156 | + * <p> |
|---|
| 157 | + * Download the license file. Only allowed when the license is ACTIVE |
|---|
| 158 | + * and license data exists. Adds a DOWNLOAD entry in history. |
|---|
| 159 | + * |
|---|
| 160 | + * @param licId License id. |
|---|
| 161 | + * @param bsc Security context. |
|---|
| 162 | + * @return 200 OK with the binary as application/octet-stream and a |
|---|
| 163 | + * Content-Disposition header; otherwise specific error codes. |
|---|
| 164 | + * @throws SeCurisServiceException if state or data is invalid. |
|---|
| 144 | 165 | */ |
|---|
| 145 | 166 | @GET |
|---|
| 146 | 167 | @Path("/{licId}/download") |
|---|
| .. | .. |
|---|
| 149 | 170 | @EnsureTransaction |
|---|
| 150 | 171 | public Response download(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 151 | 172 | |
|---|
| 152 | | - // EntityManager em = emProvider.get(); |
|---|
| 153 | 173 | License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 154 | 174 | |
|---|
| 155 | 175 | if (lic.getLicenseData() == null) { |
|---|
| .. | .. |
|---|
| 166 | 186 | } |
|---|
| 167 | 187 | |
|---|
| 168 | 188 | /** |
|---|
| 169 | | - * Activate the given license |
|---|
| 170 | | - * |
|---|
| 171 | | - * @param licId |
|---|
| 172 | | - * @param bsc |
|---|
| 173 | | - * @return |
|---|
| 174 | | - * @throws SeCurisServiceException |
|---|
| 189 | + * activate |
|---|
| 190 | + * <p> |
|---|
| 191 | + * Set license to ACTIVE provided status transition is valid, pack has |
|---|
| 192 | + * available units and request data passes validation/uniqueness. |
|---|
| 193 | + * Adds an ACTIVATE entry in history. |
|---|
| 194 | + * |
|---|
| 195 | + * @param licId License id. |
|---|
| 196 | + * @param bsc Security context. |
|---|
| 197 | + * @return 200 OK with updated license. |
|---|
| 198 | + * @throws SeCurisServiceException if invalid transition, no availability or invalid request data. |
|---|
| 175 | 199 | */ |
|---|
| 176 | 200 | @PUT |
|---|
| 177 | 201 | @POST |
|---|
| .. | .. |
|---|
| 182 | 206 | @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 183 | 207 | public Response activate(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 184 | 208 | |
|---|
| 185 | | - // EntityManager em = emProvider.get(); |
|---|
| 186 | 209 | License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 187 | 210 | |
|---|
| 188 | 211 | if (!License.Status.isActionValid(License.Action.ACTIVATION, lic.getStatus())) { |
|---|
| .. | .. |
|---|
| 211 | 234 | } |
|---|
| 212 | 235 | |
|---|
| 213 | 236 | /** |
|---|
| 214 | | - * Send license file by email to the organization |
|---|
| 215 | | - * |
|---|
| 216 | | - * @param licId |
|---|
| 217 | | - * @param bsc |
|---|
| 218 | | - * @return |
|---|
| 219 | | - * @throws SeCurisServiceException |
|---|
| 237 | + * send |
|---|
| 238 | + * <p> |
|---|
| 239 | + * Email the license file to the license owner. Builds a temporary file |
|---|
| 240 | + * using the application license filename and cleans it afterwards. |
|---|
| 241 | + * Adds a SEND entry in history. |
|---|
| 242 | + * |
|---|
| 243 | + * @param licId License id. |
|---|
| 244 | + * @param addCC whether to CC the current operator. |
|---|
| 245 | + * @param bsc Security context. |
|---|
| 246 | + * @return 200 OK with the license (no state change). |
|---|
| 247 | + * @throws SeCurisServiceException when no license file exists or user full name is missing. |
|---|
| 248 | + * @throws SeCurisException if JSON/signature process fails. |
|---|
| 220 | 249 | */ |
|---|
| 221 | 250 | @SuppressWarnings("deprecation") |
|---|
| 222 | 251 | @PUT |
|---|
| .. | .. |
|---|
| 229 | 258 | public Response send(@PathParam("licId") Integer licId, @DefaultValue("false") @FormParam("add_cc") Boolean addCC, @Context BasicSecurityContext bsc) |
|---|
| 230 | 259 | throws SeCurisServiceException, SeCurisException { |
|---|
| 231 | 260 | |
|---|
| 232 | | - // EntityManager em = emProvider.get(); |
|---|
| 233 | 261 | License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 234 | 262 | Application app = lic.getPack().getLicenseType().getApplication(); |
|---|
| 235 | 263 | File licFile = null; |
|---|
| .. | .. |
|---|
| 259 | 287 | } |
|---|
| 260 | 288 | } |
|---|
| 261 | 289 | |
|---|
| 262 | | - // lic.setModificationTimestamp(new Date()); |
|---|
| 263 | | - // em.merge(lic); |
|---|
| 264 | 290 | em.persist(licenseHelper.createLicenseHistoryAction(lic, user, LicenseHistory.Actions.SEND, "Email sent to: " + lic.getEmail())); |
|---|
| 265 | 291 | return Response.ok(lic).build(); |
|---|
| 266 | 292 | } |
|---|
| 267 | 293 | |
|---|
| 268 | 294 | /** |
|---|
| 269 | | - * Cancel given license |
|---|
| 270 | | - * |
|---|
| 271 | | - * @param licId |
|---|
| 272 | | - * @param bsc |
|---|
| 273 | | - * @return |
|---|
| 274 | | - * @throws SeCurisServiceException |
|---|
| 295 | + * cancel |
|---|
| 296 | + * <p> |
|---|
| 297 | + * Cancel a license (requires valid state transition and a non-null reason). |
|---|
| 298 | + * Delegates to {@link LicenseHelper#cancelLicense}. |
|---|
| 299 | + * |
|---|
| 300 | + * @param licId License id. |
|---|
| 301 | + * @param actionData DTO carrying the cancellation reason. |
|---|
| 302 | + * @param bsc Security context. |
|---|
| 303 | + * @return 200 OK with updated license. |
|---|
| 304 | + * @throws SeCurisServiceException when state is invalid or reason is missing. |
|---|
| 275 | 305 | */ |
|---|
| 276 | 306 | @PUT |
|---|
| 277 | 307 | @POST |
|---|
| .. | .. |
|---|
| 282 | 312 | @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 283 | 313 | public Response cancel(@PathParam("licId") Integer licId, CancellationLicenseActionBean actionData, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 284 | 314 | |
|---|
| 285 | | - // EntityManager em = emProvider.get(); |
|---|
| 286 | 315 | License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 287 | 316 | |
|---|
| 288 | 317 | if (!License.Status.isActionValid(License.Action.CANCEL, lic.getStatus())) { |
|---|
| .. | .. |
|---|
| 300 | 329 | } |
|---|
| 301 | 330 | |
|---|
| 302 | 331 | /** |
|---|
| 303 | | - * Check if there is some pack with the same code |
|---|
| 304 | | - * |
|---|
| 305 | | - * @param code |
|---|
| 306 | | - * Pack code |
|---|
| 307 | | - * @param em |
|---|
| 308 | | - * DB session object |
|---|
| 309 | | - * @return <code>true</code> if code is already used, <code>false</code> |
|---|
| 310 | | - * otherwise |
|---|
| 332 | + * create |
|---|
| 333 | + * <p> |
|---|
| 334 | + * Create a license. Validates: |
|---|
| 335 | + * <ul> |
|---|
| 336 | + * <li>Unique license code and valid CRC.</li> |
|---|
| 337 | + * <li>Activation code presence and uniqueness.</li> |
|---|
| 338 | + * <li>Valid user email.</li> |
|---|
| 339 | + * <li>Pack existence, ACTIVE status and scope authorization.</li> |
|---|
| 340 | + * <li>Request data consistency and unblock status (if provided).</li> |
|---|
| 341 | + * </ul> |
|---|
| 342 | + * If request data is provided and the Pack has availability, the license is |
|---|
| 343 | + * generated and set to ACTIVE immediately. |
|---|
| 344 | + * |
|---|
| 345 | + * @param lic License payload. |
|---|
| 346 | + * @param bsc Security context. |
|---|
| 347 | + * @return 200 OK with created license. |
|---|
| 348 | + * @throws SeCurisServiceException on validation failures. |
|---|
| 311 | 349 | */ |
|---|
| 312 | | - private boolean checkIfCodeExists(String code, EntityManager em) { |
|---|
| 313 | | - TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class); |
|---|
| 314 | | - query.setParameter("code", code); |
|---|
| 315 | | - int lics = query.getResultList().size(); |
|---|
| 316 | | - return lics > 0; |
|---|
| 317 | | - } |
|---|
| 318 | | - |
|---|
| 319 | 350 | @POST |
|---|
| 320 | 351 | @Path("/") |
|---|
| 321 | 352 | @Consumes(MediaType.APPLICATION_JSON) |
|---|
| .. | .. |
|---|
| 323 | 354 | @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 324 | 355 | @EnsureTransaction |
|---|
| 325 | 356 | public Response create(License lic, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 326 | | - // EntityManager em = emProvider.get(); |
|---|
| 327 | | - |
|---|
| 328 | 357 | if (checkIfCodeExists(lic.getCode(), em)) { |
|---|
| 329 | 358 | throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The license code is already used in an existing license"); |
|---|
| 330 | 359 | } |
|---|
| .. | .. |
|---|
| 370 | 399 | } |
|---|
| 371 | 400 | |
|---|
| 372 | 401 | if (pack.getNumAvailables() > 0) { |
|---|
| 373 | | - |
|---|
| 374 | 402 | SignedLicenseBean signedLicense = generateLicense(lic, em); |
|---|
| 375 | | - // If user provide a request data the license status is passed |
|---|
| 376 | | - // directly to ACTIVE |
|---|
| 403 | + // Move directly to ACTIVE when request data is provided |
|---|
| 377 | 404 | lic.setStatus(LicenseStatus.ACTIVE); |
|---|
| 378 | 405 | try { |
|---|
| 379 | 406 | lic.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class)); |
|---|
| .. | .. |
|---|
| 404 | 431 | return Response.ok(lic).build(); |
|---|
| 405 | 432 | } |
|---|
| 406 | 433 | |
|---|
| 434 | + /** |
|---|
| 435 | + * modify |
|---|
| 436 | + * <p> |
|---|
| 437 | + * Update license basic fields (comments, fullName, email) and, when |
|---|
| 438 | + * status is CREATED and request payload changes, re-normalize/validate and |
|---|
| 439 | + * regenerate the signed license data. Adds a MODIFY history entry. |
|---|
| 440 | + * |
|---|
| 441 | + * @param lic New values. |
|---|
| 442 | + * @param licId License id. |
|---|
| 443 | + * @param bsc Security context. |
|---|
| 444 | + * @return 200 OK with updated license. |
|---|
| 445 | + * @throws SeCurisServiceException if validation fails. |
|---|
| 446 | + */ |
|---|
| 447 | + @SuppressWarnings("deprecation") |
|---|
| 448 | + @PUT |
|---|
| 449 | + @POST |
|---|
| 450 | + @Path("/{licId}") |
|---|
| 451 | + @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 452 | + @EnsureTransaction |
|---|
| 453 | + @Consumes(MediaType.APPLICATION_JSON) |
|---|
| 454 | + @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 455 | + public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 456 | + LOG.info("Modifying license with id: {}", licId); |
|---|
| 457 | + |
|---|
| 458 | + License currentLicense = getCurrentLicense(licId, bsc, em); |
|---|
| 459 | + currentLicense.setComments(lic.getComments()); |
|---|
| 460 | + currentLicense.setFullName(lic.getFullName()); |
|---|
| 461 | + currentLicense.setEmail(lic.getEmail()); |
|---|
| 462 | + if (currentLicense.getActivationCode() == null) { |
|---|
| 463 | + currentLicense.setActivationCode(lic.getActivationCode()); |
|---|
| 464 | + } |
|---|
| 465 | + |
|---|
| 466 | + if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) { |
|---|
| 467 | + if (lic.getRequestData() != null) { |
|---|
| 468 | + SignedLicenseBean signedLicense = generateLicense(lic, em); |
|---|
| 469 | + try { |
|---|
| 470 | + // Normalize the request JSON and update signed license JSON |
|---|
| 471 | + currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class)); |
|---|
| 472 | + LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData()); |
|---|
| 473 | + if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) { |
|---|
| 474 | + throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again"); |
|---|
| 475 | + } |
|---|
| 476 | + currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense)); |
|---|
| 477 | + } catch (SeCurisException e) { |
|---|
| 478 | + LOG.error("Error generaing license JSON", e); |
|---|
| 479 | + throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON"); |
|---|
| 480 | + } |
|---|
| 481 | + } else { |
|---|
| 482 | + currentLicense.setRequestData(null); |
|---|
| 483 | + } |
|---|
| 484 | + } |
|---|
| 485 | + |
|---|
| 486 | + currentLicense.setModificationTimestamp(new Date()); |
|---|
| 487 | + em.persist(currentLicense); |
|---|
| 488 | + em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY)); |
|---|
| 489 | + |
|---|
| 490 | + return Response.ok(currentLicense).build(); |
|---|
| 491 | + } |
|---|
| 492 | + |
|---|
| 493 | + /** |
|---|
| 494 | + * delete |
|---|
| 495 | + * <p> |
|---|
| 496 | + * Delete the license when the current status allows it. If the license |
|---|
| 497 | + * was BLOCKED, removes the BlockedRequest entry to unblock the request. |
|---|
| 498 | + * |
|---|
| 499 | + * @param licId License id. |
|---|
| 500 | + * @param bsc Security context. |
|---|
| 501 | + * @return 200 OK with a success payload. |
|---|
| 502 | + * @throws SeCurisServiceException if status does not allow deletion. |
|---|
| 503 | + */ |
|---|
| 504 | + @DELETE |
|---|
| 505 | + @Path("/{licId}") |
|---|
| 506 | + @EnsureTransaction |
|---|
| 507 | + @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 508 | + @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 509 | + public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 510 | + LOG.info("Deleting license with id: {}", licId); |
|---|
| 511 | + License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 512 | + |
|---|
| 513 | + if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) { |
|---|
| 514 | + LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus()); |
|---|
| 515 | + throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name()); |
|---|
| 516 | + } |
|---|
| 517 | + if (lic.getStatus() == LicenseStatus.BLOCKED) { |
|---|
| 518 | + BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash()); |
|---|
| 519 | + if (blockedReq != null) { |
|---|
| 520 | + em.remove(blockedReq); |
|---|
| 521 | + } |
|---|
| 522 | + } |
|---|
| 523 | + |
|---|
| 524 | + em.remove(lic); |
|---|
| 525 | + return Response.ok(Utils.createMap("success", true, "id", licId)).build(); |
|---|
| 526 | + } |
|---|
| 527 | + |
|---|
| 528 | + /** |
|---|
| 529 | + * block |
|---|
| 530 | + * <p> |
|---|
| 531 | + * Block the license request data (allowed only from CANCELLED state). |
|---|
| 532 | + * Persists a {@link BlockedRequest} and transitions the license to BLOCKED. |
|---|
| 533 | + * |
|---|
| 534 | + * @param licId License id. |
|---|
| 535 | + * @param bsc Security context. |
|---|
| 536 | + * @return 200 OK with a success payload. |
|---|
| 537 | + * @throws SeCurisServiceException if state is not CANCELLED or already blocked. |
|---|
| 538 | + */ |
|---|
| 539 | + @POST |
|---|
| 540 | + @Path("/{licId}/block") |
|---|
| 541 | + @EnsureTransaction |
|---|
| 542 | + @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 543 | + @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 544 | + public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 545 | + LOG.info("Blocking license with id: {}", licId); |
|---|
| 546 | + License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 547 | + |
|---|
| 548 | + if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) { |
|---|
| 549 | + LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name()); |
|---|
| 550 | + throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status"); |
|---|
| 551 | + } |
|---|
| 552 | + if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { |
|---|
| 553 | + throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked"); |
|---|
| 554 | + } |
|---|
| 555 | + BlockedRequest blockedReq = new BlockedRequest(); |
|---|
| 556 | + blockedReq.setCreationTimestamp(new Date()); |
|---|
| 557 | + blockedReq.setBlockedBy(userHelper.getUser(bsc, em)); |
|---|
| 558 | + blockedReq.setRequestData(lic.getRequestData()); |
|---|
| 559 | + |
|---|
| 560 | + em.persist(blockedReq); |
|---|
| 561 | + lic.setStatus(LicenseStatus.BLOCKED); |
|---|
| 562 | + lic.setModificationTimestamp(new Date()); |
|---|
| 563 | + em.merge(lic); |
|---|
| 564 | + |
|---|
| 565 | + em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK)); |
|---|
| 566 | + return Response.ok(Utils.createMap("success", true, "id", licId)).build(); |
|---|
| 567 | + } |
|---|
| 568 | + |
|---|
| 569 | + /** |
|---|
| 570 | + * unblock |
|---|
| 571 | + * <p> |
|---|
| 572 | + * Remove the block for the license request data (if present) and move |
|---|
| 573 | + * license back to CANCELLED. Adds an UNBLOCK history entry. |
|---|
| 574 | + * |
|---|
| 575 | + * @param licId License id. |
|---|
| 576 | + * @param bsc Security context. |
|---|
| 577 | + * @return 200 OK with a success payload. |
|---|
| 578 | + * @throws SeCurisServiceException never if not blocked (returns success anyway). |
|---|
| 579 | + */ |
|---|
| 580 | + @POST |
|---|
| 581 | + @Path("/{licId}/unblock") |
|---|
| 582 | + @EnsureTransaction |
|---|
| 583 | + @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 584 | + @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 585 | + public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 586 | + LOG.info("Unblocking license with id: {}", licId); |
|---|
| 587 | + License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 588 | + |
|---|
| 589 | + if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { |
|---|
| 590 | + BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash()); |
|---|
| 591 | + em.remove(blockedReq); |
|---|
| 592 | + |
|---|
| 593 | + lic.setStatus(LicenseStatus.CANCELLED); |
|---|
| 594 | + lic.setModificationTimestamp(new Date()); |
|---|
| 595 | + em.merge(lic); |
|---|
| 596 | + em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK)); |
|---|
| 597 | + } else { |
|---|
| 598 | + LOG.info("Request data for license {} is NOT blocked", licId); |
|---|
| 599 | + } |
|---|
| 600 | + |
|---|
| 601 | + return Response.ok(Utils.createMap("success", true, "id", licId)).build(); |
|---|
| 602 | + } |
|---|
| 603 | + |
|---|
| 604 | + // --------------------------------------------------------------------- |
|---|
| 605 | + // Helpers |
|---|
| 606 | + // --------------------------------------------------------------------- |
|---|
| 607 | + |
|---|
| 608 | + /** |
|---|
| 609 | + * checkIfCodeExists<p> |
|---|
| 610 | + * Check if there is an existing license with the same code. |
|---|
| 611 | + * |
|---|
| 612 | + * @param code |
|---|
| 613 | + * @param entityManager |
|---|
| 614 | + */ |
|---|
| 615 | + private boolean checkIfCodeExists(String code, EntityManager em) { |
|---|
| 616 | + TypedQuery<License> query = em.createNamedQuery("license-by-code", License.class); |
|---|
| 617 | + query.setParameter("code", code); |
|---|
| 618 | + int lics = query.getResultList().size(); |
|---|
| 619 | + return lics > 0; |
|---|
| 620 | + } |
|---|
| 621 | + |
|---|
| 622 | + /** |
|---|
| 623 | + * generateLicense<p> |
|---|
| 624 | + * Generate a signed license from request data and pack metadata/expiration. |
|---|
| 625 | + * |
|---|
| 626 | + * @param license License with requestData and packId populated. |
|---|
| 627 | + * @param em Entity manager. |
|---|
| 628 | + * @return Signed license bean. |
|---|
| 629 | + * @throws SeCurisServiceException if validation/generation fails. |
|---|
| 630 | + */ |
|---|
| 407 | 631 | private SignedLicenseBean generateLicense(License license, EntityManager em) throws SeCurisServiceException { |
|---|
| 408 | 632 | SignedLicenseBean sl = null; |
|---|
| 409 | 633 | Pack pack = em.find(Pack.class, license.getPackId()); |
|---|
| .. | .. |
|---|
| 419 | 643 | } |
|---|
| 420 | 644 | |
|---|
| 421 | 645 | /** |
|---|
| 422 | | - * We check if the given request data is valid for the current Pack and has |
|---|
| 423 | | - * a valid format |
|---|
| 424 | | - * |
|---|
| 425 | | - * @param pack |
|---|
| 426 | | - * @param requestData |
|---|
| 427 | | - * @throws SeCurisServiceException |
|---|
| 646 | + * validateRequestData<p> |
|---|
| 647 | + * Validate that requestData matches the Pack and is well-formed. |
|---|
| 648 | + * |
|---|
| 649 | + * @param pack Target pack (org/type constraints). |
|---|
| 650 | + * @param requestData Raw JSON string with the license request. |
|---|
| 651 | + * @param activationCode Activation code from the license payload. |
|---|
| 652 | + * @return Parsed {@link RequestBean}. |
|---|
| 653 | + * @throws SeCurisServiceException on format mismatch or wrong codes. |
|---|
| 428 | 654 | */ |
|---|
| 429 | 655 | private RequestBean validateRequestData(Pack pack, String requestData, String activationCode) throws SeCurisServiceException { |
|---|
| 430 | 656 | if (requestData == null) { |
|---|
| .. | .. |
|---|
| 456 | 682 | return rb; |
|---|
| 457 | 683 | } |
|---|
| 458 | 684 | |
|---|
| 459 | | - @SuppressWarnings("deprecation") |
|---|
| 460 | | - @PUT |
|---|
| 461 | | - @POST |
|---|
| 462 | | - @Path("/{licId}") |
|---|
| 463 | | - @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 464 | | - @EnsureTransaction |
|---|
| 465 | | - @Consumes(MediaType.APPLICATION_JSON) |
|---|
| 466 | | - @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 467 | | - public Response modify(License lic, @PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 468 | | - LOG.info("Modifying license with id: {}", licId); |
|---|
| 469 | | - |
|---|
| 470 | | - // EntityManager em = emProvider.get(); |
|---|
| 471 | | - |
|---|
| 472 | | - License currentLicense = getCurrentLicense(licId, bsc, em); |
|---|
| 473 | | - currentLicense.setComments(lic.getComments()); |
|---|
| 474 | | - currentLicense.setFullName(lic.getFullName()); |
|---|
| 475 | | - currentLicense.setEmail(lic.getEmail()); |
|---|
| 476 | | - if (currentLicense.getActivationCode() == null) { |
|---|
| 477 | | - currentLicense.setActivationCode(lic.getActivationCode()); |
|---|
| 478 | | - } |
|---|
| 479 | | - |
|---|
| 480 | | - if (currentLicense.getStatus() == LicenseStatus.CREATED && !ObjectUtils.equals(currentLicense.getReqDataHash(), lic.getReqDataHash())) { |
|---|
| 481 | | - if (lic.getRequestData() != null) { |
|---|
| 482 | | - SignedLicenseBean signedLicense = generateLicense(lic, em); |
|---|
| 483 | | - try { |
|---|
| 484 | | - // Next 2 lines are necessary to normalize the String that |
|---|
| 485 | | - // contains |
|---|
| 486 | | - // the request. |
|---|
| 487 | | - currentLicense.setRequestData(JsonUtils.toJSON(signedLicense, RequestBean.class)); |
|---|
| 488 | | - LOG.info("JSON generated for request: \n{}", currentLicense.getRequestData()); |
|---|
| 489 | | - if (BlockedRequest.isRequestBlocked(currentLicense.getRequestData(), em)) { |
|---|
| 490 | | - throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is blocked and cannot be used again"); |
|---|
| 491 | | - } |
|---|
| 492 | | - currentLicense.setLicenseData(JsonUtils.toJSON(signedLicense)); |
|---|
| 493 | | - } catch (SeCurisException e) { |
|---|
| 494 | | - LOG.error("Error generaing license JSON", e); |
|---|
| 495 | | - throw new SeCurisServiceException(ErrorCodes.INVALID_FORMAT, "Error generaing license JSON"); |
|---|
| 496 | | - } |
|---|
| 497 | | - } else { |
|---|
| 498 | | - // This set method could pass a null value |
|---|
| 499 | | - currentLicense.setRequestData(null); |
|---|
| 500 | | - } |
|---|
| 501 | | - } |
|---|
| 502 | | - |
|---|
| 503 | | - currentLicense.setModificationTimestamp(new Date()); |
|---|
| 504 | | - em.persist(currentLicense); |
|---|
| 505 | | - em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.MODIFY)); |
|---|
| 506 | | - |
|---|
| 507 | | - return Response.ok(currentLicense).build(); |
|---|
| 508 | | - } |
|---|
| 509 | | - |
|---|
| 510 | | - @DELETE |
|---|
| 511 | | - @Path("/{licId}") |
|---|
| 512 | | - @EnsureTransaction |
|---|
| 513 | | - @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 514 | | - @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 515 | | - public Response delete(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 516 | | - LOG.info("Deleting license with id: {}", licId); |
|---|
| 517 | | - // EntityManager em = emProvider.get(); |
|---|
| 518 | | - License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 519 | | - |
|---|
| 520 | | - if (!License.Status.isActionValid(License.Action.DELETE, lic.getStatus())) { |
|---|
| 521 | | - LOG.error("License {} can not be deleted with status {}", lic.getCode(), lic.getStatus()); |
|---|
| 522 | | - throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can not be deleted in current status: " + lic.getStatus().name()); |
|---|
| 523 | | - } |
|---|
| 524 | | - if (lic.getStatus() == LicenseStatus.BLOCKED) { |
|---|
| 525 | | - // If license is removed and it's blocked then the blocked request |
|---|
| 526 | | - // should be removed, that is, |
|---|
| 527 | | - // the license deletion will unblock the request data |
|---|
| 528 | | - BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash()); |
|---|
| 529 | | - if (blockedReq != null) { |
|---|
| 530 | | - // This if is to avoid some race condition or if the request has |
|---|
| 531 | | - // been already removed manually |
|---|
| 532 | | - em.remove(blockedReq); |
|---|
| 533 | | - } |
|---|
| 534 | | - } |
|---|
| 535 | | - |
|---|
| 536 | | - em.remove(lic); |
|---|
| 537 | | - return Response.ok(Utils.createMap("success", true, "id", licId)).build(); |
|---|
| 538 | | - } |
|---|
| 539 | | - |
|---|
| 540 | | - @POST |
|---|
| 541 | | - @Path("/{licId}/block") |
|---|
| 542 | | - @EnsureTransaction |
|---|
| 543 | | - @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 544 | | - @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 545 | | - public Response block(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 546 | | - LOG.info("Blocking license with id: {}", licId); |
|---|
| 547 | | - // EntityManager em = emProvider.get(); |
|---|
| 548 | | - License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 549 | | - |
|---|
| 550 | | - if (!License.Status.isActionValid(License.Action.BLOCK, lic.getStatus())) { |
|---|
| 551 | | - LOG.error("License can only be blocked in CANCELLED status, current: {}", lic.getStatus().name()); |
|---|
| 552 | | - throw new SeCurisServiceException(ErrorCodes.WRONG_STATUS, "License can only be blocked in CANCELLED status"); |
|---|
| 553 | | - } |
|---|
| 554 | | - if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { |
|---|
| 555 | | - throw new SeCurisServiceException(ErrorCodes.BLOCKED_REQUEST_DATA, "Given request data is already blocked"); |
|---|
| 556 | | - } |
|---|
| 557 | | - BlockedRequest blockedReq = new BlockedRequest(); |
|---|
| 558 | | - blockedReq.setCreationTimestamp(new Date()); |
|---|
| 559 | | - blockedReq.setBlockedBy(userHelper.getUser(bsc, em)); |
|---|
| 560 | | - blockedReq.setRequestData(lic.getRequestData()); |
|---|
| 561 | | - |
|---|
| 562 | | - em.persist(blockedReq); |
|---|
| 563 | | - lic.setStatus(LicenseStatus.BLOCKED); |
|---|
| 564 | | - lic.setModificationTimestamp(new Date()); |
|---|
| 565 | | - em.merge(lic); |
|---|
| 566 | | - |
|---|
| 567 | | - em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.BLOCK)); |
|---|
| 568 | | - return Response.ok(Utils.createMap("success", true, "id", licId)).build(); |
|---|
| 569 | | - } |
|---|
| 570 | | - |
|---|
| 571 | | - @POST |
|---|
| 572 | | - @Path("/{licId}/unblock") |
|---|
| 573 | | - @EnsureTransaction |
|---|
| 574 | | - @Securable(roles = Rol.ADMIN | Rol.ADVANCE) |
|---|
| 575 | | - @Produces({ MediaType.APPLICATION_JSON }) |
|---|
| 576 | | - public Response unblock(@PathParam("licId") Integer licId, @Context BasicSecurityContext bsc) throws SeCurisServiceException { |
|---|
| 577 | | - LOG.info("Unblocking license with id: {}", licId); |
|---|
| 578 | | - // EntityManager em = emProvider.get(); |
|---|
| 579 | | - License lic = getCurrentLicense(licId, bsc, em); |
|---|
| 580 | | - |
|---|
| 581 | | - if (BlockedRequest.isRequestBlocked(lic.getRequestData(), em)) { |
|---|
| 582 | | - BlockedRequest blockedReq = em.find(BlockedRequest.class, lic.getReqDataHash()); |
|---|
| 583 | | - em.remove(blockedReq); |
|---|
| 584 | | - |
|---|
| 585 | | - lic.setStatus(LicenseStatus.CANCELLED); |
|---|
| 586 | | - lic.setModificationTimestamp(new Date()); |
|---|
| 587 | | - em.merge(lic); |
|---|
| 588 | | - em.persist(licenseHelper.createLicenseHistoryAction(lic, userHelper.getUser(bsc, em), LicenseHistory.Actions.UNBLOCK)); |
|---|
| 589 | | - } else { |
|---|
| 590 | | - LOG.info("Request data for license {} is NOT blocked", licId); |
|---|
| 591 | | - } |
|---|
| 592 | | - |
|---|
| 593 | | - return Response.ok(Utils.createMap("success", true, "id", licId)).build(); |
|---|
| 594 | | - } |
|---|
| 595 | | - |
|---|
| 685 | + /** |
|---|
| 686 | + * getCurrentLicense<p> |
|---|
| 687 | + * Load a license and verify scope for non-admin users. |
|---|
| 688 | + * |
|---|
| 689 | + * @param licId License id. |
|---|
| 690 | + * @param bsc Security context. |
|---|
| 691 | + * @param em Entity manager. |
|---|
| 692 | + * @return License entity. |
|---|
| 693 | + * @throws SeCurisServiceException if id is missing, not found or unauthorized. |
|---|
| 694 | + */ |
|---|
| 596 | 695 | private License getCurrentLicense(Integer licId, BasicSecurityContext bsc, EntityManager em) throws SeCurisServiceException { |
|---|
| 597 | 696 | if (licId == null || "".equals(Integer.toString(licId))) { |
|---|
| 598 | 697 | LOG.error("License ID is mandatory"); |
|---|
| .. | .. |
|---|
| 611 | 710 | return lic; |
|---|
| 612 | 711 | } |
|---|
| 613 | 712 | |
|---|
| 713 | + // --------------------------------------------------------------------- |
|---|
| 714 | + // DTOs |
|---|
| 715 | + // --------------------------------------------------------------------- |
|---|
| 716 | + |
|---|
| 717 | + /** |
|---|
| 718 | + * DTO used to carry a cancellation reason for the cancel endpoint. |
|---|
| 719 | + */ |
|---|
| 614 | 720 | @JsonAutoDetect |
|---|
| 615 | 721 | @JsonIgnoreProperties(ignoreUnknown = true) |
|---|
| 616 | 722 | static class CancellationLicenseActionBean { |
|---|
| .. | .. |
|---|
| 618 | 724 | private String reason; |
|---|
| 619 | 725 | } |
|---|
| 620 | 726 | } |
|---|
| 727 | + |
|---|