001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io.auth;
003
004import java.net.Authenticator.RequestorType;
005import java.net.PasswordAuthentication;
006import java.util.EnumMap;
007import java.util.Map;
008import java.util.Objects;
009
010import org.openstreetmap.josm.tools.Logging;
011
012/**
013 * Partial implementation of the {@link CredentialsAgent} interface.
014 * <p>
015 * Provides a memory cache for the credentials and means to query the information from the user.
016 * @since 4246
017 */
018public abstract class AbstractCredentialsAgent implements CredentialsAgent {
019
020    /**
021     * Synchronous credentials provider. Called if no credentials are cached. Can be used for user login prompt.
022     * @since 12821
023     */
024    @FunctionalInterface
025    public interface CredentialsProvider {
026        /**
027         * Fills the given response with appropriate user credentials.
028         * @param requestorType type of the entity requesting authentication
029         * @param agent the credentials agent requesting credentials
030         * @param response authentication response to fill
031         * @param username the known username, if any. Likely to be empty
032         * @param password the known password, if any. Likely to be empty
033         * @param host the host against authentication will be performed
034         */
035        void provideCredentials(RequestorType requestorType, AbstractCredentialsAgent agent, CredentialsAgentResponse response,
036                String username, String password, String host);
037    }
038
039    private static volatile CredentialsProvider credentialsProvider =
040            (a, b, c, d, e, f) -> Logging.error("Credentials provider has not been set");
041
042    /**
043     * Sets the global credentials provider.
044     * @param provider credentials provider. Called if no credentials are cached. Can be used for user login prompt
045     */
046    public static void setCredentialsProvider(CredentialsProvider provider) {
047        credentialsProvider = Objects.requireNonNull(provider, "provider");
048    }
049
050    protected Map<RequestorType, PasswordAuthentication> memoryCredentialsCache = new EnumMap<>(RequestorType.class);
051
052    @Override
053    public CredentialsAgentResponse getCredentials(final RequestorType requestorType, final String host, boolean noSuccessWithLastResponse)
054            throws CredentialsAgentException {
055        if (requestorType == null)
056            return null;
057        PasswordAuthentication credentials = lookup(requestorType, host);
058        final String username = (credentials == null || credentials.getUserName() == null) ? "" : credentials.getUserName();
059        final String password = (credentials == null || credentials.getPassword() == null) ? "" : String.valueOf(credentials.getPassword());
060
061        final CredentialsAgentResponse response = new CredentialsAgentResponse();
062
063        /*
064         * Last request was successful and there was no credentials stored in file (or only the username is stored).
065         * -> Try to recall credentials that have been entered manually in this session.
066         */
067        if (!noSuccessWithLastResponse && memoryCredentialsCache.containsKey(requestorType) &&
068                (credentials == null || credentials.getPassword() == null || credentials.getPassword().length == 0)) {
069            PasswordAuthentication pa = memoryCredentialsCache.get(requestorType);
070            response.setUsername(pa.getUserName());
071            response.setPassword(pa.getPassword());
072            response.setCanceled(false);
073        /*
074         * Prompt the user for credentials. This happens the first time each
075         * josm start if the user does not save the credentials to preference
076         * file (username=="") and each time after authentication failed
077         * (noSuccessWithLastResponse == true).
078         */
079        } else if (noSuccessWithLastResponse || username.isEmpty() || password.isEmpty()) {
080            credentialsProvider.provideCredentials(requestorType, this, response, username, password, host);
081            if (response.isCanceled() || response.getUsername() == null || response.getPassword() == null) {
082                return response;
083            }
084            if (response.isSaveCredentials()) {
085                store(requestorType, host, new PasswordAuthentication(
086                        response.getUsername(),
087                        response.getPassword()
088                ));
089            } else {
090                // User decides not to save credentials to file. Keep it in memory so we don't have to ask over and over again.
091                memoryCredentialsCache.put(requestorType, new PasswordAuthentication(response.getUsername(), response.getPassword()));
092            }
093        } else {
094            // We got it from file.
095            response.setUsername(username);
096            response.setPassword(password.toCharArray());
097            response.setCanceled(false);
098        }
099        return response;
100    }
101
102    @Override
103    public final void purgeCredentialsCache(RequestorType requestorType) {
104        memoryCredentialsCache.remove(requestorType);
105    }
106
107    /**
108     * Provide the text for a checkbox that offers to save the
109     * username and password that has been entered by the user.
110     * @return checkbox text
111     */
112    public abstract String getSaveUsernameAndPasswordCheckboxText();
113}