BasicUser.java

/*
 * Copyright 2018 Global Crop Diversity Trust
 *
 * 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.genesys.blocks.security.model;

import java.time.Instant;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.ElementCollection;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Transient;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.ListUtils;
import org.genesys.blocks.model.JsonViews;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

/**
 * The Class BasicUser.
 *
 * When you extend this class, make sure you use:
 * 
 * <pre>@DiscriminatorValue(value = "1")</pre>
 *
 * @param <R> the generic type
 */
@MappedSuperclass
@DiscriminatorValue(value = "1")
@Getter
@Setter
public abstract class BasicUser<R extends GrantedAuthority> extends AclSid implements UserDetails {

	/** The Constant serialVersionUID. */
	private static final long serialVersionUID = -5318892732608111516L;

	/**
	 * The Enum AccountType.
	 */
	public static enum AccountType {

		/** The local. */
		LOCAL,
		/** The ldap. */
		LDAP,
		/** The google. */
		GOOGLE,
		/** The system. */
		SYSTEM,
		
		/** Deleted user accounts. */
		DELETED
	}

	/** The uuid. */
	@Column(length = 36, unique = true)
	private String uuid;

	/** The email. */
	@JsonView(JsonViews.Public.class)
	@Column(nullable = false, unique = true, length = 60)
	private String email;

	/** The short name. */
	@JsonView(JsonViews.Public.class)
	@Column(unique = true, length = 20)
	private String shortName;

	/** The full name. */
	@JsonView(JsonViews.Public.class)
	@Column(length = 60)
	private String fullName;

	/** The password. */
	@JsonIgnore
	@Column(length = 60)
	private String password;

	/** Account control. */
	@JsonView(JsonViews.Internal.class)
	private Instant accountExpires;

	/** The locked until. */
	@JsonView(JsonViews.Internal.class)
	private Instant lockedUntil;

	/** The password expires. */
	@JsonView(JsonViews.Internal.class)
	private Instant passwordExpires;

	/** The roles. */
	@JsonView(JsonViews.Protected.class)
	@ElementCollection(fetch = FetchType.EAGER)
	@Enumerated(EnumType.STRING)
	@CollectionTable(name = "userrole", joinColumns = @JoinColumn(name = "userId"))
	@Column(name = "role")
	private Set<R> roles = new HashSet<>();

	/** The account type. */
	@JsonView(JsonViews.Protected.class)
	@Enumerated(EnumType.STRING)
	@Column(length = 20)
	private AccountType accountType = AccountType.LOCAL;

	/** The date of last successful login. */
	@JsonView(JsonViews.Internal.class)
	private Instant lastLogin;

	/**
	 * The {@code runtimeAuthorities} are the actual authorities of the user.
	 * They are usually a list of dynamic + assigned roles + default roles IN THAT ORDER!
	 * The order of authorities is important! 
	 */
	@Transient
	@JsonIgnore
	@EqualsAndHashCode.Exclude private List<GrantedAuthority> runtimeAuthorities;

	/**
	 * Instantiates a new basic user.
	 */
	public BasicUser() {
		setPrincipal(true);
	}
	
	/**
	 * Ensure UUID.
	 */
	@PrePersist
	@PreUpdate
	void ensureUUID() {
		if (this.uuid == null) {
			this.uuid = UUID.randomUUID().toString();
		}

		// Use #email as SID name
		setSid(email);
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return getUsername();
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.springframework.security.core.userdetails.UserDetails#getAuthorities()
	 */
	@Transient
	@Override
	@JsonIgnore
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// runtimeAuthorities contain the final set!
		if (CollectionUtils.isNotEmpty(runtimeAuthorities)) {
			return runtimeAuthorities;
		}
		throw new RuntimeException("BUG: runtimeAuthorities are not set!");
	}

	/**
	 * {@link #getUsername()} must return the name used by UserDetailService in the {@link org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(String)}
	 * 
	 * @see org.springframework.security.core.userdetails.UserDetails#getUsername()
	 */
	@Override
	public String getUsername() {
		return email;
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.springframework.security.core.userdetails.UserDetails#isAccountNonExpired
	 * ()
	 */
	@Override
	@JsonView(JsonViews.Protected.class)
	public boolean isAccountNonExpired() {
		return true;
		// TODO Re-enable when we have a way to extend accounts
//		return (accountExpires == null) || !accountExpires.before(new Date());
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.springframework.security.core.userdetails.UserDetails#isAccountNonLocked(
	 * )
	 */
	@Override
	@JsonView(JsonViews.Protected.class)
	public boolean isAccountNonLocked() {
		return (lockedUntil == null) || !lockedUntil.isAfter(Instant.now());
	}

	/**
	 * Checks if is account locked.
	 *
	 * @return true, if is account locked
	 */
	@JsonView(JsonViews.Protected.class)
	public boolean isAccountLocked() {
		return !isAccountNonLocked();
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.security.core.userdetails.UserDetails#
	 * isCredentialsNonExpired()
	 */
	@Override
	@JsonView(JsonViews.Protected.class)
	public boolean isCredentialsNonExpired() {
		return (passwordExpires == null) || !passwordExpires.isBefore(Instant.now());
	}

	/*
	 * (non-Javadoc)
	 * @see org.springframework.security.core.userdetails.UserDetails#isEnabled()
	 */
	@Override
	@JsonView(JsonViews.Protected.class)
	public boolean isEnabled() {
		return super.isActive();
	}
	
	/**
	 * Set the actual authorities to use at runtime. See {@link #runtimeAuthorities}.
	 *
	 * @param authorities the new runtime authorities
	 */
	public void setRuntimeAuthorities(List<GrantedAuthority> authorities) {
		this.runtimeAuthorities = ListUtils.unmodifiableList(authorities);
	}
}