Joaquín Reñé
2025-10-07 146a0fb8b0e90f9196e569152f649baf60d6cc8f
securis/src/main/java/net/curisit/securis/services/PackResource.java
....@@ -1,3 +1,6 @@
1
+/*
2
+ * Copyright @ 2013 CurisTEC, S.A.S. All Rights Reserved.
3
+ */
14 package net.curisit.securis.services;
25
36 import java.security.Principal;
....@@ -53,31 +56,35 @@
5356 import net.curisit.securis.utils.TokenHelper;
5457
5558 /**
56
- * Pack resource, this service will provide methods to create, modify and delete
57
- * packs
58
- *
59
- * @author roberto <roberto.sanchez@curisit.net>
59
+ * PackResource
60
+ * <p>
61
+ * Manages Packs (group of licenses bound to an organization, application/type,
62
+ * and configuration/metadata). Provides list/filter, get, create, modify,
63
+ * state transitions (activate/hold/cancel) and deletion.
64
+ *
65
+ * @author JRA
66
+ * Last reviewed by JRA on Oct 5, 2025.
6067 */
6168 @Path("/pack")
6269 public class PackResource {
6370
6471 private static final Logger LOG = LogManager.getLogger(PackResource.class);
6572
66
- @Inject
67
- TokenHelper tokenHelper;
73
+ @Inject TokenHelper tokenHelper;
74
+ @Inject MetadataHelper metadataHelper;
75
+ @Inject private LicenseHelper licenseHelper;
6876
69
- @Inject
70
- MetadataHelper metadataHelper;
71
-
72
- @Context
73
- EntityManager em;
74
-
75
- @Inject
76
- private LicenseHelper licenseHelper;
77
+ @Context EntityManager em;
7778
7879 /**
79
- *
80
- * @return the server version in format majorVersion.minorVersion
80
+ * index
81
+ * <p>
82
+ * List packs with optional filters (organizationId, applicationId, licenseTypeId).
83
+ * For non-admins, results are scoped by both apps and orgs from {@link BasicSecurityContext}.
84
+ *
85
+ * @param uriInfo supplies query parameters.
86
+ * @param bsc security scope/roles.
87
+ * @return 200 OK with the list (possibly empty).
8188 */
8289 @GET
8390 @Path("/")
....@@ -86,70 +93,25 @@
8693 public Response index(@Context UriInfo uriInfo, @Context BasicSecurityContext bsc) {
8794 LOG.info("Getting packs list ");
8895 MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
89
-
90
- // EntityManager em = emProvider.get();
9196 em.clear();
9297
9398 TypedQuery<Pack> q = createQuery(queryParams, bsc);
9499 if (q == null) {
95100 return Response.ok().build();
96101 }
97
-
98102 List<Pack> list = q.getResultList();
99
-
100103 return Response.ok(list).build();
101104 }
102105
103
- private String generateWhereFromParams(boolean addWhere, MultivaluedMap<String, String> queryParams) {
104
- List<String> conditions = new ArrayList<>();
105
- if (queryParams.containsKey("organizationId")) {
106
- conditions.add(String.format("pa.organization.id = %s", queryParams.getFirst("organizationId")));
107
- }
108
- if (queryParams.containsKey("applicationId")) {
109
- conditions.add(String.format("pa.licenseType.application.id = %s", queryParams.getFirst("applicationId")));
110
- }
111
- if (queryParams.containsKey("licenseTypeId")) {
112
- conditions.add(String.format("pa.licenseType.id = %s", queryParams.getFirst("licenseTypeId")));
113
- }
114
- String connector = addWhere ? " where " : " and ";
115
- return (conditions.isEmpty() ? "" : connector) + String.join(" and ", conditions);
116
- }
117
-
118
- private TypedQuery<Pack> createQuery(MultivaluedMap<String, String> queryParams, BasicSecurityContext bsc) {
119
- TypedQuery<Pack> q;
120
- String hql = "SELECT pa FROM Pack pa";
121
- if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
122
- hql += generateWhereFromParams(true, queryParams);
123
- q = em.createQuery(hql, Pack.class);
124
- } else {
125
- if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
126
- return null;
127
- }
128
- if (bsc.getOrganizationsIds() == null || bsc.getOrganizationsIds().isEmpty()) {
129
- hql += " where pa.licenseType.application.id in :list_ids_app ";
130
- } else {
131
- hql += " where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app ";
132
- }
133
- hql += generateWhereFromParams(false, queryParams);
134
- q = em.createQuery(hql, Pack.class);
135
- if (hql.contains("list_ids_org")) {
136
- q.setParameter("list_ids_org", bsc.getOrganizationsIds());
137
- }
138
- q.setParameter("list_ids_app", bsc.getApplicationsIds());
139
- LOG.info("Getting packs from orgs: {} and apps: {}", bsc.getOrganizationsIds(), bsc.getApplicationsIds());
140
- }
141
-
142
- return q;
143
- }
144
-
145
- private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
146
- LOG.error("Pack with id {} not accesible by user {}", pack, user);
147
- return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
148
- }
149
-
150106 /**
151
- *
152
- * @return the server version in format majorVersion.minorVersion
107
+ * get
108
+ * <p>
109
+ * Fetch a pack by id. If the caller is an ADVANCE user, validates
110
+ * the organization scope.
111
+ *
112
+ * @param packId pack id.
113
+ * @param bsc security context.
114
+ * @return 200 OK with entity, or 404/401 accordingly.
153115 */
154116 @GET
155117 @Path("/{packId}")
....@@ -162,7 +124,6 @@
162124 return Response.status(Status.NOT_FOUND).build();
163125 }
164126
165
- // EntityManager em = emProvider.get();
166127 em.clear();
167128 Pack pack = em.find(Pack.class, packId);
168129 if (pack == null) {
....@@ -175,6 +136,18 @@
175136 return Response.ok(pack).build();
176137 }
177138
139
+ /**
140
+ * create
141
+ * <p>
142
+ * Create a new pack. Validates code uniqueness, sets organization and
143
+ * license type references, stamps creator and timestamps, and persists
144
+ * metadata entries.
145
+ *
146
+ * @param pack payload.
147
+ * @param bsc security context (for createdBy).
148
+ * @return 200 OK with created pack or 404 when references not found.
149
+ * @throws SeCurisServiceException on duplicated code.
150
+ */
178151 @POST
179152 @Path("/")
180153 @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
....@@ -184,7 +157,6 @@
184157 @EnsureTransaction
185158 public Response create(Pack pack, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
186159 LOG.info("Creating new pack");
187
- // EntityManager em = emProvider.get();
188160
189161 if (checkIfCodeExists(pack.getCode(), em)) {
190162 throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is already used in an existing pack");
....@@ -221,67 +193,16 @@
221193 }
222194
223195 /**
224
- * Check if there is some pack with the same code
225
- *
226
- * @param code
227
- * Pack code
228
- * @param em
229
- * DB session object
230
- * @return <code>true</code> if code is already used, <code>false</code>
231
- * otherwise
196
+ * modify
197
+ * <p>
198
+ * Update a pack basic fields and reconcile metadata (remove/merge/persist).
199
+ * If metadata keys changed, marks dependent licenses metadata as obsolete via
200
+ * {@link MetadataHelper#markObsoleteMetadata}.
201
+ *
202
+ * @param pack payload values.
203
+ * @param packId target id.
204
+ * @return 200 OK with updated pack or 404 on ref errors.
232205 */
233
- private boolean checkIfCodeExists(String code, EntityManager em) {
234
- TypedQuery<Pack> query = em.createNamedQuery("pack-by-code", Pack.class);
235
- query.setParameter("code", code);
236
- int packs = query.getResultList().size();
237
- return packs > 0;
238
- }
239
-
240
- /**
241
- *
242
- * @return The next available code suffix in pack for license code
243
- * @throws SeCurisServiceException
244
- */
245
- @GET
246
- @Path("/{packId}/next_license_code")
247
- @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
248
- @Produces({ MediaType.TEXT_PLAIN })
249
- public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
250
- // EntityManager em = emProvider.get();
251
-
252
- if (packId == null) {
253
- throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
254
- }
255
- Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
256
- Pack pack = em.find(Pack.class, packId);
257
- ;
258
-
259
- String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
260
- return Response.ok(licCode).build();
261
- }
262
-
263
- private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
264
- LicenseType lt = null;
265
- if (licTypeId != null) {
266
- lt = em.find(LicenseType.class, pack.getLicTypeId());
267
- if (lt == null) {
268
- LOG.error("Pack license type with id {} not found in DB", licTypeId);
269
- throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
270
- }
271
- }
272
- pack.setLicenseType(lt);
273
- }
274
-
275
- private Set<String> getMdKeys(Set<PackMetadata> mds) {
276
- Set<String> ids = new HashSet<String>();
277
- if (mds != null) {
278
- for (PackMetadata md : mds) {
279
- ids.add(md.getKey());
280
- }
281
- }
282
- return ids;
283
- }
284
-
285206 @PUT
286207 @POST
287208 @Path("/{packId}")
....@@ -292,7 +213,6 @@
292213 @Produces({ MediaType.APPLICATION_JSON })
293214 public Response modify(Pack pack, @PathParam("packId") Integer packId) {
294215 LOG.info("Modifying pack with id: {}", packId);
295
- // EntityManager em = emProvider.get();
296216 Pack currentPack = em.find(Pack.class, packId);
297217
298218 try {
....@@ -348,6 +268,15 @@
348268 return Response.ok(currentPack).build();
349269 }
350270
271
+ /**
272
+ * activate
273
+ * <p>
274
+ * Move a pack to ACTIVE (only from allowed states).
275
+ *
276
+ * @param packId target pack id.
277
+ * @return 200 OK with updated pack or error when invalid transition.
278
+ * @throws SeCurisServiceException when invalid state transition.
279
+ */
351280 @POST
352281 @Path("/{packId}/activate")
353282 @EnsureTransaction
....@@ -357,7 +286,6 @@
357286 @Produces({ MediaType.APPLICATION_JSON })
358287 public Response activate(@PathParam("packId") Integer packId) throws SeCurisServiceException {
359288 LOG.info("Activating pack with id: {}", packId);
360
- // EntityManager em = emProvider.get();
361289
362290 Pack currentPack = em.find(Pack.class, packId);
363291
....@@ -372,8 +300,17 @@
372300 return Response.ok(currentPack).build();
373301 }
374302
303
+ /**
304
+ * onhold
305
+ * <p>
306
+ * Put a pack ON_HOLD from allowed states.
307
+ *
308
+ * @param packId id.
309
+ * @return 200 OK with updated pack or error on invalid state.
310
+ * @throws SeCurisServiceException on invalid state.
311
+ */
375312 @POST
376
- @Path("/{packId}/putonhold")
313
+ @Path("/{packId}/putonhold}")
377314 @EnsureTransaction
378315 @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
379316 @RolesAllowed(BasicSecurityContext.ROL_ADMIN)
....@@ -381,7 +318,6 @@
381318 @Produces({ MediaType.APPLICATION_JSON })
382319 public Response onhold(@PathParam("packId") Integer packId) throws SeCurisServiceException {
383320 LOG.info("Putting On hold pack with id: {}", packId);
384
- // EntityManager em = emProvider.get();
385321
386322 Pack currentPack = em.find(Pack.class, packId);
387323
....@@ -396,6 +332,18 @@
396332 return Response.ok(currentPack).build();
397333 }
398334
335
+ /**
336
+ * cancel
337
+ * <p>
338
+ * Cancel a pack. Cascades cancel to ACTIVE/PRE_ACTIVE licenses in the pack
339
+ * via {@link LicenseHelper#cancelLicense}.
340
+ *
341
+ * @param packId id.
342
+ * @param reason cancellation reason.
343
+ * @param bsc actor for history entries.
344
+ * @return 200 OK with updated pack.
345
+ * @throws SeCurisServiceException on invalid state.
346
+ */
399347 @POST
400348 @Path("/{packId}/cancel")
401349 @EnsureTransaction
....@@ -405,7 +353,6 @@
405353 @Produces({ MediaType.APPLICATION_JSON })
406354 public Response cancel(@PathParam("packId") Integer packId, @FormParam("reason") String reason, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
407355 LOG.info("Cancelling pack with id: {}", packId);
408
- // EntityManager em = emProvider.get();
409356
410357 Pack currentPack = em.find(Pack.class, packId);
411358
....@@ -426,18 +373,41 @@
426373 return Response.ok(currentPack).build();
427374 }
428375
429
- private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
430
- Organization org = null;
431
- if (orgId != null) {
432
- org = em.find(Organization.class, orgId);
433
- if (org == null) {
434
- LOG.error("Organization pack with id {} not found in DB", orgId);
435
- throw new SeCurisException("Pack organization not found with ID: " + orgId);
436
- }
376
+ /**
377
+ * getCodeSuffix
378
+ * <p>
379
+ * Compute the next available license code for a pack, by asking the helper
380
+ * for the next numeric suffix and composing with {@link LicUtils}.
381
+ *
382
+ * @param packId id.
383
+ * @param bsc (unused) security context.
384
+ * @return 200 OK with the full code text.
385
+ * @throws SeCurisServiceException if packId missing.
386
+ */
387
+ @GET
388
+ @Path("/{packId}/next_license_code")
389
+ @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
390
+ @Produces({ MediaType.TEXT_PLAIN })
391
+ public Response getCodeSuffix(@PathParam("packId") Integer packId, @Context BasicSecurityContext bsc) throws SeCurisServiceException {
392
+ if (packId == null) {
393
+ throw new SeCurisServiceException(ErrorCodes.INVALID_DATA, "The pack code is mandatory");
437394 }
438
- currentPack.setOrganization(org);
395
+ Integer codeSuffix = licenseHelper.getNextCodeSuffix(packId, em);
396
+ Pack pack = em.find(Pack.class, packId);
397
+ String licCode = LicUtils.getLicenseCode(pack.getCode(), codeSuffix);
398
+ return Response.ok(licCode).build();
439399 }
440400
401
+ /**
402
+ * delete
403
+ * <p>
404
+ * Delete a pack after ensuring there are no ACTIVE/PRE_ACTIVE licenses.
405
+ * Removes remaining licenses then the pack itself.
406
+ *
407
+ * @param packId String id.
408
+ * @return 200 OK with success map, 404 if missing, or 409 if active license exists.
409
+ * @throws SeCurisServiceException on constraint errors.
410
+ */
441411 @DELETE
442412 @Path("/{packId}")
443413 @Securable(roles = Rol.ADMIN | Rol.ADVANCE)
....@@ -446,13 +416,11 @@
446416 @Produces({ MediaType.APPLICATION_JSON })
447417 public Response delete(@PathParam("packId") String packId) throws SeCurisServiceException {
448418 LOG.info("Deleting pack with id: {}", packId);
449
- // EntityManager em = emProvider.get();
450419 Pack pack = em.find(Pack.class, Integer.parseInt(packId));
451420 if (pack == null) {
452421 LOG.error("Pack with id {} can not be deleted, It was not found in DB", packId);
453422 return Response.status(Status.NOT_FOUND).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Pack was not found, ID: " + packId).build();
454423 }
455
- // Pack metadata is removed in cascade automatically.
456424
457425 Set<License> licenses = pack.getLicenses();
458426 for (License license : licenses) {
....@@ -466,4 +434,149 @@
466434 return Response.ok(Utils.createMap("success", true, "id", packId)).build();
467435 }
468436
437
+ // ---------------------------------------------------------------------
438
+ // Helpers
439
+ // ---------------------------------------------------------------------
440
+
441
+ /**
442
+ * generateWhereFromParams<p>
443
+ * Generate where clause to include to a query
444
+ *
445
+ * @param addWhere
446
+ * @param queryParams
447
+ * @return whereClause
448
+ */
449
+ private String generateWhereFromParams(boolean addWhere, MultivaluedMap<String, String> queryParams) {
450
+ List<String> conditions = new ArrayList<>();
451
+ if (queryParams.containsKey("organizationId")) {
452
+ conditions.add(String.format("pa.organization.id = %s", queryParams.getFirst("organizationId")));
453
+ }
454
+ if (queryParams.containsKey("applicationId")) {
455
+ conditions.add(String.format("pa.licenseType.application.id = %s", queryParams.getFirst("applicationId")));
456
+ }
457
+ if (queryParams.containsKey("licenseTypeId")) {
458
+ conditions.add(String.format("pa.licenseType.id = %s", queryParams.getFirst("licenseTypeId")));
459
+ }
460
+ String connector = addWhere ? " where " : " and ";
461
+ return (conditions.isEmpty() ? "" : connector) + String.join(" and ", conditions);
462
+ }
463
+
464
+ /**
465
+ * createQuery<p>
466
+ * Build a typed query considering role-based scopes and filters.
467
+ *
468
+ * @param queryParams
469
+ * @param basicSecurityContext
470
+ */
471
+ private TypedQuery<Pack> createQuery(MultivaluedMap<String, String> queryParams, BasicSecurityContext bsc) {
472
+ TypedQuery<Pack> q;
473
+ String hql = "SELECT pa FROM Pack pa";
474
+ if (bsc.isUserInRole(BasicSecurityContext.ROL_ADMIN)) {
475
+ hql += generateWhereFromParams(true, queryParams);
476
+ q = em.createQuery(hql, Pack.class);
477
+ } else {
478
+ if (bsc.getApplicationsIds() == null || bsc.getApplicationsIds().isEmpty()) {
479
+ return null;
480
+ }
481
+ if (bsc.getOrganizationsIds() == null || bsc.getOrganizationsIds().isEmpty()) {
482
+ hql += " where pa.licenseType.application.id in :list_ids_app ";
483
+ } else {
484
+ hql += " where pa.organization.id in :list_ids_org and pa.licenseType.application.id in :list_ids_app ";
485
+ }
486
+ hql += generateWhereFromParams(false, queryParams);
487
+ q = em.createQuery(hql, Pack.class);
488
+ if (hql.contains("list_ids_org")) {
489
+ q.setParameter("list_ids_org", bsc.getOrganizationsIds());
490
+ }
491
+ q.setParameter("list_ids_app", bsc.getApplicationsIds());
492
+ LOG.info("Getting packs from orgs: {} and apps: {}", bsc.getOrganizationsIds(), bsc.getApplicationsIds());
493
+ }
494
+ return q;
495
+ }
496
+
497
+ /**
498
+ * generateErrorUnathorizedAccess<p>
499
+ * Convenience 401 generator with log.
500
+ *
501
+ * @param pack
502
+ * @param user
503
+ */
504
+ private Response generateErrorUnathorizedAccess(Pack pack, Principal user) {
505
+ LOG.error("Pack with id {} not accesible by user {}", pack, user);
506
+ return Response.status(Status.UNAUTHORIZED).header(DefaultExceptionHandler.ERROR_MESSAGE_HEADER, "Unathorized access to pack").build();
507
+ }
508
+
509
+ /**
510
+ * setPackLicenseType<p>
511
+ * Set the pack type
512
+ *
513
+ * @param pack
514
+ * @param licTypeId
515
+ * @param em
516
+ * @throws SeCurisException
517
+ */
518
+ private void setPackLicenseType(Pack pack, Integer licTypeId, EntityManager em) throws SeCurisException {
519
+ LicenseType lt = null;
520
+ if (licTypeId != null) {
521
+ lt = em.find(LicenseType.class, pack.getLicTypeId());
522
+ if (lt == null) {
523
+ LOG.error("Pack license type with id {} not found in DB", licTypeId);
524
+ throw new SeCurisException("Pack license type not found with ID: " + licTypeId);
525
+ }
526
+ }
527
+ pack.setLicenseType(lt);
528
+ }
529
+
530
+ /**
531
+ * getMdKeys<p>
532
+ * Get the MD keys
533
+ *
534
+ * @param mds
535
+ * @return mdKeys
536
+ */
537
+ private Set<String> getMdKeys(Set<PackMetadata> mds) {
538
+ Set<String> ids = new HashSet<String>();
539
+ if (mds != null) {
540
+ for (PackMetadata md : mds) {
541
+ ids.add(md.getKey());
542
+ }
543
+ }
544
+ return ids;
545
+ }
546
+
547
+ /**
548
+ * checkIfCodeExists<p>
549
+ * Check if the code already exist
550
+ *
551
+ * @param code
552
+ * @param em
553
+ * @return codeExist
554
+ */
555
+ private boolean checkIfCodeExists(String code, EntityManager em) {
556
+ TypedQuery<Pack> query = em.createNamedQuery("pack-by-code", Pack.class);
557
+ query.setParameter("code", code);
558
+ int packs = query.getResultList().size();
559
+ return packs > 0;
560
+ }
561
+
562
+ /**
563
+ * setPackOrganization<p>
564
+ * Set the organization of the pack
565
+ *
566
+ * @param currentPack
567
+ * @param orgId
568
+ * @param em
569
+ * @throws SeCurisException
570
+ */
571
+ private void setPackOrganization(Pack currentPack, Integer orgId, EntityManager em) throws SeCurisException {
572
+ Organization org = null;
573
+ if (orgId != null) {
574
+ org = em.find(Organization.class, orgId);
575
+ if (org == null) {
576
+ LOG.error("Organization pack with id {} not found in DB", orgId);
577
+ throw new SeCurisException("Pack organization not found with ID: " + orgId);
578
+ }
579
+ }
580
+ currentPack.setOrganization(org);
581
+ }
469582 }