Ticket #2710: initial_multiuser_rework.patch

File initial_multiuser_rework.patch, 23.3 KB (added by taylor.smock, 7 years ago)

Initial patch to support multiple users. This is mostly adding new methods to support passing different auth tokens around. The upload panel has a dropdown box that currently doesn't do anything except show the logged in user's name, which is the reason for adding support for passing different auth tokens. TODO actually use input from the dropdown box and create a method to add additional user accounts.

  • src/org/openstreetmap/josm/data/UserIdentityManager.java

     
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    66import java.text.MessageFormat;
     7import java.util.ArrayList;
     8import java.util.LinkedHashMap;
     9import java.util.List;
     10import java.util.Map;
    711
    812import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
    913import org.openstreetmap.josm.data.osm.User;
     
    7882                instance.initFromPreferences();
    7983            }
    8084            Config.getPref().addPreferenceChangeListener(instance);
     85            instance.populateAllUsers();
    8186        }
    8287        return instance;
    8388    }
    8489
    85     private String userName;
    86     private UserInfo userInfo;
    8790    private boolean accessTokenKeyChanged;
    8891    private boolean accessTokenSecretChanged;
    8992
     93    private String userName;
     94    private UserInfo userInfo;
     95    private LinkedHashMap<String, UserInfo> users;
     96
    9097    private UserIdentityManager() {
     98        users = new LinkedHashMap<>();
    9199    }
    92100
    93101    /**
     
    112120        if (trimmedUserName.isEmpty())
    113121            throw new IllegalArgumentException(
    114122                    MessageFormat.format("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName));
    115         this.userName = trimmedUserName;
     123        userName = trimmedUserName;
    116124        userInfo = null;
    117125    }
    118126
     
    132140        if (trimmedUserName.isEmpty())
    133141            throw new IllegalArgumentException(tr("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName));
    134142        CheckParameterUtil.ensureParameterNotNull(userInfo, "userInfo");
    135         this.userName = trimmedUserName;
     143        userName = trimmedUserName;
    136144        this.userInfo = userInfo;
    137145    }
    138146
     
    238246    }
    239247
    240248    /**
     249     * Initializes the user identity manager from OAuth request of user details.
     250     * @param oauth The {@code OAuthAccessTokenHolder} with the key and secret
     251     * @see #initFromPreferences
     252     * @since xxx
     253     */
     254    public void initFromOauth(OAuthAccessTokenHolder oauth) {
     255        try {
     256            OsmServerUserInfoReader osmServerReader = new OsmServerUserInfoReader();
     257            UserInfo info = osmServerReader.fetchUserInfo(NullProgressMonitor.INSTANCE, oauth);
     258            setFullyIdentified(info.getDisplayName(), info);
     259        } catch (IllegalArgumentException | OsmTransferException e) {
     260            Logging.error(e);
     261        }
     262    }
     263
     264
     265    private List<List<String>> getDefaultOAuthList() {
     266        List<String> variables = new ArrayList<>();
     267        variables.add(Config.getPref().get("oauth.settings.access-token-url"));
     268        variables.add(Config.getPref().get("oauth.access-token.key"));
     269        variables.add(Config.getPref().get("oauth.access-token.secret"));
     270        List<List<String>> rList = new ArrayList<>();
     271        rList.add(variables);
     272        return rList;
     273    }
     274    /**
     275     * Populate the users
     276     * @since xxx
     277     */
     278    public void populateAllUsers() {
     279        if (OsmApi.isUsingOAuth() && OAuthAccessTokenHolder.getInstance().containsAccessToken() &&
     280                !NetworkManager.isOffline(OnlineResource.OSM_API)) {
     281            OAuthAccessTokenHolder oauth = new OAuthAccessTokenHolder();
     282            List<List<String>> authList = Config.getPref().getListOfLists("oauth.all-tokens", getDefaultOAuthList());
     283            for (List<String> list : authList) { // TODO fix
     284                oauth.setAccessToken(list.get(1), list.get(2));
     285                instance.initFromOauth(oauth);
     286                users.put(getUserName(), getUserInfo());
     287            }
     288            try {
     289                instance.initFromOAuth();
     290            } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
     291                Logging.error(e);
     292                // Fall back to preferences if OAuth identification fails for any reason
     293                instance.initFromPreferences();
     294            }
     295        } else {
     296            instance.initFromPreferences();
     297        }
     298
     299    }
     300
     301    /**
    241302     * Replies true if the user with name <code>username</code> is the current user
    242303     *
    243304     * @param userName the user name
     
    264325        }
    265326    }
    266327
     328    /**
     329     * Get all information on all users that have logged in to JOSM
     330     * @return A {@code HashMap} with username/UserInfo pairs.
     331     */
     332    public Map<String, UserInfo> getAllUserInformation() {
     333        return users;
     334    }
     335
    267336    /* ------------------------------------------------------------------- */
    268337    /* interface PreferenceChangeListener                                  */
    269338    /* ------------------------------------------------------------------- */
  • src/org/openstreetmap/josm/data/oauth/OAuthAccessTokenHolder.java

     
    33
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
     6import org.openstreetmap.josm.io.OsmApi;
    67import org.openstreetmap.josm.io.auth.CredentialsAgent;
    78import org.openstreetmap.josm.io.auth.CredentialsAgentException;
    89import org.openstreetmap.josm.spi.preferences.Config;
     
    1617public class OAuthAccessTokenHolder {
    1718    private static OAuthAccessTokenHolder instance;
    1819
     20    private OsmApi osmApi = OsmApi.getOsmApi();
     21
    1922    /**
    2023     * Replies the unique instance.
    2124     * @return The unique instance of {@code OAuthAccessTokenHolder}
     
    186189    }
    187190
    188191    /**
     192     * Set the API to use with the user
     193     * @param serverUrl The URL for the OSM server
     194     * @since xxx
     195     */
     196    public void setOsmApi(String serverUrl) {
     197        osmApi = OsmApi.getOsmApi(serverUrl);
     198    }
     199
     200    /**
     201     * Get the osmApi for use with this oauth object
     202     * @return The OsmApi to use
     203     */
     204    public OsmApi getOsmApi() {
     205        return osmApi;
     206    }
     207
     208    /**
    189209     * Clears the content of this holder
    190210     */
    191211    public void clear() {
  • src/org/openstreetmap/josm/gui/io/UploadParameterSummaryPanel.java

     
    1010import java.util.Optional;
    1111
    1212import javax.swing.BorderFactory;
     13import javax.swing.DefaultComboBoxModel;
     14import javax.swing.JComboBox;
    1315import javax.swing.JLabel;
    1416import javax.swing.JPanel;
    1517import javax.swing.event.HyperlinkEvent;
    1618import javax.swing.event.HyperlinkListener;
    1719
     20import org.openstreetmap.josm.data.UserIdentityManager;
    1821import org.openstreetmap.josm.data.osm.Changeset;
     22import org.openstreetmap.josm.data.osm.UserInfo;
    1923import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
    2024import org.openstreetmap.josm.io.Capabilities;
    2125import org.openstreetmap.josm.io.OsmApi;
     
    114118        return msg;
    115119    }
    116120
     121    protected JComboBox<String> buildPossibleUserBox() {
     122        DefaultComboBoxModel<String> model = new DefaultComboBoxModel<>();
     123        UserInfo user = UserIdentityManager.getInstance().getUserInfo();
     124        String userName = user.getDisplayName() != null ? user.getDisplayName() : tr("Please login");
     125        model.addElement(userName);
     126        JComboBox<String> rBox = new JComboBox<>(model);
     127        return rBox;
     128    }
     129
    117130    protected void build() {
    118131        jepMessage = new JMultilineLabel("");
    119132        jepMessage.addHyperlinkListener(this);
     
    128141        JPanel pnl = new JPanel(new BorderLayout());
    129142        pnl.add(lblWarning, BorderLayout.NORTH);
    130143        add(pnl, BorderLayout.WEST);
     144        add(buildPossibleUserBox(), BorderLayout.SOUTH);
    131145    }
    132146
    133147    public void setConfigurationParameterRequestListener(ConfigurationParameterRequestHandler handler) {
  • src/org/openstreetmap/josm/io/OsmConnection.java

     
    104104     * Adds an authentication header for basic authentication
    105105     *
    106106     * @param con the connection
    107      * @throws OsmTransferException if something went wrong. Check for nested exceptions
     107     * @param response the response with username/password information
    108108     */
    109     protected void addBasicAuthorizationHeader(HttpClient con) throws OsmTransferException {
    110         CredentialsAgentResponse response;
    111         try {
    112             synchronized (CredentialsManager.getInstance()) {
    113                 response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
    114                 con.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
    115             }
    116         } catch (CredentialsAgentException e) {
    117             throw new OsmTransferException(e);
    118         }
     109    protected void addBasicAuthorizationHeader(HttpClient con, CredentialsAgentResponse response) {
    119110        if (response != null) {
    120111            if (response.isCanceled()) {
    121112                cancel = true;
     
    132123     * Signs the connection with an OAuth authentication header
    133124     *
    134125     * @param connection the connection
     126     * @param holder specific OAuth access token
    135127     *
    136128     * @throws MissingOAuthAccessTokenException if there is currently no OAuth Access Token configured
    137129     * @throws OsmTransferException if signing fails
    138130     */
    139     protected void addOAuthAuthorizationHeader(HttpClient connection) throws OsmTransferException {
     131    protected void addOAuthAuthorizationHeader(HttpClient connection, OAuthAccessTokenHolder holder) throws OsmTransferException {
    140132        if (oauthParameters == null) {
    141133            oauthParameters = OAuthParameters.createFromApiUrl(OsmApi.getOsmApi().getServerUrl());
    142134        }
    143135        OAuthConsumer consumer = oauthParameters.buildConsumer();
    144         OAuthAccessTokenHolder holder = OAuthAccessTokenHolder.getInstance();
    145136        if (!holder.containsAccessToken()) {
    146137            obtainAccessToken(connection);
    147138        }
     
    177168    }
    178169
    179170    protected void addAuth(HttpClient connection) throws OsmTransferException {
    180         final String authMethod = OsmApi.getAuthMethod();
    181         if ("basic".equals(authMethod)) {
    182             addBasicAuthorizationHeader(connection);
    183         } else if ("oauth".equals(authMethod)) {
    184             addOAuthAuthorizationHeader(connection);
     171        addAuth(connection, null);
     172    }
     173
     174    /**
     175     * Add authorization information to a connection
     176     * @param connection to add authorization information to
     177     * @param holder A {@code CredentialsAgentResponse} for basic authorization,
     178     * {@code OAuthAccessTokenHolder} for OAuth, or {@code null} for defaults.
     179     * @throws OsmTransferException if the authorization is not valid
     180     */
     181    protected void addAuth(HttpClient connection, Object holder) throws OsmTransferException{
     182        if (holder == null) {
     183            final String authMethod = OsmApi.getAuthMethod();
     184            if ("basic".equals(authMethod)) {
     185                CredentialsAgentResponse response;
     186                try {
     187                    synchronized (CredentialsManager.getInstance()) {
     188                        response = CredentialsManager.getInstance().getCredentials(RequestorType.SERVER,
     189                        connection.getURL().getHost(), false /* don't know yet whether the credentials will succeed */);
     190                    }
     191                } catch (CredentialsAgentException e) {
     192                    throw new OsmTransferException(e);
     193                }
     194                holder = response;
     195            } else if ("oauth".equals(authMethod)) {
     196                holder = OAuthAccessTokenHolder.getInstance();
     197            } else {
     198                String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
     199                Logging.warn(msg);
     200                throw new OsmTransferException(msg);
     201            }
     202        }
     203
     204        if (holder instanceof OAuthAccessTokenHolder) {
     205            addOAuthAuthorizationHeader(connection, (OAuthAccessTokenHolder) holder);
     206        } else if (holder instanceof CredentialsAgentResponse) {
     207            addBasicAuthorizationHeader(connection, (CredentialsAgentResponse) holder);
    185208        } else {
    186             String msg = tr("Unexpected value for preference ''{0}''. Got ''{1}''.", "osm-server.auth-method", authMethod);
     209            String msg = tr("Unexpected object for authorizations. Got ''{0}''.", holder.getClass().getName());
    187210            Logging.warn(msg);
    188211            throw new OsmTransferException(msg);
    189212        }
  • src/org/openstreetmap/josm/io/OsmServerReader.java

     
    7979     * @throws OsmTransferException if data transfer errors occur
    8080     */
    8181    protected InputStream getInputStream(String urlStr, ProgressMonitor progressMonitor, String reason) throws OsmTransferException {
     82        return getInputStream(urlStr, progressMonitor, reason, null);
     83    }
     84
     85    /**
     86     * Open a connection to the given url and return a reader on the input stream
     87     * from that connection. In case of user cancel, return <code>null</code>.
     88     * Relative URL's are directed to API base URL.
     89     * @param urlStr The url to connect to.
     90     * @param progressMonitor progress monitoring and abort handler
     91     * @param reason The reason to show on console. Can be {@code null} if no reason is given
     92     * @param authentication A {@code CredentialsAgentResponse} for basic authorization,
     93     * {@code OAuthAccessTokenHolder} for OAuth, or {@code null} for defaults.
     94     * @return A reader reading the input stream (servers answer) or <code>null</code>.
     95     * @throws OsmTransferException if data transfer errors occur
     96     */
     97    protected InputStream getInputStream(String urlStr, ProgressMonitor progressMonitor, String reason, Object authentication) throws OsmTransferException {
    8298        try {
    8399            api.initialize(progressMonitor);
    84100            String url = urlStr.startsWith("http") ? urlStr : (getBaseUrl() + urlStr);
    85             return getInputStreamRaw(url, progressMonitor, reason);
     101            return getInputStreamRaw(url, progressMonitor, reason, authentication);
    86102        } finally {
    87103            progressMonitor.invalidate();
    88104        }
     
    122138    }
    123139
    124140    /**
     141     * Open a connection to the given url and return a reader on the input stream
     142     * from that connection. In case of user cancel, return <code>null</code>.
     143     * @param urlStr The exact url to connect to.
     144     * @param progressMonitor progress monitoring and abort handler
     145     * @param reason The reason to show on console. Can be {@code null} if no reason is given
     146     * @return An reader reading the input stream (servers answer) or <code>null</code>.
     147     * @throws OsmTransferException if data transfer errors occur
     148     * @since xxx
     149     */
     150    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason, Object auth) throws OsmTransferException {
     151        return getInputStreamRaw(urlStr, progressMonitor, reason, false, "GET", null, auth);
     152    }
     153
     154    /**
    125155     * Open a connection to the given url (if HTTP, trough a GET request) and return a reader on the input stream
    126156     * from that connection. In case of user cancel, return <code>null</code>.
    127157     * @param urlStr The exact url to connect to.
     
    151181     * @throws OsmTransferException if data transfer errors occur
    152182     * @since 12596
    153183     */
     184    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason,
     185            boolean uncompressAccordingToContentDisposition, String httpMethod, byte[] requestBody) throws OsmTransferException {
     186        return getInputStreamRaw(urlStr, progressMonitor, reason, uncompressAccordingToContentDisposition, httpMethod, requestBody, null);
     187    }
     188
     189    /**
     190     * Open a connection to the given url (if HTTP, with the specified method) and return a reader on the input stream
     191     * from that connection. In case of user cancel, return <code>null</code>.
     192     * @param urlStr The exact url to connect to.
     193     * @param progressMonitor progress monitoring and abort handler
     194     * @param reason The reason to show on console. Can be {@code null} if no reason is given
     195     * @param uncompressAccordingToContentDisposition Whether to inspect the HTTP header {@code Content-Disposition}
     196     *                                                for {@code filename} and uncompress a gzip/bzip2/xz/zip stream.
     197     * @param httpMethod HTTP method ("GET", "POST" or "PUT")
     198     * @param requestBody HTTP request body (for "POST" and "PUT" methods only). Must be null for "GET" method.
     199     * @param auth A {@code CredentialsAgentResponse} for basic authorization,
     200     * {@code OAuthAccessTokenHolder} for OAuth, or {@code null} for defaults.
     201     * @return An reader reading the input stream (servers answer) or {@code null}.
     202     * @throws OsmTransferException if data transfer errors occur
     203     * @since xxx
     204     */
    154205    @SuppressWarnings("resource")
    155206    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason,
    156             boolean uncompressAccordingToContentDisposition, String httpMethod, byte[] requestBody) throws OsmTransferException {
     207            boolean uncompressAccordingToContentDisposition, String httpMethod, byte[] requestBody,
     208            Object auth) throws OsmTransferException {
    157209        try {
    158210            OnlineResource.JOSM_WEBSITE.checkOfflineAccess(urlStr, Config.getUrls().getJOSMWebsite());
    159211            OnlineResource.OSM_API.checkOfflineAccess(urlStr, OsmApi.getOsmApi().getServerUrl());
     
    182234            activeConnection = client;
    183235            adaptRequest(client);
    184236            if (doAuthenticate) {
    185                 addAuth(client);
     237                addAuth(client, auth);
    186238            }
    187239            if (cancel)
    188240                throw new OsmTransferCanceledException("Operation canceled");
     
    415467     */
    416468    public <T> T fetchData(String api, String subtask, DomParser<T> parser, ProgressMonitor monitor, String reason)
    417469            throws OsmTransferException {
     470        return fetchData(api, subtask, parser, monitor, reason, null);
     471    }
     472
     473    /**
     474     * Fetches generic data from the DOM document resulting an API call.
     475     * @param api the OSM API call
     476     * @param subtask the subtask translated message
     477     * @param parser the parser converting the DOM document (OSM API result)
     478     * @param <T> data type
     479     * @param monitor The progress monitor
     480     * @param reason The reason to show on console. Can be {@code null} if no reason is given
     481     * @param authentication A {@code CredentialsAgentResponse} for basic authorization,
     482     * {@code OAuthAccessTokenHolder} for OAuth, or {@code null} for defaults.
     483     * @return The converted data
     484     * @throws OsmTransferException if something goes wrong
     485     * @since 12510
     486     */
     487    public <T> T fetchData(String api, String subtask, DomParser<T> parser, ProgressMonitor monitor, String reason, Object authentication)
     488            throws OsmTransferException {
    418489        try {
    419490            monitor.beginTask("");
    420491            monitor.indeterminateSubTask(subtask);
    421             try (InputStream in = getInputStream(api, monitor.createSubTaskMonitor(1, true), reason)) {
     492            try (InputStream in = getInputStream(api, monitor.createSubTaskMonitor(1, true), reason, authentication)) {
    422493                return parser.parse(XmlUtils.parseSafeDOM(in));
    423494            }
    424495        } catch (OsmTransferException e) {
  • src/org/openstreetmap/josm/io/OsmServerUserInfoReader.java

     
    1313import javax.xml.xpath.XPathFactory;
    1414
    1515import org.openstreetmap.josm.data.coor.LatLon;
     16import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
    1617import org.openstreetmap.josm.data.osm.DataSet;
    1718import org.openstreetmap.josm.data.osm.UserInfo;
    1819import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     20import org.openstreetmap.josm.io.auth.CredentialsAgentResponse;
     21import org.openstreetmap.josm.tools.Logging;
    1922import org.openstreetmap.josm.tools.UncheckedParseException;
    2023import org.openstreetmap.josm.tools.XmlParsingException;
    2124import org.openstreetmap.josm.tools.date.DateUtils;
     
    159162    }
    160163
    161164    /**
     165     * Fetches user info without explicit reason with a specific authentication
     166     * @param monitor The progress monitor
     167     * @param authentication The authentication object ({@code OAuthAccessTokenHolder}
     168     * or {@code CredentialsAgentResponse})
     169     * @return The user info
     170     * @throws OsmTransferException if something goes wrong
     171     * @since xxx
     172     */
     173    public UserInfo fetchUserInfo(ProgressMonitor monitor, Object authentication) throws OsmTransferException {
     174        if (authentication instanceof String) {
     175            return fetchUserInfo(monitor, null, (String) authentication);
     176        } else {
     177            return fetchUserInfo(monitor, authentication, null);
     178        }
     179    }
     180
     181    /**
    162182     * Fetches user info, with an explicit reason.
    163183     * @param monitor The progress monitor
    164184     * @param reason The reason to show on console. Can be {@code null} if no reason is given
     
    167187     * @since 6695
    168188     */
    169189    public UserInfo fetchUserInfo(ProgressMonitor monitor, String reason) throws OsmTransferException {
    170         return fetchData("user/details", tr("Reading user info ..."),
    171                 OsmServerUserInfoReader::buildFromXML, monitor, reason);
     190        return fetchUserInfo(monitor, null, reason);
    172191    }
     192
     193    /**
     194     * Fetches user info, with an explicit reason.
     195     * @param monitor The progress monitor
     196     * @param authentication A {@code CredentialsAgentResponse} for basic authorization,
     197     * {@code OAuthAccessTokenHolder} for OAuth, or {@code null} for defaults.
     198     * @param reason The reason to show on console. Can be {@code null} if no reason is given
     199     * @return The user info
     200     * @throws OsmTransferException if something goes wrong
     201     * @since xxx
     202     */
     203    public UserInfo fetchUserInfo(ProgressMonitor monitor, Object authentication , String reason) throws OsmTransferException {
     204        if (authentication instanceof OAuthAccessTokenHolder || authentication instanceof CredentialsAgentResponse
     205                || authentication == null) {
     206            return fetchData("user/details", tr("Reading user info ..."),
     207                    OsmServerUserInfoReader::buildFromXML, monitor, reason, authentication);
     208        } else {
     209            String msg = tr("We did not get a valid authentication object ({0})", authentication.getClass().getName());
     210            Logging.warn(msg);
     211            throw new OsmTransferException(msg);
     212        }
     213    }
    173214}