/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.catalina.authenticator;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.Principal;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.http.HttpServletRequest;

import org.apache.catalina.Session;

/**
 * A class that represents entries in the cache of authenticated users. This is necessary to make it available to
 * <code>AuthenticatorBase</code> subclasses that need it in order to perform reauthentications when SingleSignOn is in
 * use.
 *
 * @see SingleSignOn
 * @see AuthenticatorBase#reauthenticateFromSSO
 */
public class SingleSignOnEntry implements Serializable {

    private static final long serialVersionUID = 1L;

    // ------------------------------------------------------ Instance Fields

    /**
     * The authentication type (BASIC, CLIENT_CERT, DIGEST or FORM).
     */
    private String authType = null;

    /**
     * The password used for authentication.
     */
    private String password = null;

    // Marked as transient so special handling can be applied to serialization
    private transient Principal principal = null;

    /**
     * Set of session keys associated with this SSO entry. Backed by ConcurrentHashMap to take
     * advantage of its thread safety features.
     */
    private final Map<SingleSignOnSessionKey,SingleSignOnSessionKey> sessionKeys = new ConcurrentHashMap<>();

    /**
     * The username used for authentication.
     */
    private String username = null;

    /**
     * Flag indicating whether re-authentication is supported.
     */
    private boolean canReauthenticate = false;

    // --------------------------------------------------------- Constructors

    /**
     * Creates a new SingleSignOnEntry
     *
     * @param principal the <code>Principal</code> returned by the latest call to <code>Realm.authenticate</code>.
     * @param authType  the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM)
     * @param username  the username (if any) used for the authentication
     * @param password  the password (if any) used for the authentication
     */
    public SingleSignOnEntry(Principal principal, String authType, String username, String password) {

        updateCredentials(principal, authType, username, password);
    }

    // ------------------------------------------------------- Package Methods

    /**
     * Adds a <code>Session</code> to the list of those associated with this SSO.
     *
     * @param sso     The <code>SingleSignOn</code> valve that is managing the SSO session.
     * @param ssoId   The ID of the SSO session.
     * @param session The <code>Session</code> being associated with the SSO.
     */
    public void addSession(SingleSignOn sso, String ssoId, Session session) {
        SingleSignOnSessionKey key = new SingleSignOnSessionKey(session);
        SingleSignOnSessionKey currentKey = sessionKeys.putIfAbsent(key, key);
        if (currentKey == null) {
            // Session not previously added
            session.addSessionListener(sso.getSessionListener(ssoId));
        }
    }

    /**
     * Removes the given <code>Session</code> from the list of those associated with this SSO.
     *
     * @param session the <code>Session</code> to remove.
     */
    public void removeSession(Session session) {
        SingleSignOnSessionKey key = new SingleSignOnSessionKey(session);
        sessionKeys.remove(key);
    }

    /**
     * Removes the given <code>Session</code> from the list of those associated with this SSO, using the previous
     * sessionId
     *
     * @param session      the <code>Session</code> to remove.
     * @param oldSessionId the previous sessionId of the <code>Session</code> to remove.
     */
    public void removeSession(Session session, String oldSessionId) {
        SingleSignOnSessionKey key = new SingleSignOnSessionKey(session, oldSessionId);
        sessionKeys.remove(key);
    }

    /**
     * Returns the HTTP Session identifiers associated with this SSO.
     *
     * @return the identifiers for the HTTP sessions that are currently associated with this SSO entry
     */
    public Set<SingleSignOnSessionKey> findSessions() {
        return sessionKeys.keySet();
    }

    /**
     * Gets the name of the authentication type originally used to authenticate the user associated with the SSO.
     *
     * @return "BASIC", "CLIENT_CERT", "DIGEST", "FORM" or "NONE"
     */
    public String getAuthType() {
        return this.authType;
    }

    /**
     * Gets whether the authentication type associated with the original authentication supports reauthentication.
     *
     * @return <code>true</code> if <code>getAuthType</code> returns "BASIC" or "FORM", <code>false</code> otherwise.
     */
    public boolean getCanReauthenticate() {
        return this.canReauthenticate;
    }

    /**
     * Gets the password credential (if any) associated with the SSO.
     *
     * @return the password credential associated with the SSO, or <code>null</code> if the original authentication type
     *             does not involve a password.
     */
    public String getPassword() {
        return this.password;
    }

    /**
     * Gets the <code>Principal</code> that has been authenticated by the SSO.
     *
     * @return The Principal that was created by the authentication that triggered the creation of the SSO entry
     */
    public Principal getPrincipal() {
        return this.principal;
    }

    /**
     * Gets the username provided by the user as part of the authentication process.
     *
     * @return The username that was authenticated as part of the authentication that triggered the creation of the SSO
     *             entry
     */
    public String getUsername() {
        return this.username;
    }


    /**
     * Updates the SingleSignOnEntry to reflect the latest security information associated with the caller.
     *
     * @param principal the <code>Principal</code> returned by the latest call to <code>Realm.authenticate</code>.
     * @param authType  the type of authenticator used (BASIC, CLIENT_CERT, DIGEST or FORM)
     * @param username  the username (if any) used for the authentication
     * @param password  the password (if any) used for the authentication
     */
    public synchronized void updateCredentials(Principal principal, String authType, String username, String password) {
        this.principal = principal;
        this.authType = authType;
        this.username = username;
        this.password = password;
        this.canReauthenticate =
                (HttpServletRequest.BASIC_AUTH.equals(authType) || HttpServletRequest.FORM_AUTH.equals(authType));
    }


    /**
     * Custom serialization to handle the transient principal field.
     *
     * @param out the object output stream
     * @throws IOException if an I/O error occurs
     */
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        if (principal instanceof Serializable) {
            out.writeBoolean(true);
            out.writeObject(principal);
        } else {
            out.writeBoolean(false);
        }
    }

    /**
     * Custom deserialization to restore the transient principal field.
     *
     * @param in the object input stream
     * @throws IOException if an I/O error occurs
     * @throws ClassNotFoundException if the principal class cannot be found
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        boolean hasPrincipal = in.readBoolean();
        if (hasPrincipal) {
            principal = (Principal) in.readObject();
        }
    }
}
