LdaptiveAuthenticationProperties.java

/*
 * Copyright 2014 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.spring.security.ldaptive.authentication;

import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bremersee.spring.security.core.authority.mapping.CaseTransformation;
import org.ldaptive.SearchScope;
import org.springframework.util.ObjectUtils;

/**
 * The ldaptive authentication properties.
 *
 * @author Christian Bremer
 */
@Data
@NoArgsConstructor
public class LdaptiveAuthenticationProperties implements Serializable {

  @Serial
  private static final long serialVersionUID = 1L;

  /**
   * The username (like 'anna') to bind dn (like 'cn=anna,ou=people,dc=example,dc=org') converter.
   */
  protected UsernameToBindDnConverterProperty usernameToBindDnConverter;

  /**
   * The user base dn (like 'ou=people,dc=example,dc=org'). This value is always required.
   */
  protected String userBaseDn;

  /**
   * A list with refused usernames.
   */
  protected List<String> refusedUsernames;

  /**
   * The object class of the user (like 'inetOrgPerson'). The selected template contains a default.
   */
  protected String userObjectClass;

  /**
   * The username attribute of the user (like 'uid' or 'sAMAccountName'). The selected template
   * contains a default.
   */
  protected String usernameAttribute;

  /**
   * Applies only for simple bind. The rdn attribute of the user. This is normally the same as the
   * username attribute.
   */
  protected String userRdnAttribute;

  /**
   * The password attribute of the user (like 'userPassword'). If it is empty, a simple user bind
   * will be done with the credentials of the user for authentication. If it is present, the
   * connection to the ldap server must be done by a 'global' user and a password encoder that fits
   * your requirements must be present. The default password encoder only supports SHA, that is
   * insecure.
   */
  protected String passwordAttribute;

  /**
   * The password last set attribute (like 'pwdLastSet') can be used to activate the remember-me
   * functionality.
   */
  protected String passwordLastSetAttribute;

  /**
   * The filter to find the user. If it is empty, it will be generated from {@code userObjectClass}
   * and {@code usernameAttribute} like this {@code (&(objectClass=inetOrgPerson)(uid={0}))}.
   */
  protected String userFindOneFilter;

  /**
   * The scope to find a user. Default is 'one level'.
   */
  protected SearchScope userFindOneSearchScope;

  /**
   * The first name attribute of the user. Default is 'givenName'.
   */
  protected String firstNameAttribute;

  /**
   * The last name attribute of the user. Default is 'sn'.
   */
  protected String lastNameAttribute;

  /**
   * The email attribute of the user. Default is 'mail';
   */
  protected String emailAttribute;

  /**
   * The account control evaluator.
   */
  protected AccountControlEvaluatorProperty accountControlEvaluator;

  /**
   * The group fetch strategy.
   */
  protected GroupFetchStrategy groupFetchStrategy;

  /**
   * The member attribute.
   */
  protected String memberAttribute;

  /**
   * The group base dn (like 'ou=groups,dc=example,dc=org'). It's only required, if
   * {@code groupFetchStrategy} is set to {@code GROUP_CONTAINS_USERS}.
   */
  protected String groupBaseDn;

  /**
   * The group search scope. It's only required, if {@code groupFetchStrategy} is set to
   * {@code GROUP_CONTAINS_USERS},
   */
  protected SearchScope groupSearchScope;

  /**
   * The group object class. It's only required, if {@code groupFetchStrategy} is set to
   * {@code GROUP_CONTAINS_USERS}
   */
  protected String groupObjectClass;

  /**
   * The group id attribute. It's only required, if {@code groupFetchStrategy} is set to
   * {@code GROUP_CONTAINS_USERS}
   */
  protected String groupIdAttribute;

  /**
   * The group member attribute. It's only required, if {@code groupFetchStrategy} is set to
   * {@code GROUP_CONTAINS_USERS}
   */
  protected String groupMemberAttribute;

  /**
   * The group member format. It's only required, if {@code groupFetchStrategy} is set to
   * {@code GROUP_CONTAINS_USERS}
   */
  protected String groupMemberFormat;

  /**
   * The role mappings.
   */
  protected List<RoleMapping> roleMapping;

  /**
   * The default roles.
   */
  protected List<String> defaultRoles;

