ActuatorAuthProperties.java

/*
 * Copyright 2020 the original author or authors.
 *
 * 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.
 */

package org.bremersee.actuator.security.authentication;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.bremersee.security.authentication.AccessExpressionUtils;
import org.bremersee.security.authentication.AutoSecurityMode;
import org.bremersee.security.authentication.ClientCredentialsFlowProperties;
import org.bremersee.security.core.AuthorityConstants;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;

/**
 * Actuator authentication and authorization properties.
 *
 * @author Christian Bremer
 */
@ConfigurationProperties(prefix = "bremersee.actuator.auth")
@Getter
@Setter
@ToString
@EqualsAndHashCode
@Validated
public class ActuatorAuthProperties {

  /**
   * Specifies the behaviour of the actuator security auto configuration.
   */
  @NotNull
  private AutoSecurityMode enable = AutoSecurityMode.OTHER;

  /**
   * The order of the actuator security auto configuration.
   */
  private int order = 61;

  /**
   * Specifies whether cors should be enabled for the actuator endpoints or not.
   */
  private boolean enableCors = true;

  /**
   * A list with unauthenticated actuator endpoints.
   */
  @NotNull
  private List<Class<?>> unauthenticatedEndpoints = new ArrayList<>();

  /**
   * The roles which can write to protected actuator endpoints. The role names normally start with {@code ROLE_}.
   */
  @NotNull
  private List<String> adminRoles = new ArrayList<>();

  /**
   * The roles which can read protected actuator endpoints. The role names normally start with {@code ROLE_}.
   */
  @NotNull
  private List<String> roles = new ArrayList<>();

  /**
   * The IP addresses which can access protected actuator endpoints without authentication.
   */
  @NotNull
  private List<String> ipAddresses = new ArrayList<>();

  /**
   * The JWK uri.
   */
  private String jwkSetUri;

  /**
   * The JWS algorithm
   */
  @NotEmpty
  private String jwsAlgorithm = "RS256";

  /**
   * The issuer uri.
   */
  private String issuerUri;

  /**
   * The json path in the JWT to the roles.
   */
  @NotEmpty
  private String rolesJsonPath = "$.realm_access.roles";

  /**
   * Specifies whether the roles value is a list (json array) or a simple string.
   */
  private boolean rolesValueList = true;

  /**
   * The role value separator to use if the role value is a simple string.
   */
  @NotNull
  private String rolesValueSeparator = " ";

  /**
   * The role prefix to add.
   */
  @NotNull
  private String rolePrefix = "ROLE_";

  /**
   * The json path in the JWT to the user name.
   */
  @NotEmpty
  private String nameJsonPath = "$.preferred_username";

  /**
   * The password flow properties for the actuator endpoints.
   */
  @NotNull
  private ActuatorPasswordFlow passwordFlow = new ActuatorPasswordFlow();

  /**
   * Gets unauthenticated endpoints or defaults (these are the health and info endpoints).
   *
   * @return the unauthenticated endpoints
   */
  @NotNull
  public List<Class<?>> unauthenticatedEndpointsOrDefaults() {
    if (unauthenticatedEndpoints.isEmpty()) {
      unauthenticatedEndpoints.add(
          org.springframework.boot.actuate.health.HealthEndpoint.class);
      unauthenticatedEndpoints.add(
          org.springframework.boot.actuate.info.InfoEndpoint.class);
    }
    return unauthenticatedEndpoints;
  }

  /**
   * Roles or defaults.
   *
   * @return the roles
   */
  @NotNull
  public Set<String> rolesOrDefaults() {
    final TreeSet<String> roleSet = new TreeSet<>(roles);
    if (roleSet.isEmpty()) {
      roleSet.add(AuthorityConstants.ACTUATOR_ROLE_NAME);
      roleSet.add(AuthorityConstants.ACTUATOR_ADMIN_ROLE_NAME);
      roleSet.add(AuthorityConstants.ADMIN_ROLE_NAME);
    }
    return roleSet.stream()
        .map(this::ensureRolePrefix)
        .collect(Collectors.toSet());
  }

  /**
   * Admin roles or defaults.
   *
   * @return the admin roles
   */
  @NotNull
  public Set<String> adminRolesOrDefaults() {
    final TreeSet<String> roleSet = new TreeSet<>(adminRoles);
    if (roleSet.isEmpty()) {
      roleSet.add(AuthorityConstants.ACTUATOR_ADMIN_ROLE_NAME);
      roleSet.add(AuthorityConstants.ADMIN_ROLE_NAME);
    }
    return roleSet.stream()
        .map(this::ensureRolePrefix)
        .collect(Collectors.toSet());
  }

  /**
   * Build access expression (SpEL) for actuator endpoints.
   *
   * @return the access expression (SpEL) for actuator endpoints
   */
  @NotNull
  public String buildAccessExpression() {
    return AccessExpressionUtils.hasAuthorityOrIpAddressExpr(
        rolesOrDefaults(), null, ipAddresses);
  }

  /**
   * Build access expression (SpEL) for admin actuator endpoints.
   *
   * @return the access expression (SpEL) for admin actuator endpoints
   */
  @NotNull
  public String buildAdminAccessExpression() {
    return AccessExpressionUtils.hasAuthorityOrIpAddressExpr(adminRolesOrDefaults(), null, null);
  }

  /**
   * Ensure role prefix.
   *
   * @param role the role
   * @return the role with prefix
   */
  @NotNull
  public String ensureRolePrefix(@NotNull String role) {
    final String prefix = rolePrefix.trim();
    return StringUtils.hasText(prefix) && role.startsWith(prefix) ? role : prefix + role;
  }

  /**
   * OAuth2 password flow configuration properties.
   */
  @Getter
  @Setter
  @ToString(exclude = {"clientSecret"})
  @EqualsAndHashCode(exclude = {"clientSecret"})
  public static class ActuatorPasswordFlow implements ClientCredentialsFlowProperties {

    private String tokenEndpoint;

    private String clientId;

    private String clientSecret;
  }
}