source: josm/trunk/src/org/openstreetmap/josm/data/UserIdentityManager.java@ 18900

Last change on this file since 18900 was 18655, checked in by taylor.smock, 17 months ago

See #20768: OpenStreetMap OAuth2 support

This fixes an issue where the user would be prompted to log in after
removing the OAuth token since the message notifier thread was not stopped.

  • Property svn:eol-style set to native
File size: 13.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.text.MessageFormat;
7
8import org.openstreetmap.josm.data.osm.User;
9import org.openstreetmap.josm.data.osm.UserInfo;
10import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
11import org.openstreetmap.josm.io.NetworkManager;
12import org.openstreetmap.josm.io.OnlineResource;
13import org.openstreetmap.josm.io.OsmApi;
14import org.openstreetmap.josm.io.OsmServerUserInfoReader;
15import org.openstreetmap.josm.io.OsmTransferException;
16import org.openstreetmap.josm.io.auth.CredentialsManager;
17import org.openstreetmap.josm.spi.preferences.Config;
18import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
19import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
20import org.openstreetmap.josm.spi.preferences.StringSetting;
21import org.openstreetmap.josm.tools.CheckParameterUtil;
22import org.openstreetmap.josm.tools.JosmRuntimeException;
23import org.openstreetmap.josm.tools.ListenerList;
24import org.openstreetmap.josm.tools.Logging;
25import org.openstreetmap.josm.tools.Utils;
26
27/**
28 * UserIdentityManager is a global object which keeps track of what JOSM knows about
29 * the identity of the current user.
30 *
31 * JOSM can be operated anonymously provided the current user never invokes an operation
32 * on the OSM server which required authentication. In this case JOSM neither knows
33 * the user name of the OSM account of the current user nor its unique id. Perhaps the
34 * user doesn't have one.
35 *
36 * If the current user supplies a user name and a password in the JOSM preferences JOSM
37 * can partially identify the user.
38 *
39 * The current user is fully identified if JOSM knows both the user name and the unique
40 * id of the users OSM account. The latter is retrieved from the OSM server with a
41 * <code>GET /api/0.6/user/details</code> request, submitted with the user name and password
42 * of the current user.
43 *
44 * The global UserIdentityManager listens to {@link PreferenceChangeEvent}s and keeps track
45 * of what the current JOSM instance knows about the current user. Other subsystems can
46 * let the global UserIdentityManager know in case they fully identify the current user, see
47 * {@link #setFullyIdentified}.
48 *
49 * The information kept by the UserIdentityManager can be used to
50 * <ul>
51 * <li>safely query changesets owned by the current user based on its user id, not on its user name</li>
52 * <li>safely search for objects last touched by the current user based on its user id, not on its user name</li>
53 * </ul>
54 * @since 12743 (renamed from {@code org.openstreetmap.josm.gui.JosmUserIdentityManager})
55 * @since 2689 (creation)
56 */
57public final class UserIdentityManager implements PreferenceChangedListener {
58
59 private static UserIdentityManager instance;
60 private final ListenerList<UserIdentityListener> listeners = ListenerList.create();
61
62 /**
63 * Replies the unique instance of the JOSM user identity manager
64 *
65 * @return the unique instance of the JOSM user identity manager
66 */
67 public static synchronized UserIdentityManager getInstance() {
68 if (instance == null) {
69 instance = new UserIdentityManager();
70 if (OsmApi.isUsingOAuthAndOAuthSetUp(OsmApi.getOsmApi()) && !NetworkManager.isOffline(OnlineResource.OSM_API)) {
71 try {
72 instance.initFromOAuth();
73 } catch (JosmRuntimeException | IllegalArgumentException | IllegalStateException e) {
74 Logging.error(e);
75 // Fall back to preferences if OAuth identification fails for any reason
76 instance.initFromPreferences();
77 }
78 } else {
79 instance.initFromPreferences();
80 }
81 Config.getPref().addPreferenceChangeListener(instance);
82 }
83 return instance;
84 }
85
86 private String userName;
87 private UserInfo userInfo;
88 private boolean accessTokenKeyChanged;
89 private boolean accessTokenSecretChanged;
90
91 private UserIdentityManager() {
92 }
93
94 /**
95 * Remembers the fact that the current JOSM user is anonymous.
96 */
97 public void setAnonymous() {
98 userName = null;
99 userInfo = null;
100 fireUserIdentityChanged();
101 }
102
103 /**
104 * Remembers the fact that the current JOSM user is partially identified
105 * by the user name of its OSM account.
106 *
107 * @param userName the user name. Must not be null. Must not be empty (whitespace only).
108 * @throws IllegalArgumentException if userName is null
109 * @throws IllegalArgumentException if userName is empty
110 */
111 public void setPartiallyIdentified(String userName) {
112 CheckParameterUtil.ensureParameterNotNull(userName, "userName");
113 String trimmedUserName = userName.trim();
114 if (trimmedUserName.isEmpty())
115 throw new IllegalArgumentException(
116 MessageFormat.format("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName));
117 this.userName = trimmedUserName;
118 userInfo = null;
119 fireUserIdentityChanged();
120 }
121
122 /**
123 * Remembers the fact that the current JOSM user is fully identified with a
124 * verified pair of user name and user id.
125 *
126 * @param userName the user name. Must not be null. Must not be empty.
127 * @param userInfo additional information about the user, retrieved from the OSM server and including the user id
128 * @throws IllegalArgumentException if userName is null
129 * @throws IllegalArgumentException if userName is empty
130 * @throws IllegalArgumentException if userInfo is null
131 */
132 public void setFullyIdentified(String userName, UserInfo userInfo) {
133 CheckParameterUtil.ensureParameterNotNull(userName, "userName");
134 String trimmedUserName = userName.trim();
135 if (trimmedUserName.isEmpty())
136 throw new IllegalArgumentException(tr("Expected non-empty value for parameter ''{0}'', got ''{1}''", "userName", userName));
137 CheckParameterUtil.ensureParameterNotNull(userInfo, "userInfo");
138 this.userName = trimmedUserName;
139 this.userInfo = userInfo;
140 fireUserIdentityChanged();
141 }
142
143 /**
144 * Replies true if the current JOSM user is anonymous.
145 *
146 * @return {@code true} if the current user is anonymous.
147 */
148 public boolean isAnonymous() {
149 return userName == null && userInfo == null;
150 }
151
152 /**
153 * Replies true if the current JOSM user is partially identified.
154 *
155 * @return true if the current JOSM user is partially identified.
156 */
157 public boolean isPartiallyIdentified() {
158 return userName != null && userInfo == null;
159 }
160
161 /**
162 * Replies true if the current JOSM user is fully identified.
163 *
164 * @return true if the current JOSM user is fully identified.
165 */
166 public boolean isFullyIdentified() {
167 return userName != null && userInfo != null;
168 }
169
170 /**
171 * Replies the user name of the current JOSM user. null, if {@link #isAnonymous()} is true.
172 *
173 * @return the user name of the current JOSM user
174 */
175 public String getUserName() {
176 return userName;
177 }
178
179 /**
180 * Replies the user id of the current JOSM user. 0, if {@link #isAnonymous()} or
181 * {@link #isPartiallyIdentified()} is true.
182 *
183 * @return the user id of the current JOSM user
184 */
185 public int getUserId() {
186 if (userInfo == null) return 0;
187 return userInfo.getId();
188 }
189
190 /**
191 * Replies verified additional information about the current user if the user is
192 * {@link #isFullyIdentified()}.
193 *
194 * @return verified additional information about the current user
195 */
196 public UserInfo getUserInfo() {
197 return userInfo;
198 }
199
200 /**
201 * Returns the identity as a {@link User} object
202 *
203 * @return the identity as user, or {@link User#getAnonymous()} if {@link #isAnonymous()}
204 */
205 public User asUser() {
206 return isAnonymous() ? User.getAnonymous() : User.createOsmUser(userInfo != null ? userInfo.getId() : 0, userName);
207 }
208
209 /**
210 * Initializes the user identity manager from Basic Authentication values in the {@link org.openstreetmap.josm.data.Preferences}
211 * This method should be called if {@code osm-server.auth-method} is set to {@code basic}.
212 * @see #initFromOAuth
213 */
214 public void initFromPreferences() {
215 String credentialsUserName = CredentialsManager.getInstance().getUsername();
216 if (isAnonymous()) {
217 if (!Utils.isBlank(credentialsUserName)) {
218 setPartiallyIdentified(credentialsUserName);
219 }
220 } else {
221 if (credentialsUserName != null && !credentialsUserName.equals(this.userName)) {
222 setPartiallyIdentified(credentialsUserName);
223 }
224 // else: same name in the preferences as JOSM already knows about.
225 // keep the state, be it partially or fully identified
226 }
227 }
228
229 /**
230 * Initializes the user identity manager from OAuth request of user details.
231 * This method should be called if {@code osm-server.auth-method} is set to {@code oauth}.
232 * @see #initFromPreferences
233 * @since 5434
234 */
235 public void initFromOAuth() {
236 try {
237 UserInfo info = new OsmServerUserInfoReader().fetchUserInfo(NullProgressMonitor.INSTANCE);
238 setFullyIdentified(info.getDisplayName(), info);
239 } catch (IllegalArgumentException | OsmTransferException e) {
240 Logging.error(e);
241 }
242 }
243
244 /**
245 * Replies true if the user with name <code>username</code> is the current user
246 *
247 * @param userName the user name
248 * @return true if the user with name <code>username</code> is the current user
249 */
250 public boolean isCurrentUser(String userName) {
251 return this.userName != null && this.userName.equals(userName);
252 }
253
254 /**
255 * Replies true if the current user is {@link #isFullyIdentified() fully identified} and the {@link #getUserId() user ids} match,
256 * or if the current user is not {@link #isFullyIdentified() fully identified} and the {@link #getUserName() user names} match.
257 *
258 * @param user the user to test
259 * @return true if given user is the current user
260 */
261 public boolean isCurrentUser(User user) {
262 if (user == null) {
263 return false;
264 } else if (isFullyIdentified()) {
265 return getUserId() == user.getId();
266 } else {
267 return isCurrentUser(user.getName());
268 }
269 }
270
271 /* ------------------------------------------------------------------- */
272 /* interface PreferenceChangeListener */
273 /* ------------------------------------------------------------------- */
274 @Override
275 public void preferenceChanged(PreferenceChangeEvent evt) {
276 switch (evt.getKey()) {
277 case "osm-server.username":
278 String newUserName = "";
279 if (evt.getNewValue() instanceof StringSetting) {
280 newUserName = ((StringSetting) evt.getNewValue()).getValue();
281 }
282 if (Utils.isBlank(newUserName)) {
283 setAnonymous();
284 } else if (!newUserName.equals(userName)) {
285 setPartiallyIdentified(newUserName);
286 }
287 return;
288 case "osm-server.url":
289 String newUrl = null;
290 if (evt.getNewValue() instanceof StringSetting) {
291 newUrl = ((StringSetting) evt.getNewValue()).getValue();
292 }
293 if (Utils.isBlank(newUrl)) {
294 setAnonymous();
295 } else if (isFullyIdentified()) {
296 setPartiallyIdentified(getUserName());
297 }
298 break;
299 case "oauth.access-token.key":
300 accessTokenKeyChanged = true;
301 break;
302 case "oauth.access-token.secret":
303 accessTokenSecretChanged = true;
304 break;
305 default: // Do nothing
306 if (evt.getKey() != null && evt.getKey().equals("oauth.access-token.parameters.OAuth20." + OsmApi.getOsmApi().getHost())) {
307 accessTokenKeyChanged = true;
308 accessTokenSecretChanged = true;
309 }
310 }
311 // oauth.access-token.parameters.OAuth20.api.openstreetmap.org
312
313 if (accessTokenKeyChanged && accessTokenSecretChanged) {
314 accessTokenKeyChanged = false;
315 accessTokenSecretChanged = false;
316 if (OsmApi.isUsingOAuthAndOAuthSetUp(OsmApi.getOsmApi())) {
317 getInstance().initFromOAuth();
318 } else if (OsmApi.isUsingOAuth()) {
319 setAnonymous();
320 }
321 }
322 }
323
324 /**
325 * This listener is notified whenever the osm user is changed.
326 */
327 @FunctionalInterface
328 public interface UserIdentityListener {
329 /**
330 * The current user was changed.
331 */
332 void userIdentityChanged();
333 }
334
335 /**
336 * Add a listener that listens to changes of the current user.
337 * @param listener The listener
338 */
339 public void addListener(UserIdentityListener listener) {
340 listeners.addListener(listener);
341 }
342
343 private void fireUserIdentityChanged() {
344 listeners.fireEvent(UserIdentityListener::userIdentityChanged);
345 }
346}
Note: See TracBrowser for help on using the repository browser.