GenericUser.java

package cn.home1.oss.lib.security.api;

import static cn.home1.oss.lib.common.CodecUtils.urlDecode;
import static cn.home1.oss.lib.common.CodecUtils.urlEncode;
import static cn.home1.oss.lib.security.api.OAuth2Utils.fromOAuth2Authentication;
import static cn.home1.oss.lib.security.api.OAuth2Utils.isOAuth2Authentication;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.Boolean.FALSE;
import static java.lang.Thread.currentThread;
import static java.util.stream.Collectors.toSet;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.splitPreserveAllTokens;

import com.google.common.collect.ImmutableSet;

import cn.home1.oss.lib.common.Defaults;
import cn.home1.oss.lib.common.JaxbMapAdapter;
import cn.home1.oss.lib.common.JaxbUtils;
import cn.home1.oss.lib.security.internal.BaseGrantedAuthority;

import com.fasterxml.jackson.annotation.JsonIgnore;

import io.swagger.annotations.ApiModelProperty;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

import org.joda.time.DateTime;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.ClassUtils;

import java.security.Principal;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

/**
 * A generic token of user. Never extends GenericUser, use {@link GenericUser#fromPrincipal} {@link
 * GenericUser#fromUser}. username => type:id:name:tel:wx
 */
@XmlRootElement(name = "genericUser")
@XmlAccessorType(XmlAccessType.FIELD)
@Builder(builderMethodName = "genericUserBuilder")
@AllArgsConstructor(access = PACKAGE)
@NoArgsConstructor(access = PRIVATE)
@EqualsAndHashCode(exclude = {"timestamp", "uuid"})
@ToString
@Setter(value = PRIVATE)
@Getter
@Slf4j
@SuppressWarnings({"PMD", "serial"})
public final class GenericUser //
  implements org.springframework.security.core.userdetails.UserDetails, CredentialsContainer {

  public static final String GENERIC_USER_COOKIE = "genericUserCookie";
  public static final String GENERIC_USER_TOKEN = "genericUserToken";
  static final String DELIMITER = "+";
  static final String USER_TYPE_UNKNOWN = "UNKNOWN";
  private static final String CLASS_OAUTH2_AUTHENTICATION = //
    "org.springframework.security.oauth2.provider.OAuth2Authentication";
  private static final Boolean OAUTH2_AUTHENTICATION_PRESENT;

  static {
    OAUTH2_AUTHENTICATION_PRESENT = ClassUtils.isPresent( //
      CLASS_OAUTH2_AUTHENTICATION, currentThread().getContextClassLoader());
  }

  private boolean accountNonExpired;
  private boolean accountNonLocked;
  @ApiModelProperty(dataType = "java.lang.String", example = "ADMIN,USER,OTHER")
  @XmlElementWrapper(name = "authorities")
  @XmlElement(name = "authority", type = BaseGrantedAuthority.class)
  private Set<GrantedAuthority> authorities;
  private boolean credentialsNonExpired;
  private boolean enabled;
  private String password;
  private String username;

  @XmlJavaTypeAdapter(value = JaxbMapAdapter.class, type = Map.class)
  private Map<String, String> properties;
  @ApiModelProperty(hidden = true)
  @NonNull
  @XmlJavaTypeAdapter(value = JaxbUtils.DatimeAdapter.class, type = DateTime.class)
  private DateTime timestamp;
  @NonNull
  private String uuid;

  public static GenericUser fromPrincipal(final Principal principal) {
    final GenericUser result;
    if (principal == null) {
      result = null;
    } else {
      if (principal instanceof PreAuthenticatedAuthenticationToken) {
        final PreAuthenticatedAuthenticationToken token = (PreAuthenticatedAuthenticationToken) principal;
        result = isGenericUser(token.getPrincipal()) ? (GenericUser) token.getPrincipal() : null;
      } else if (principal instanceof UsernamePasswordAuthenticationToken) {
        final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) principal;
        result = isGenericUser(token.getPrincipal()) ? (GenericUser) token.getPrincipal() : null;
      } else if (OAUTH2_AUTHENTICATION_PRESENT && isOAuth2Authentication(principal)) {
        result = fromOAuth2Authentication(principal);
      } else if (principal instanceof Authentication) {
        final Authentication authentication = (Authentication) principal;
        result = isGenericUser(authentication.getPrincipal()) ? (GenericUser) authentication.getPrincipal() : null;
      } else {
        if (log.isInfoEnabled()) {
          log.info("unknown principal: {}:{}", principal.getClass(), principal);
        }
        result = null;
      }
    }
    return result;
  }

  public static Optional<GenericUser> fromSecurityContext() {
    final SecurityContext securityContext = SecurityContextHolder.getContext();
    final Authentication authentication = securityContext.getAuthentication();
    return Optional.ofNullable(fromPrincipal(authentication));
  }

  public static GenericUser fromUser(final User user) {
    return user != null ? fromUser(user, uuid()) : null;
  }

  private static GenericUser fromUser(final User user, final String uuid) {
    final GenericUser output;
    if (user == null) {
      output = null;
    } else {
      final Set<GrantedAuthority> authorities = user.getAuthorities() != null ? //
        user.getAuthorities().stream() //
          .map(authority -> new BaseGrantedAuthority(authority.getAuthority())) //
          .collect(toSet()) : ImmutableSet.of();
      output = new GenericUser();
      output.setAccountNonExpired(user.isAccountNonExpired());
      output.setAccountNonLocked(user.isAccountNonLocked());
      output.setAuthorities(authorities);
      output.setCredentialsNonExpired(user.isCredentialsNonExpired());
      output.setEnabled(user.isEnabled());
      // DaoAuthenticationProvider#additionalAuthenticationChecks need this value
      output.setPassword(user.getPassword());
      output.setUsername(toUsername(user));
      output.setProperties(user.getProperties());

      output.setTimestamp(Defaults.now());
      output.setUuid(uuid);
    }
    return output;
  }

  public static boolean isGenericUser(final Object object) {
    return object != null && GenericUser.class.isAssignableFrom(object.getClass());
  }

  public static boolean isGenericUserLogin(final GenericUser genericUser) {
    return genericUser != null && isNotBlank(genericUser.getId()) && !USER_TYPE_UNKNOWN.equals(genericUser.getType());
  }

  private static String toUsername(final User user) {
    final String type = checkNotNull(user, "null user").getType();
    final String id = user.getId() != null ? user.getId() : "";
    final String name = user.getName();
    return toUsername(type, id, name);
  }

  static String toUsername(final String type, final String id, final String name) {
    return urlEncode(type) + DELIMITER //
      + urlEncode(id) + DELIMITER //
      + urlEncode(isNotBlank(name) ? name : "");
  }

  public static GenericUser unknownUser() {
    return GenericUser.genericUserBuilder() //
      .authorities(ImmutableSet.of()) //
      .enabled(FALSE) //
      .username(GenericUser.toUsername(GenericUser.USER_TYPE_UNKNOWN, "", GenericUser.USER_TYPE_UNKNOWN)) //
      .password("") //
      .accountNonExpired(FALSE) //
      .accountNonLocked(FALSE) //
      .credentialsNonExpired(FALSE) //
      .timestamp(Defaults.now()) //
      .uuid(GenericUser.uuid()) //
      .build();
  }

  private static String uuid() {
    return UUID.randomUUID().toString().replaceAll("-", "");
  }

  @Override
  public void eraseCredentials() {
    this.password = null;
  }

  @JsonIgnore
  public String getType() {
    return this.fromUsername(0, null);
  }

  // @JsonIgnore//api needs expose id
  public String getId() {
    return this.fromUsername(1, "");
  }

  @JsonIgnore
  public String getName() {
    return this.fromUsername(2, "");
  }

  private String fromUsername(final int index, final String defaultValue) {
    return isNotBlank(this.username) ? urlDecode(splitPreserveAllTokens(this.username, DELIMITER)[index]) :
      defaultValue;
  }

  private void setUuid(final String uuid) {
    checkArgument(isNotBlank(uuid));
    this.uuid = uuid.replaceAll("-", "");
  }

  public UserDetails toUserInfo() {
    return UserDetails.userDetailsBuilder()
      .authorities(this.getAuthorities() != null ? this.getAuthorities() : ImmutableSet.of())
      .enabled(this.isEnabled())
      .id(this.getId())
      .name(this.getName())
      .password("[PROTECTED]")
      .properties(this.getProperties())
      .build();
  }
}