All Downloads are FREE. Search and download functionalities are using the official Maven repository.

alpine.server.resources.AlpineResource Maven / Gradle / Ivy

/*
 * This file is part of Alpine.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 * Copyright (c) Steve Springett. All Rights Reserved.
 */
package alpine.server.resources;

import alpine.common.logging.Logger;
import alpine.common.validation.ValidationException;
import alpine.common.validation.ValidationTask;
import alpine.model.ApiKey;
import alpine.model.LdapUser;
import alpine.model.ManagedUser;
import alpine.model.OidcUser;
import alpine.model.UserPrincipal;
import alpine.persistence.AlpineQueryManager;
import alpine.resources.AlpineRequest;
import io.jsonwebtoken.lang.Collections;
import org.apache.commons.lang3.StringUtils;
import org.glassfish.jersey.server.validation.ValidationError;
import org.owasp.security.logging.SecurityMarkers;
import org.slf4j.Marker;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
 * A value-added resource that all Alpine resources should extend from. This resource provides
 * access to pagination, ordering, filtering, convenience methods for obtaining specific HTTP
 * request information, along with the ability to perform input validation and automatically
 * fail requests (with HTTP status 400) if validation failure occurs.
 *
 * @author Steve Springett
 * @since 1.0.0
 */
public abstract class AlpineResource {

    private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory();

    protected static final String TOTAL_COUNT_HEADER = "X-Total-Count";

    @Context
    private ContainerRequestContext requestContext;

    @Context
    private HttpServletRequest request;

    @Context
    private UriInfo uriInfo;

    private AlpineRequest alpineRequest;


    /**
     * Returns the ContainerRequestContext. This is automatically injected
     * into every instance of an AlpineResource.
     * @return the ContainerRequestContext
     * @since 1.0.0
     */
    protected ContainerRequestContext getRequestContext() {
        return requestContext;
    }

    /**
     * Returns the UriInfo. This is automatically injected into every
     * instance of an AlpineResource.
     * @return the UriInfo
     * @since 1.0.0
     */
    protected UriInfo getUriInfo() {
        return uriInfo;
    }

    /**
     * Returns the AlpineRequest object containing pagination, order,
     * filters, and other Alpine-specified aspects of the request.
     * @return a AlpineRequest object
     * @since 1.0.0
     */
    protected AlpineRequest getAlpineRequest() {
        final Object alpineRequest = requestContext.getProperty("AlpineRequest");
        if (alpineRequest != null) {
            return (AlpineRequest) alpineRequest;
        } else {
            return null;
        }
    }

    /**
     * Convenience method that returns the remote IP address that made
     * the request.
     * @return the remote IP address as a String
     * @since 1.0.0
     */
    protected String getRemoteAddress() {
        return request.getRemoteAddr();
    }

    /**
     * Convenience method that returns the remote hostname that made
     * the request.
     * @return the remote hostname as a String
     * @since 1.0.0
     */
    protected String getRemoteHost() {
        return request.getRemoteHost();
    }

    /**
     * Convenience method that returns the User-Agent string for the
     * application that made the request.
     * @return the User-Agent as a String
     * @since 1.0.0
     */
    protected String getUserAgent() {
        return requestContext.getHeaderString("User-Agent");
    }

    /**
     * Returns a Validator instance. Internally, this uses
     * Validation.buildDefaultValidatorFactory().getValidator() so only call
     * this method sparingly and keep a reference to the Validator if possible.
     * @return a Validator object
     * @since 1.0.0
     */
    protected Validator getValidator() {
        return VALIDATOR_FACTORY.getValidator();
    }

