AccessExpressionUtils.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.security.authentication;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.bremersee.security.authentication.AuthProperties.EurekaAccessProperties;
import org.bremersee.security.authentication.AuthProperties.PathMatcherProperties;
import org.springframework.util.StringUtils;

/**
 * The access expression utilities.
 *
 * @author Christian Bremer
 */
public abstract class AccessExpressionUtils {

  /**
   * The deny all spring expression.
   */
  static final String DENY_ALL = "denyAll";

  /**
   * The permit all spring expression.
   */
  static final String PERMIT_ALL = "permitAll";

  /**
   * The is authenticated spring expression.
   */
  static final String IS_AUTHENTICATED = "isAuthenticated()";

  private static final String HAS_AUTHORITY_TEMPLATE = "hasAuthority('%s')";

  private static final String HAS_ANY_AUTHORITY_TEMPLATE = "hasAnyAuthority(%s)";

  private static final String HAS_IP_ADDRESS_TEMPLATE = "hasIpAddress('%s')";

  private AccessExpressionUtils() {
  }

  /**
   * Builds the {@code hasAuthority} expression.
   *
   * @param role the role
   * @param ensurePrefixFunction the ensure prefix function
   * @return the {@code hasAuthority} expression
   */
  public static String hasAuthorityExpr(
      String role,
      Function<String, String> ensurePrefixFunction) {

    return String.format(
        HAS_AUTHORITY_TEMPLATE,
        Optional.ofNullable(ensurePrefixFunction)
            .map(f -> f.apply(role))
            .orElse(role));
  }

  /**
   * Builds the {@code hasAnyAuthority} expression.
   *
   * @param roles the roles
   * @param ensurePrefixFunction the ensure prefix function
   * @return the {@code hasAnyAuthority} expression
   */
  public static String hasAnyAuthorityExpr(
      Collection<String> roles,
      Function<String, String> ensurePrefixFunction) {

    return Optional.ofNullable(roles)
        .map(list -> hasAnyAuthorityExprNullSave(
            list.stream().filter(StringUtils::hasText).collect(Collectors.toList()),
            ensurePrefixFunction))
        .orElse("");
  }

  private static String hasAnyAuthorityExprNullSave(
      Collection<String> roles,
      Function<String, String> ensurePrefixFunction) {

    return Optional.of(roles.size())
        .filter(size -> size > 1)
        .map(size -> roles.stream()
            .filter(StringUtils::hasText)
            .map(role -> Optional.ofNullable(ensurePrefixFunction)
                .map(f -> f.apply(role))
                .orElse(role))
            .map(role -> "'" + role + "'")
            .collect(Collectors.joining(",")))
        .map(value -> String.format(HAS_ANY_AUTHORITY_TEMPLATE, value))
        .orElseGet(() -> hasAuthorityExpr(roles.stream()
            .findFirst().orElse(""), ensurePrefixFunction));
  }

  /**
   * Builds the {@code hasIpAddress} expression.
   *
   * @param ip the ip
   * @return the {@code hasIpAddress} expression
   */
  public static String hasIpAddressExpr(String ip) {
    if (StringUtils.hasText(ip)) {
      return String.format(HAS_IP_ADDRESS_TEMPLATE, ip);
    }
    return "";
  }

  /**
   * Builds the {@code hasIpAddress} expression.
   *
   * @param ips the ips
   * @return the {@code hasIpAddress} expression
   */
  public static String hasIpAddressExpr(Collection<String> ips) {
    return Optional.ofNullable(ips)
        .map(list -> list.stream()
            .filter(StringUtils::hasText)
            .map(AccessExpressionUtils::hasIpAddressExpr)
            .collect(Collectors.joining(" or ")))
        .orElse("");
  }

  /**
   * Builds an access expression from the given roles and ip addresses.
   *
   * @param roles the roles
   * @param ensurePrefixFunction the ensure prefix function
   * @param ips the ips
   * @return the access expression
   */
  public static String hasAuthorityOrIpAddressExpr(
      Collection<String> roles,
      Function<String, String> ensurePrefixFunction,
      Collection<String> ips) {

    TreeSet<String> roleSet = roles instanceof TreeSet
        ? (TreeSet<String>) roles
        : roles == null ? new TreeSet<>() : new TreeSet<>(roles);
    StringBuilder sb = new StringBuilder();
    if (roleSet.size() > 1) {
      sb.append(hasAnyAuthorityExpr(roleSet, ensurePrefixFunction));
    } else if (roleSet.size() == 1) {
      sb.append(hasAuthorityExpr(roleSet.first(), ensurePrefixFunction));
    }
    Set<String> ipSet = ips instanceof Set
        ? (Set<String>) ips
        : ips == null ? Collections.emptySet() : new LinkedHashSet<>(ips);
    if (!ipSet.isEmpty()) {
      if (sb.length() > 0) {
        sb.append(" or ");
      }
      sb.append(hasIpAddressExpr(ipSet));
    }
    return sb.toString();
  }

  /**
   * Build access expression of the given path matcher properties.
   *
   * @param properties the properties
   * @param ensureRolePrefixFunction the ensure role prefix function
   * @return the access expression
   */
  static String buildAccessExpression(
      PathMatcherProperties properties,
      Function<String, String> ensureRolePrefixFunction) {

    if (AccessMode.AUTHENTICATED == properties.getAccessMode()) {
      StringBuilder sb = new StringBuilder();
      sb.append(hasAuthorityOrIpAddressExpr(
          properties.getRoles(),
          ensureRolePrefixFunction,
          properties.getIpAddresses()));
      if (properties.getRoles() == null || properties.getRoles().isEmpty()) {
        if (sb.length() > 0) {
          sb.append(" or ");
        }
        sb.append(IS_AUTHENTICATED);
      }
      return sb.toString();
    }
    return properties.getAccessMode().getExpressionValue();
  }

  /**
   * Build access expression of the given eureka access properties.
   *
   * @param properties the properties
   * @param ensureRolePrefixFunction the ensure role prefix function
   * @return the access expression
   */
  static String buildAccessExpression(
      EurekaAccessProperties properties,
      Function<String, String> ensureRolePrefixFunction) {

    if (StringUtils.hasText(properties.getRole())) {
      return hasAuthorityOrIpAddressExpr(
          Collections.singleton(properties.getRole()),
          ensureRolePrefixFunction,
          properties.getIpAddresses());
    }
    String ipsExpr = hasIpAddressExpr(properties.getIpAddresses());
    return StringUtils.hasText(ipsExpr)
        ? IS_AUTHENTICATED + " or " + ipsExpr
        : IS_AUTHENTICATED;
  }

}