/** * Copyright (c) 2009 - 2012 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package org.candlepin.resource; import static org.quartz.JobBuilder.newJob; import org.candlepin.auth.Verify; import org.candlepin.common.exceptions.BadRequestException; import org.candlepin.common.exceptions.NotFoundException; import org.candlepin.common.paging.Page; import org.candlepin.common.paging.PageRequest; import org.candlepin.common.paging.Paginate; import org.candlepin.common.util.SuppressSwaggerCheck; import org.candlepin.controller.Entitler; import org.candlepin.controller.PoolManager; import org.candlepin.model.Cdn; import org.candlepin.model.Consumer; import org.candlepin.model.ConsumerCurator; import org.candlepin.model.Entitlement; import org.candlepin.model.EntitlementCurator; import org.candlepin.model.EntitlementFilterBuilder; import org.candlepin.model.Pool; import org.candlepin.model.SubscriptionsCertificate; import org.candlepin.pinsetter.tasks.RegenProductEntitlementCertsJob; import org.candlepin.policy.ValidationResult; import org.candlepin.policy.js.entitlement.Enforcer; import org.candlepin.policy.js.entitlement.Enforcer.CallerType; import org.candlepin.policy.js.entitlement.EntitlementRulesTranslator; import org.candlepin.resource.util.EntitlementFinderUtil; import org.candlepin.resteasy.parameter.CandlepinParam; import org.candlepin.resteasy.parameter.KeyValueParameter; import org.candlepin.util.Util; import com.google.inject.Inject; import org.apache.commons.lang.StringUtils; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnap.commons.i18n.I18n; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import io.swagger.annotations.Authorization; /** * REST api gateway for the User object. */ @Path("/entitlements") @Api(value = "entitlements", authorizations = { @Authorization("basic") }) public class EntitlementResource { private static Logger log = LoggerFactory.getLogger(EntitlementResource.class); private final ConsumerCurator consumerCurator; private PoolManager poolManager; private final EntitlementCurator entitlementCurator; private I18n i18n; private Entitler entitler; private Enforcer enforcer; private EntitlementRulesTranslator messageTranslator; @Inject public EntitlementResource(EntitlementCurator entitlementCurator, ConsumerCurator consumerCurator, PoolManager poolManager, I18n i18n, Entitler entitler, Enforcer enforcer, EntitlementRulesTranslator messageTranslator) { this.entitlementCurator = entitlementCurator; this.consumerCurator = consumerCurator; this.i18n = i18n; this.poolManager = poolManager; this.entitler = entitler; this.enforcer = enforcer; this.messageTranslator = messageTranslator; } private void verifyExistence(Object o, String id) { if (o == null) { throw new RuntimeException( i18n.tr("Object with ID ''{0}'' could not found.", id)); } } @ApiOperation(notes = "Checks Consumer for Product Entitlement", value = "hasEntitlement") @ApiResponses({ @ApiResponse(code = 404, message = "") }) @GET @Produces(MediaType.APPLICATION_JSON) @Path("consumer/{consumer_uuid}/product/{product_id}") public Entitlement hasEntitlement(@PathParam("consumer_uuid") String consumerUuid, @PathParam("product_id") String productId) { Consumer consumer = consumerCurator.findByUuid(consumerUuid); verifyExistence(consumer, consumerUuid); for (Entitlement e : consumer.getEntitlements()) { if (e.getPool().getProductId().equals(productId)) { return e; } } throw new NotFoundException(i18n.tr( "Unit ''{0}'' has no subscription for product ''{1}''.", consumerUuid, productId)); } @ApiOperation(notes = "Retrieves list of Entitlements", value = "listAllForConsumer") @ApiResponses({ @ApiResponse(code = 400, message = "") }) @GET @Produces(MediaType.APPLICATION_JSON) @Paginate public List<Entitlement> listAllForConsumer( @QueryParam("consumer") String consumerUuid, @QueryParam("matches") String matches, @QueryParam("attribute") @CandlepinParam(type = KeyValueParameter.class) List<KeyValueParameter> attrFilters, @Context PageRequest pageRequest) { EntitlementFilterBuilder filters = EntitlementFinderUtil.createFilter(matches, attrFilters); Page<List<Entitlement>> p; if (consumerUuid != null) { Consumer consumer = consumerCurator.findByUuid(consumerUuid); if (consumer == null) { throw new BadRequestException( i18n.tr("Unit with ID ''{0}'' could not be found.", consumerUuid)); } p = entitlementCurator.listByConsumer(consumer, null, filters, pageRequest); } else { p = entitlementCurator.listAll(filters, pageRequest); } // Store the page for the LinkHeaderResponseFilter ResteasyProviderFactory.pushContext(Page.class, p); return p.getPageData(); } @ApiOperation(notes = "Retrieves a single Entitlement", value = "getEntitlement") @ApiResponses({ @ApiResponse(code = 404, message = "") }) @GET @Produces(MediaType.APPLICATION_JSON) @Path("{entitlement_id}") public Entitlement getEntitlement( @PathParam("entitlement_id") @Verify(Entitlement.class) String entitlementId) { Entitlement entitlement = entitlementCurator.find(entitlementId); if (entitlement == null) { throw new NotFoundException( i18n.tr("Entitlement with ID ''{0}'' could not be found.", entitlementId) ); } // Impl note: // If the entitlement is dirty, this performs an in-place update of the entitlement, not a // generation of a new entitlement object, as the name would imply. poolManager.regenerateDirtyEntitlements(Arrays.asList(entitlement)); return entitlement; } @ApiOperation(notes = "Updates an Entitlement. This only works for the quantity.", value = "updateEntitlement") @ApiResponses({ @ApiResponse(code = 404, message = "") }) @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @Path("{entitlement_id}") public void updateEntitlement( @PathParam("entitlement_id") @Verify(Entitlement.class) String id, @ApiParam(name = "update", required = true) Entitlement update) { // Check that quantity param was set and is not 0: if (update.getQuantity() <= 0) { throw new BadRequestException( i18n.tr("Quantity value must be greater than 0.")); } // Verify entitlement exists: Entitlement entitlement = entitlementCurator.find(id); if (entitlement != null) { // make sure that this will be a change if (!entitlement.getQuantity().equals(update.getQuantity())) { Consumer consumer = entitlement.getConsumer(); entitler.adjustEntitlementQuantity(consumer, entitlement, update.getQuantity()); } } else { throw new NotFoundException( i18n.tr("Entitlement with ID ''{0}'' could not be found.", id)); } } @ApiOperation(notes = "Retrieves a Subscription Certificate. We can't return CdnInfo " + "at this time, but when the time comes this is the implementation we want to start" + " from. It will require changes to thumbslug. will also" + " @Produces(MediaType.APPLICATION_JSON)", value = "getUpstreamCert") @ApiResponses({ @ApiResponse(code = 404, message = "") }) @GET @Produces(MediaType.TEXT_PLAIN) @Path("{dbid}/upstream_cert") public String getUpstreamCert( @PathParam("dbid") String entitlementId) { Entitlement ent = entitlementCurator.find(entitlementId); if (ent == null) { throw new NotFoundException(i18n.tr( "Entitlement with ID ''{0}'' could not be found.", entitlementId)); } Pool entPool = ent.getPool(); if (!StringUtils.isBlank(entPool.getSourceStackId())) { /* * A derived pool originating from a stacked parent pool will have no subscription * ID as the pool is technically from many subscriptions. (i.e. all the * entitlements in the stack) In this case we must look up an active entitlement * in the hosts stack, and use this as our upstream certificate. */ log.debug("Entitlement is from a stack derived pool, searching for oldest " + "active entitlements in source stack."); ent = entitlementCurator.findUpstreamEntitlementForStack( entPool.getSourceStack().getSourceConsumer(), entPool.getSourceStackId()); } if (ent == null || ent.getPool() == null || ent.getPool().getCertificate() == null) { throw new NotFoundException( i18n.tr("Unable to find upstream certificate for entitlement: {0}", entitlementId) ); } SubscriptionsCertificate cert = ent.getPool().getCertificate(); return cert.getCert() + cert.getKey(); } @ApiOperation(notes = "Deletes an Entitlement", value = "unbind") @ApiResponses({ @ApiResponse(code = 403, message = ""), @ApiResponse(code = 404, message = "") }) @DELETE @Produces(MediaType.WILDCARD) @Path("/{dbid}") public void unbind(@PathParam("dbid") String dbid) { Entitlement toDelete = entitlementCurator.find(dbid); if (toDelete != null) { poolManager.revokeEntitlement(toDelete); return; } throw new NotFoundException( i18n.tr("Entitlement with ID ''{0}'' could not be found.", dbid)); } @ApiOperation(notes = "Regenerates the Entitlement Certificates for a Product", value = "regenerateEntitlementCertificatesForProduct") @ApiResponses({ @ApiResponse(code = 202, message = "") }) @PUT @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.WILDCARD) @Path("product/{product_id}") public JobDetail regenerateEntitlementCertificatesForProduct( @PathParam("product_id") String productId, @QueryParam("lazy_regen") @DefaultValue("true") boolean lazyRegen) { JobDataMap map = new JobDataMap(); map.put(RegenProductEntitlementCertsJob.PROD_ID, productId); map.put(RegenProductEntitlementCertsJob.LAZY_REGEN, lazyRegen); JobDetail detail = newJob(RegenProductEntitlementCertsJob.class) .withIdentity("regen_entitlement_cert_of_prod" + Util.generateUUID()) .usingJobData(map) .build(); return detail; } @ApiOperation(notes = "Migrate entitlements from one distributor consumer to another." + " Can specify full or partial quantity. No specified quantity " + "will lead to full migration of the entitlement.", value = "migrateEntitlement") @ApiResponses({ @ApiResponse(code = 404, message = "") }) @PUT @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.WILDCARD) @Path("{entitlement_id}/migrate") public Response migrateEntitlement( @PathParam("entitlement_id") @Verify(Entitlement.class) String id, @QueryParam("to_consumer") @Verify(Consumer.class) String uuid, @QueryParam("quantity") Integer quantity) { // confirm entitlement Entitlement entitlement = entitlementCurator.find(id); List<Entitlement> entitlements = new ArrayList<Entitlement>(); if (entitlement != null) { if (quantity == null) { quantity = entitlement.getQuantity(); } if (quantity > 0 && quantity <= entitlement.getQuantity()) { Consumer sourceConsumer = entitlement.getConsumer(); Consumer destinationConsumer = consumerCurator.verifyAndLookupConsumer(uuid); if (!sourceConsumer.isManifestDistributor()) { throw new BadRequestException(i18n.tr( "Entitlement migration is not permissible for units of type ''{0}''", sourceConsumer.getType().getLabel())); } if (!destinationConsumer.isManifestDistributor()) { throw new BadRequestException(i18n.tr( "Entitlement migration is not permissible for units of type ''{0}''", destinationConsumer.getType().getLabel())); } if (!sourceConsumer.getOwner().getKey().equals(destinationConsumer.getOwner().getKey())) { throw new BadRequestException(i18n.tr( "Source and destination units must belong to the same organization")); } // test to ensure destination can use the pool ValidationResult result = enforcer.preEntitlement(destinationConsumer, entitlement.getPool(), 0, CallerType.BIND); if (!result.isSuccessful()) { throw new BadRequestException(i18n.tr( "The entitlement cannot be utilized by the destination unit: ") + messageTranslator.poolErrorToMessage(entitlement.getPool(), result.getErrors().get(0))); } if (quantity.intValue() == entitlement.getQuantity()) { unbind(id); } else { entitler.adjustEntitlementQuantity(sourceConsumer, entitlement, entitlement.getQuantity() - quantity); } Pool pool = entitlement.getPool(); entitlements.addAll(entitler.bindByPoolQuantity(destinationConsumer, pool.getId(), quantity)); // Trigger events: entitler.sendEvents(entitlements); } else { throw new BadRequestException(i18n.tr( "The quantity specified must be greater than zero " + "and less than or equal to the total for this entitlement")); } } else { throw new NotFoundException( i18n.tr("Entitlement with ID ''{0}'' could not be found.", id)); } return Response.status(Response.Status.OK) .type(MediaType.APPLICATION_JSON).entity(entitlements).build(); } /** * * CdnInfo represents a container for subscription entitlement and cdn */ @SuppressSwaggerCheck public static class CdnInfo implements Serializable { private static final long serialVersionUID = 523637879312970984L; private Cdn cdn; private String subCert; public CdnInfo() { } public CdnInfo(Cdn cdn, String subEntitlement) { this.cdn = cdn; this.subCert = subEntitlement; } public Cdn getCdn() { return this.cdn; } public void setCdn(Cdn cdn) { this.cdn = cdn; } public String getSubCert() { return this.subCert; } public void setSubCert(String subCert) { this.subCert = subCert; } } }