    /**
     * Accepts the result from one of the many validation methods available and
     * returns a List of ValidationErrors. If the size of the List is 0, no errors
     * were encounter during validation.
     *
     * Usage:
     * 
     *     Validator validator = getValidator();
     *     List<ValidationError> errors = contOnValidationError(
     *         validator.validateProperty(myObject, "uuid"),
     *         validator.validateProperty(myObject, "name")
     *      );
     *      // If validation fails, this line will be reached.
     * 
* * @param violationsArray a Set of one or more ConstraintViolations * @return a List of zero or more ValidationErrors * @since 1.0.0 */ @SafeVarargs protected final List contOnValidationError(final Set>... violationsArray) { final List errors = new ArrayList<>(); for (final Set> violations : violationsArray) { for (final ConstraintViolation violation : violations) { if (violation.getPropertyPath().iterator().next().getName() != null) { final String path = violation.getPropertyPath() != null ? violation.getPropertyPath().toString() : null; final String message = violation.getMessage() != null ? StringUtils.removeStart(violation.getMessage(), path + ".") : null; final String messageTemplate = violation.getMessageTemplate(); final String invalidValue = violation.getInvalidValue() != null ? violation.getInvalidValue().toString() : null; final ValidationError error = new ValidationError(message, messageTemplate, path, invalidValue); errors.add(error); } } } return errors; } /** * Wrapper around {@link #contOnValidationError(Set[])} but instead of returning * a list of errors, this method will halt processing of the request by throwing * a BadRequestException, setting the HTTP status to 400 (BAD REQUEST) and providing * a full list of validation errors in the body of the response. * * Usage: *
     *     Validator validator = getValidator();
     *     failOnValidationError(
     *         validator.validateProperty(myObject, "uuid"),
     *         validator.validateProperty(myObject, "name")
     *      );
     *      // If validation fails, this line will not be reached.
     * 
* * @param violationsArray a Set of one or more ConstraintViolations * @since 1.0.0 */ @SafeVarargs protected final void failOnValidationError(final Set>... violationsArray) { final List errors = contOnValidationError(violationsArray); if (! Collections.isEmpty(errors)) { throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build()); } } /** * Given one or mote ValidationTasks, this method will return a List of * ValidationErrors. If the size of the List is 0, no errors were encountered * during validation. * * Usage: *
     *     List<ValidationException> errors = contOnValidationError(
     *         new ValidationTask(pattern, input, errorMessage),
     *         new ValidationTask(pattern, input, errorMessage)
     *      );
     *      // If validation fails, this line will be reached.
     * 
* * @param validationTasks an array of one or more ValidationTasks * @return a List of zero or more ValidationException * @since 1.0.0 */ protected final List contOnValidationError(final ValidationTask... validationTasks) { final List errors = new ArrayList<>(); for (final ValidationTask validationTask: validationTasks) { if (!validationTask.isRequired() && validationTask.getInput() == null) { continue; } if (!validationTask.getPattern().matcher(validationTask.getInput()).matches()) { errors.add(new ValidationException(validationTask.getInput(), validationTask.getErrorMessage())); } } return errors; } /** * Wrapper around {@link #contOnValidationError(ValidationTask[])} but instead of returning * a list of errors, this method will halt processing of the request by throwing * a BadRequestException, setting the HTTP status to 400 (BAD REQUEST) and providing * a full list of validation errors in the body of the response. * * Usage: *
     *     List<ValidationException> errors = failOnValidationError(
     *         new ValidationTask(pattern, input, errorMessage),
     *         new ValidationTask(pattern, input, errorMessage)
     *      );
     *      // If validation fails, this line will not be reached.
     * 
* * @param validationTasks an array of one or more ValidationTasks * @since 1.0.0 */ protected final void failOnValidationError(final ValidationTask... validationTasks) { final List errors = contOnValidationError(validationTasks); if (! Collections.isEmpty(errors)) { throw new BadRequestException(Response.status(Response.Status.BAD_REQUEST).entity(errors).build()); } } /** * Returns the principal for who initiated the request. * @return a Principal object * @see alpine.model.ApiKey * @see alpine.model.LdapUser * @since 1.0.0 */ protected Principal getPrincipal() { final Object principal = requestContext.getProperty("Principal"); if (principal != null) { return (Principal) principal; } else { return null; } } protected boolean isOidcUser() { return getPrincipal() instanceof OidcUser; } /** * @return true is the current Principal is an instance of LdapUser. False if not. * @since 1.0.0 */ protected boolean isLdapUser() { return getPrincipal() instanceof LdapUser; } /** * @return true is the current Principal is an instance of ManagedUser. False if not. * @since 1.0.0 */ protected boolean isManagedUser() { return getPrincipal() instanceof ManagedUser; } /** * @return true is the current Principal is an instance of ApiKey. False if not. * @since 1.0.0 */ protected boolean isApiKey() { return getPrincipal() instanceof ApiKey; } /** * Convenience method that returns true if the principal has the specified permission, * or false if not. * @param permission the permission to check * @return true if principal has permission assigned, false if not * @since 1.2.0 */ protected boolean hasPermission(final String permission) { if (getPrincipal() == null) { return false; } try (AlpineQueryManager qm = new AlpineQueryManager()) { boolean hasPermission = false; if (getPrincipal() instanceof ApiKey) { hasPermission = qm.hasPermission((ApiKey)getPrincipal(), permission); } else if (getPrincipal() instanceof UserPrincipal) { hasPermission = qm.hasPermission((UserPrincipal)getPrincipal(), permission, true); } return hasPermission; } } /** * Logs a security event to the security audit log. Expects one of: * {@link SecurityMarkers#SECURITY_AUDIT} * {@link SecurityMarkers#SECURITY_SUCCESS} * {@link SecurityMarkers#SECURITY_FAILURE} * @param logger the logger to use * @param marker the marker to add to the event * @param message the initial content of the event * @since 1.0.0 */ protected void logSecurityEvent(final Logger logger, final Marker marker, final String message) { if (!(SecurityMarkers.SECURITY_AUDIT == marker || SecurityMarkers.SECURITY_SUCCESS == marker || SecurityMarkers.SECURITY_FAILURE == marker)) { return; } final StringBuilder sb = new StringBuilder(); sb.append(message).append(" "); if (getPrincipal() != null) { sb.append("by: ").append(getPrincipal().getName()).append(" "); } sb.append("/ IP Address: ").append(getRemoteAddress()).append(" "); sb.append("/ User Agent: ").append(getUserAgent()); logger.info(marker, sb.toString()); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy