Token.java

/*
 * The coLAB project
 * Copyright (C) 2021-2024 AlbaSim, MEI, HEIG-VD, HES-SO
 *
 * Licensed under the MIT License
 */
package ch.colabproject.colab.api.model.token;

import ch.colabproject.colab.api.controller.token.TokenManager;
import ch.colabproject.colab.api.exceptions.ColabMergeException;
import ch.colabproject.colab.api.model.ColabEntity;
import ch.colabproject.colab.api.model.common.DeletionStatus;
import ch.colabproject.colab.api.model.common.Tracking;
import ch.colabproject.colab.api.model.tools.EntityHelper;
import ch.colabproject.colab.api.model.user.HashMethod;
import ch.colabproject.colab.api.security.permissions.Conditions;
import ch.colabproject.colab.generator.model.exceptions.HttpException;
import ch.colabproject.colab.generator.model.tools.DateSerDe;
import ch.colabproject.colab.generator.model.tools.PolymorphicDeserializer;

import javax.json.bind.annotation.JsonbTransient;
import javax.json.bind.annotation.JsonbTypeDeserializer;
import javax.json.bind.annotation.JsonbTypeSerializer;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.time.OffsetDateTime;
import java.util.Arrays;

/**
 * A token grants access to a specific action.
 *
 * @author maxence
 */
@Entity
// JOINED inheritance will generate one "abstract" account table and one table for each subclass.
// Having one table per subclass allows subclasses to define their own indexes and constraints
@Inheritance(strategy = InheritanceType.JOINED)
@JsonbTypeDeserializer(PolymorphicDeserializer.class)
public abstract class Token implements ColabEntity, TokenWithURL {

    /**
     * a token does not need a salt but, as some HashMethods require one, let's use a hard-coded
     * one
     */
    public static final String SALT = "CAFEBEEF";

    private static final long serialVersionUID = 1L;

    /** token sequence name */
    public static final String TOKEN_SEQUENCE_NAME = "token_seq";

    // ---------------------------------------------------------------------------------------------
    // fields
    // ---------------------------------------------------------------------------------------------

    /**
     * Project ID.
     */
    @Id
    @SequenceGenerator(name = TOKEN_SEQUENCE_NAME, allocationSize = 20)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = TOKEN_SEQUENCE_NAME)
    private Long id;

    /**
     * creation + modification + erasure tracking data
     */
    @Embedded
    private Tracking trackingData;

    /**
     * Is it in a bin or ready to be definitely deleted. Null means active.
     */
    @Enumerated(EnumType.STRING)
    private DeletionStatus deletionStatus;

    /**
     * token hashed with the hashMethod
     */
    @NotNull
    @JsonbTransient
    private byte[] hashedToken;

    /**
     * Hash method used to hash the plainText token
     */
    @Column(length = 100)
    @Enumerated(value = EnumType.STRING)
    @NotNull
    @JsonbTransient
    private HashMethod hashMethod;

    /**
     * Indicate whether a token must be consumed by an unauthenticated user
     */
    @NotNull
    private Boolean authenticationRequired;

    /**
     * token expiration date. TODO: schedule deletion of outdated tokens
     */
    @JsonbTypeDeserializer(DateSerDe.class)
    @JsonbTypeSerializer(DateSerDe.class)
    private OffsetDateTime expirationDate;

    // ---------------------------------------------------------------------------------------------
    // getters and setters
    // ---------------------------------------------------------------------------------------------

    /**
     * @return the token ID
     */
    @Override
    public Long getId() {
        return id;
    }

    /**
     * Set id
     *
     * @param id id
     */
    public void setId(Long id) {
        this.id = id;
    }

    /**
     * Get the tracking data
     *
     * @return tracking data
     */
    @Override
    public Tracking getTrackingData() {
        return trackingData;
    }

    /**
     * Set tracking data
     *
     * @param trackingData new tracking data
     */
    @Override
    public void setTrackingData(Tracking trackingData) {
        this.trackingData = trackingData;
    }

    @Override
    public DeletionStatus getDeletionStatus() {
        return deletionStatus;
    }

    @Override
    public void setDeletionStatus(DeletionStatus status) {
        this.deletionStatus = status;
    }

    /**
     * Get the value of hashedToken
     *
     * @return the value of hashedToken
     */
    public byte[] getHashedToken() {
        return hashedToken;
    }

    /**
     * Set the value of hashedToken
     *
     * @param hashedToken new value of hashedToken
     */
    @Override
    public void setHashedToken(byte[] hashedToken) {
        this.hashedToken = hashedToken;
    }

    /**
     * Get the value of hashMethod
     *
     * @return the value of hashMethod
     */
    public HashMethod getHashMethod() {
        return hashMethod;
    }

    /**
     * Set the value of hashMethod
     *
     * @param hashMethod new value of hashMethod
     */
    @Override
    public void setHashMethod(HashMethod hashMethod) {
        this.hashMethod = hashMethod;
    }

    /**
     * Get the value of authenticationRequired
     *
     * @return the value of authenticationRequired
     */
    public Boolean isAuthenticationRequired() {
        return authenticationRequired;
    }

    /**
     * Set the value of authenticationRequired
     *
     * @param authenticationRequired new value of authenticationRequired
     */
    public void setAuthenticationRequired(Boolean authenticationRequired) {
        this.authenticationRequired = authenticationRequired;
    }

    /**
     * Get the value of expirationDate
     *
     * @return the value of expirationDate
     */
    public OffsetDateTime getExpirationDate() {
        return expirationDate;
    }

    /**
     * Set the value of expirationDate. Null have no time-related expiration.
     *
     * @param expirationDate new value of expirationDate
     */
    public void setExpirationDate(OffsetDateTime expirationDate) {
        this.expirationDate = expirationDate;
    }

    // ---------------------------------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------------------------------

    /**
     * URL to redirect the user to once the token has been consumed.
     *
     * @return redirect to URL
     */

    @SuppressWarnings("unused") // used by front-end
    public abstract String getRedirectTo();

    /**
     * token effect. As some token may require the requestManager, give it to them.
     *
     * @param tokenManager token manager that handles all token-specific logic
     *
     * @return true if the token can be consumed
     *
     * @throws HttpException if consumption fails
     */
    public abstract boolean consume(TokenManager tokenManager);

    /**
     * Does it have to be destroyed after one consumption, or can it live indefinitely.
     *
     * @return the expiration policy
     */
    @JsonbTransient
    public abstract ExpirationPolicy getExpirationPolicy();

    /**
     * Check plain token against hashed persisted one
     *
     * @param plainToken the plain token to check
     *
     * @return true if there is a match
     */
    public boolean checkHash(String plainToken) {
        byte[] submitted = this.getHashMethod().hash(plainToken, SALT);
        return Arrays.equals(submitted, this.getHashedToken());
    }

    /**
     * Check is the token is outdated. A token without expirationDate is never outdated.
     *
     * @return true if the token is outdated.
     */
    @JsonbTransient
    public boolean isOutdated() {
        if (this.expirationDate != null) {
            return this.expirationDate.isBefore(OffsetDateTime.now());
        }
        return false;
    }

    // ---------------------------------------------------------------------------------------------
    // concerning the whole class
    // ---------------------------------------------------------------------------------------------

    @Override
    public void mergeToUpdate(ColabEntity other) throws ColabMergeException {
        // nothing to do
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getReadCondition() {
        return Conditions.alwaysTrue;
    }

    @Override
    @JsonbTransient
    public Conditions.Condition getUpdateCondition() {
        // TODO: decide what to do
        return Conditions.alwaysTrue;
    }

    @Override
    public int hashCode() {
        return EntityHelper.hashCode(this);
    }

    @Override
    @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
    public boolean equals(Object obj) {
        return EntityHelper.equals(this, obj);
    }

}