  /**
   * The role prefix (like 'ROLE_').
   */
  protected String rolePrefix;

  /**
   * The role case transformation.
   */
  protected CaseTransformation roleCaseTransformation;

  /**
   * The string replacements for roles.
   */
  protected List<StringReplacement> roleStringReplacements;

  /**
   * To role mappings map.
   *
   * @return the map
   */
  public Map<String, String> toRoleMappings() {
    return Stream.ofNullable(getRoleMapping())
        .flatMap(Collection::stream)
        .collect(Collectors.toMap(
            RoleMapping::getSource,
            RoleMapping::getTarget,
            (first, second) -> first,
            LinkedHashMap::new));
  }

  /**
   * To role string replacements map.
   *
   * @return the map
   */
  public Map<String, String> toRoleStringReplacements() {
    return Stream.ofNullable(getRoleStringReplacements())
        .flatMap(Collection::stream)
        .collect(Collectors.toMap(
            StringReplacement::getRegex,
            StringReplacement::getReplacement,
            (first, second) -> first,
            LinkedHashMap::new));
  }

  /**
   * The ldaptive authentication properties with defaults.
   */
  public static class WithDefaults extends LdaptiveAuthenticationProperties {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * Instantiates a new ldaptive authentication properties with defaults.
     */
    public WithDefaults() {
      refusedUsernames = new ArrayList<>();

      usernameToBindDnConverter = UsernameToBindDnConverterProperty.BY_USER_RDN_ATTRIBUTE;

      userObjectClass = "inetOrgPerson";
      usernameAttribute = "uid";

      userFindOneSearchScope = SearchScope.ONELEVEL;
      firstNameAttribute = "givenName";
      lastNameAttribute = "sn";
      emailAttribute = "mail";
      memberAttribute = "memberOf";
      accountControlEvaluator = AccountControlEvaluatorProperty.NONE;

      groupFetchStrategy = GroupFetchStrategy.USER_CONTAINS_GROUPS;
      groupObjectClass = "groupOfUniqueNames";
      groupMemberAttribute = "uniqueMember";
      groupSearchScope = SearchScope.ONELEVEL;
      roleMapping = new ArrayList<>();
      defaultRoles = new ArrayList<>();
      roleStringReplacements = new ArrayList<>();
      roleCaseTransformation = CaseTransformation.NONE;
    }

    /**
     * Get user rdn attribute.
     *
     * @return the user rdn attribute
     */
    @Override
    public String getUserRdnAttribute() {
      if (ObjectUtils.isEmpty(userRdnAttribute)) {
        return usernameAttribute;
      }
      return userRdnAttribute;
    }

    /**
     * Get user find one filter.
     *
     * @return the user find one filter
     */
    @Override
    public String getUserFindOneFilter() {
      if (ObjectUtils.isEmpty(userFindOneFilter)
          && !ObjectUtils.isEmpty(getUserObjectClass())
          && !ObjectUtils.isEmpty(getUsernameAttribute())) {
        return String
            .format("(&(objectClass=%s)(%s={0}))", getUserObjectClass(), getUsernameAttribute());
      }
      return userFindOneFilter;
    }

  }

  /**
   * The group fetch strategy.
   */
  public enum GroupFetchStrategy {

    /**
     * Groups will not be fetched.
     */
    NONE,

    /**
     * User contains groups group-fetch strategy.
     */
    USER_CONTAINS_GROUPS,

    /**
     * Group contains users group-fetch strategy.
     */
    GROUP_CONTAINS_USERS
  }

  /**
   * The string replacement.
   */
  @Data
  @AllArgsConstructor
  public static class StringReplacement implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * The regular expression to which the string is to be matched. '{@code [- ]}' for example would
     * replace every '-' and every space.
     */
    private String regex;

    /**
     * The string to be substituted for each match.
     */
    private String replacement;

    /**
     * Instantiates a new string replacement.
     */
    public StringReplacement() {
      super();
    }
  }

  /**
   * The role mapping.
   */
  @Data
  @AllArgsConstructor
  public static class RoleMapping implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * The value from the ldap (like 'developers').
     */
    private String source;

    /**
     * The value in the spring security context (like 'ROLE_DEVELOPER').
     */
    private String target;

    /**
     * Instantiates a new role mapping.
     */
    public RoleMapping() {
      super();
    }
  }

}