From 7a919aa942ecac9ce8a5e73f6b53a258f365ce9d Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 2 Aug 2023 14:20:54 -0400 Subject: [PATCH 01/25] pause commit on this branch (broken) --- Model/pom.xml | 6 ++++ .../gusdb/wdk/model/config/ModelConfig.java | 1 + .../gusdb/wdk/model/config/OAuthConfig.java | 29 ------------------- .../org/gusdb/wdk/session/OAuthClient.java | 2 +- Service/pom.xml | 6 ++++ 5 files changed, 14 insertions(+), 30 deletions(-) delete mode 100644 Model/src/main/java/org/gusdb/wdk/model/config/OAuthConfig.java diff --git a/Model/pom.xml b/Model/pom.xml index af5da321e9..01d9880204 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -90,6 +90,12 @@ fgputil-db + + org.gusdb + oauth2-client + 2.0.0-SNAPSHOT + + com.itextpdf itextpdf diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java index 7d70842d8b..03b7ec2f59 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java @@ -11,6 +11,7 @@ import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.IoUtil; import org.gusdb.fgputil.Named.NamedObject; +import org.gusdb.oauth2.client.OAuthConfig; /** * An object representation of the {@code model-config.xml} file. It holds all the configuration information diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/OAuthConfig.java b/Model/src/main/java/org/gusdb/wdk/model/config/OAuthConfig.java deleted file mode 100644 index e57c757cd4..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/config/OAuthConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.gusdb.wdk.model.config; - -public interface OAuthConfig { - - /** - * @return base URL of OAuth2 server to use for authentication - */ - String getOauthUrl(); - - /** - * @return OAuth2 client ID to use for authentication - */ - String getOauthClientId(); - - /** - * @return OAuth2 client secret to use for authentication - */ - String getOauthClientSecret(); - - /** - * @return key store file containing acceptable SSL hosts/certs - */ - String getKeyStoreFile(); - - /** - * @return pass phrase needed to access key store - */ - String getKeyStorePassPhrase(); -} diff --git a/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java b/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java index dd56f0f0c7..2fbe68f10b 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java +++ b/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java @@ -21,8 +21,8 @@ import org.glassfish.jersey.client.ClientConfig; import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.IoUtil; +import org.gusdb.oauth2.client.OAuthConfig; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.config.OAuthConfig; import org.json.JSONException; import org.json.JSONObject; diff --git a/Service/pom.xml b/Service/pom.xml index 4b89378b8f..132d102562 100644 --- a/Service/pom.xml +++ b/Service/pom.xml @@ -51,6 +51,12 @@ fgputil-client + + org.gusdb + oauth2-client + 2.0.0-SNAPSHOT + + org.glassfish.jersey.core jersey-server From a9f222ef84e0d70109b00945b2e3ed920dd36404 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 6 Sep 2023 11:32:48 -0400 Subject: [PATCH 02/25] Checkpoint commit; not working --- Model/pom.xml | 11 -- .../gusdb/wdk/model/config/ModelConfig.java | 3 +- .../org/gusdb/wdk/session/OAuthClient.java | 134 -------------- .../java/org/gusdb/wdk/session/OAuthUtil.java | 34 ---- .../wdk/session/WdkOAuthClientWrapper.java | 98 ++++++++++ .../gusdb/wdk/session/WdkTrustManager.java | 172 ------------------ .../wdk/service/WdkServiceApplication.java | 2 - .../wdk/service/service/OAuthService.java | 39 ---- .../wdk/service/service/QuestionService.java | 1 + .../wdk/service/service/SessionService.java | 71 ++++++-- 10 files changed, 160 insertions(+), 405 deletions(-) delete mode 100644 Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/session/OAuthUtil.java create mode 100644 Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/session/WdkTrustManager.java delete mode 100644 Service/src/main/java/org/gusdb/wdk/service/service/OAuthService.java diff --git a/Model/pom.xml b/Model/pom.xml index 01d9880204..69ecb362df 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -121,17 +121,6 @@ jersey-container-grizzly2-http - - io.jsonwebtoken - jjwt - - - com.fasterxml.jackson.core - jackson-databind - - - - javax.mail mail diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java index 03b7ec2f59..6409c37e1a 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java @@ -11,6 +11,7 @@ import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.IoUtil; import org.gusdb.fgputil.Named.NamedObject; +import org.gusdb.oauth2.client.KeyStoreTrustManager.KeyStoreConfig; import org.gusdb.oauth2.client.OAuthConfig; /** @@ -19,7 +20,7 @@ * * @author Jerric */ -public class ModelConfig implements OAuthConfig { +public class ModelConfig implements OAuthConfig, KeyStoreConfig { private static final Logger LOG = Logger.getLogger(ModelConfig.class); diff --git a/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java b/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java deleted file mode 100644 index 2fbe68f10b..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java +++ /dev/null @@ -1,134 +0,0 @@ -package org.gusdb.wdk.session; - -import static org.gusdb.fgputil.FormatUtil.NL; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.nio.file.Paths; -import java.util.List; -import java.util.Map.Entry; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedHashMap; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; - -import org.apache.log4j.Logger; -import org.glassfish.jersey.client.ClientConfig; -import org.gusdb.fgputil.FormatUtil; -import org.gusdb.fgputil.IoUtil; -import org.gusdb.oauth2.client.OAuthConfig; -import org.gusdb.wdk.model.WdkModelException; -import org.json.JSONException; -import org.json.JSONObject; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.impl.TextCodec; - -public class OAuthClient { - - private static final Logger LOG = Logger.getLogger(OAuthClient.class); - - private final String _oauthServerBase; - private final String _clientId; - private final String _clientSecret; - private final TrustManager _trustManager; - - public OAuthClient(OAuthConfig config) throws WdkModelException { - _oauthServerBase = config.getOauthUrl(); - _clientId = config.getOauthClientId(); - _clientSecret = config.getOauthClientSecret(); - _trustManager = getTrustManager(config); - } - - private static TrustManager getTrustManager(OAuthConfig config) throws WdkModelException { - String keyStoreFile = config.getKeyStoreFile(); - return (keyStoreFile.isEmpty() ? new WdkTrustManager() : - new WdkTrustManager(Paths.get(keyStoreFile), config.getKeyStorePassPhrase())); - } - - public long getUserIdFromAuthCode(String authCode, String redirectUri) throws WdkModelException { - - try { - String oauthUrl = _oauthServerBase + "/token"; - - // build form parameters for token request - MultivaluedMap formData = new MultivaluedHashMap<>(); - formData.add("grant_type", "authorization_code"); - formData.add("code", authCode); - formData.add("redirect_uri", redirectUri); - formData.add("client_id", _clientId); - formData.add("client_secret", _clientSecret); - - SSLContext sslContext = SSLContext.getInstance("SSL"); - sslContext.init(null, new TrustManager[]{ _trustManager }, null); - - LOG.info("Building token request with the following URL: " + oauthUrl + - " and params: " + dumpMultiMap(formData)); - - // build request and get token response - Response response = ClientBuilder.newBuilder() - .withConfig(new ClientConfig()) - .sslContext(sslContext) - .build() - .target(oauthUrl) - .request(MediaType.APPLICATION_JSON) - .post(Entity.form(formData)); - - if (response.getStatus() == 200) { - // Success! Read result into buffer and convert to JSON - InputStream resultStream = (InputStream)response.getEntity(); - ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - IoUtil.transferStream(buffer, resultStream); - JSONObject json = new JSONObject(new String(buffer.toByteArray())); - LOG.debug("Response received from OAuth server for token request: " + json.toString(2)); - // get id_token from object and decode to user ID - String idToken = json.getString("id_token"); - return getUserIdFromIdToken(idToken, _clientSecret); - } - else { - // Failure; throw exception - throw new WdkModelException("OAuth2 token request failed with status " + - response.getStatus() + ": " + response.getStatusInfo().getReasonPhrase() + NL + response.getEntity()); - } - } - catch(WdkModelException e) { - throw e; - } - catch(Exception e) { - throw new WdkModelException("Unable to complete OAuth token request to fetch user id", e); - } - } - - private static long getUserIdFromIdToken(String idToken, String clientSecret) throws WdkModelException { - try { - LOG.debug("Attempting parse of id token [" + idToken + "] using client secret '" + clientSecret +"'"); - String encodedKey = TextCodec.BASE64.encode(clientSecret); - Claims claims = Jwts.parser().setSigningKey(encodedKey).parseClaimsJws(idToken).getBody(); - // TODO: verify additional claims for security - String userIdStr = claims.getSubject(); - LOG.debug("Received token for sub '" + userIdStr + "' and preferred_username '" + claims.get("preferred_username")); - if (FormatUtil.isInteger(userIdStr)) { - return Long.valueOf(userIdStr); - } - throw new WdkModelException("Subject returned by OAuth server [" + userIdStr + "] is not a valid user ID."); - } - catch (JSONException e) { - throw new WdkModelException("JWT body returned is not a valid JSON object."); - } - } - - private static String dumpMultiMap(MultivaluedMap formData) { - StringBuilder str = new StringBuilder("{").append(NL); - for (Entry> entry : formData.entrySet()) { - str.append(" ").append(entry.getKey()).append(": ") - .append(FormatUtil.arrayToString(entry.getValue().toArray())).append(NL); - } - return str.append("}").append(NL).toString(); - } -} diff --git a/Model/src/main/java/org/gusdb/wdk/session/OAuthUtil.java b/Model/src/main/java/org/gusdb/wdk/session/OAuthUtil.java deleted file mode 100644 index 7036e3d770..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/session/OAuthUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.gusdb.wdk.session; - -import java.util.Date; -import java.util.UUID; - -import org.gusdb.fgputil.EncryptionUtil; -import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.WdkModelException; - -public class OAuthUtil { - - public static final String STATE_TOKEN_KEY = "OAUTH_STATE_TOKEN"; - - /** - * Generates a new state token based on the current time, a random UUID, and - * WDK model's current secret key value. This value should be added to the - * session when a user is about to attempt a login; it will then be checked - * against the state token accompanying the authentication token returned by - * the OAuth server after the user has been authenticated. This is to prevent - * cross-site request forgery attacks. - * - * @param wdkModel WDK Model (used to fetch secret key) - * @return generated state token - * @throws WdkModelException if unable to access secret key - */ - public static String generateStateToken(WdkModel wdkModel) throws WdkModelException { - String saltedString = - UUID.randomUUID() + ":::" + - String.valueOf(new Date().getTime()) + ":::" + - wdkModel.getModelConfig().getSecretKey(); - return EncryptionUtil.encrypt(saltedString); - } - -} diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java new file mode 100644 index 0000000000..f7f75708ba --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -0,0 +1,98 @@ +package org.gusdb.wdk.session; + +import static org.gusdb.fgputil.FormatUtil.NL; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map.Entry; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.apache.log4j.Logger; +import org.glassfish.jersey.client.ClientConfig; +import org.gusdb.fgputil.FormatUtil; +import org.gusdb.fgputil.IoUtil; +import org.gusdb.oauth2.client.OAuthConfig; +import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; +import org.gusdb.oauth2.client.KeyStoreTrustManager; +import org.gusdb.oauth2.client.OAuthClient; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.config.ModelConfig; +import org.gusdb.wdk.model.user.RegisteredUser; +import org.gusdb.wdk.model.user.User; +import org.gusdb.wdk.model.user.UserFactory; +import org.json.JSONException; +import org.json.JSONObject; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.impl.TextCodec; + +public class WdkOAuthClientWrapper { + + private static final Logger LOG = Logger.getLogger(WdkOAuthClientWrapper.class); + + private final WdkModel _wdkModel; + private final OAuthConfig _config; + private final OAuthClient _client; + + public WdkOAuthClientWrapper(WdkModel wdkModel) throws WdkModelException { + try { + _wdkModel = wdkModel; + _config = wdkModel.getModelConfig(); + _client = new OAuthClient(OAuthClient.getTrustManager(wdkModel.getModelConfig())); + } + catch (Exception e) { + throw new WdkModelException("Unable to instantiate OAuthClient from config", e); + } + } + + public ValidatedToken getValidatedAuthToken(String authCode, String redirectUri) { + return _client.getValidatedAuthToken(_config, authCode, redirectUri); + } + + public User getUserFromValidatedToken(ValidatedToken token, UserFactory userFactory) { + Claims claims = token.getTokenContents(); + User user = new RegisteredUser(_wdkModel, + Long.parseLong(claims.getSubject()), + claims.get("email", String.class), + t, String signature, String stableId) + //long userId = client.getUserIdFromAuthCode(authCode, appUrl); + //User newUser = wdkModel.getUserFactory().login(userId); + if (newUser == null) { + throw new WdkModelException("Unable to find user with ID " + userId + + ", returned by OAuth service with auth code " + authCode); + } + } + + /* Leaving here temporarily as a reference + private static long getUserIdFromIdToken(String idToken, String clientSecret) throws WdkModelException { + try { + LOG.debug("Attempting parse of id token [" + idToken + "] using client secret '" + clientSecret +"'"); + String encodedKey = TextCodec.BASE64.encode(clientSecret); + Claims claims = Jwts.parser().setSigningKey(encodedKey).parseClaimsJws(idToken).getBody(); + // TODO: verify additional claims for security + String userIdStr = claims.getSubject(); + LOG.debug("Received token for sub '" + userIdStr + "' and preferred_username '" + claims.get("preferred_username")); + if (FormatUtil.isInteger(userIdStr)) { + return Long.valueOf(userIdStr); + } + throw new WdkModelException("Subject returned by OAuth server [" + userIdStr + "] is not a valid user ID."); + } + catch (JSONException e) { + throw new WdkModelException("JWT body returned is not a valid JSON object."); + } + } + */ + +} diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkTrustManager.java b/Model/src/main/java/org/gusdb/wdk/session/WdkTrustManager.java deleted file mode 100644 index e0c2e79f2b..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkTrustManager.java +++ /dev/null @@ -1,172 +0,0 @@ -package org.gusdb.wdk.session; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.Socket; -import java.nio.file.Path; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; - -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509ExtendedTrustManager; - -import org.apache.log4j.Logger; -import org.gusdb.wdk.model.WdkModelException; - -/** - * Implementation of SSL trust manager that uses a Java key store for the basis - * of certification checks. Failures are simply logged. An alternate - * constructor creates a "dummy" trust manager that allows anything (i.e. no - * security at all). This can be used when the keystore file is unknown, - * unavailable, or unconfigured. - * - * This code was adapted from a sample and explanation here: - * https://docs.oracle.com/javase/7/docs/technotes/guides/security/jsse/JSSERefGuide.html - * - * To see the certs (probably) loaded by this class by EuPathDB, run: - * > keytool -list -v -keystore /etc/pki/java/cacerts - * - * @author rdoherty - */ -public class WdkTrustManager extends X509ExtendedTrustManager { - - private static final Logger LOG = Logger.getLogger(WdkTrustManager.class); - - /** - * The default PKIX X509ExtendedTrustManager. We'll delegate decisions to it, - * and fall back to the logic in this class if the default - * X509ExtendedTrustManager doesn't trust it. - */ - private final X509ExtendedTrustManager _pkixTrustManager; - - public WdkTrustManager() { - // do nothing; this constructor creates a trust manager that allows anything - _pkixTrustManager = null; - } - - public WdkTrustManager(Path keyStoreFile, String passPhrase) throws WdkModelException { - try (InputStream fileStream = new FileInputStream(keyStoreFile.toAbsolutePath().toString())) { - // create a "default" JSSE X509ExtendedTrustManager - KeyStore ks = KeyStore.getInstance("JKS"); - ks.load(fileStream, (passPhrase == null || passPhrase.isEmpty() ? null : passPhrase.toCharArray())); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); - tmf.init(ks); - - TrustManager[] tms = tmf.getTrustManagers(); - - /* - * Iterate over the returned trust managers, look for an instance of X509TrustManager. If found, use that - * as our "default" trust manager. - */ - for (int i = 0; i < tms.length; i++) { - if (tms[i] instanceof X509ExtendedTrustManager) { - _pkixTrustManager = (X509ExtendedTrustManager) tms[i]; - return; - } - } - - // Can't find a valid trust manager in the factory - throw new WdkModelException("Couldn't initialize trust manager using key store file " + keyStoreFile); - } - catch (IOException | NoSuchAlgorithmException | CertificateException | KeyStoreException e) { - throw new WdkModelException("Couldn't initialize trust manager using key store file " + keyStoreFile, e); - } - } - - /* - * Delegate to the default trust manager. - */ - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - try { - if (_pkixTrustManager != null) _pkixTrustManager.checkClientTrusted(chain, authType); - } - catch (CertificateException e) { - LOG.error("SSL validation check failed.", e); - throw e; - } - } - - /* - * Delegate to the default trust manager. - */ - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - try { - if (_pkixTrustManager != null) _pkixTrustManager.checkServerTrusted(chain, authType); - } - catch (CertificateException e) { - // Possibly pop up a dialog box asking whether to trust the cert chain? - LOG.error("SSL validation check failed.", e); - throw e; - } - } - - /* - * Connection-sensitive verification. - */ - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) - throws CertificateException { - try { - if (_pkixTrustManager != null) _pkixTrustManager.checkClientTrusted(chain, authType, socket); - } - catch (CertificateException e) { - LOG.error("SSL validation check failed.", e); - throw e; - } - } - - @Override - public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) - throws CertificateException { - try { - if (_pkixTrustManager != null) _pkixTrustManager.checkClientTrusted(chain, authType, engine); - } - catch (CertificateException e) { - LOG.error("SSL validation check failed.", e); - throw e; - } - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) - throws CertificateException { - try { - if (_pkixTrustManager != null) _pkixTrustManager.checkServerTrusted(chain, authType, socket); - } - catch (CertificateException e) { - LOG.error("SSL validation check failed.", e); - throw e; - } - } - - @Override - public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) - throws CertificateException { - try { - if (_pkixTrustManager != null) _pkixTrustManager.checkServerTrusted(chain, authType, engine); - } - catch (CertificateException e) { - LOG.error("SSL validation check failed.", e); - throw e; - } - } - - /* - * Merely pass this through. - */ - @Override - public X509Certificate[] getAcceptedIssuers() { - return _pkixTrustManager == null ? - new X509Certificate[]{} : - _pkixTrustManager.getAcceptedIssuers(); - } -} diff --git a/Service/src/main/java/org/gusdb/wdk/service/WdkServiceApplication.java b/Service/src/main/java/org/gusdb/wdk/service/WdkServiceApplication.java index e070526544..891ae04041 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/WdkServiceApplication.java +++ b/Service/src/main/java/org/gusdb/wdk/service/WdkServiceApplication.java @@ -20,7 +20,6 @@ import org.gusdb.wdk.service.provider.JsonSchemaProvider; import org.gusdb.wdk.service.service.AnswerService; import org.gusdb.wdk.service.service.ClientErrorReportingService; -import org.gusdb.wdk.service.service.OAuthService; import org.gusdb.wdk.service.service.OntologyService; import org.gusdb.wdk.service.service.ProjectService; import org.gusdb.wdk.service.service.PublicStrategyService; @@ -82,7 +81,6 @@ public Set> getClasses() { // add service classes .add(ProjectService.class) .add(SystemService.class) - .add(OAuthService.class) .add(ProfileService.class) .add(PreferenceService.class) .add(RecordService.class) diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/OAuthService.java b/Service/src/main/java/org/gusdb/wdk/service/service/OAuthService.java deleted file mode 100644 index f6bcae1a94..0000000000 --- a/Service/src/main/java/org/gusdb/wdk/service/service/OAuthService.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.gusdb.wdk.service.service; - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import org.gusdb.wdk.core.api.JsonKeys; -import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.session.OAuthUtil; -import org.json.JSONObject; - -@Path("/oauth") -public class OAuthService extends AbstractWdkService { - - // ===== OAuth 2.0 + OpenID Connect Support ===== - /** - * Create anti-forgery state token, add to session, and return. This is - * requested by the client when the user tries to log in using an OAuth2 - * server. The value generated will be used to check the state token passed - * to the oauth processing action. They must match for the login to succeed. - * We generate a new value each time; all but one of "overlapping" login - * attempts by the same user will thus fail. - * - * @return OAuth 2.0 state token - * @throws WdkModelException if unable to read WDK secret key from file - */ - @GET - @Path("state-token") - @Produces(MediaType.APPLICATION_JSON) - public Response getOauthStateToken() throws WdkModelException { - String newToken = OAuthUtil.generateStateToken(getWdkModel()); - getSession().setAttribute(OAuthUtil.STATE_TOKEN_KEY, newToken); - JSONObject json = new JSONObject(); - json.put(JsonKeys.OAUTH_STATE_TOKEN, newToken); - return Response.ok(json.toString()).build(); - } -} diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java index ac5eefe21c..4daf4acee7 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java @@ -263,6 +263,7 @@ public JSONArray getQuestionChange(@PathParam(SEARCH_PATH_PARAM) String question ValidationLevel.DISPLAYABLE, FillStrategy.FILL_PARAM_IF_MISSING_OR_INVALID) .getDisplayablyValid() + // throwing model exception here because we should always be able to generate a set of defaults using FILL_PARAM_IF_MISSING_OR_INVALID .getOrThrow(spec -> new WdkModelException("Unable to produce a valid spec from incoming param values")); // see if changed param value changed during build; if so, then it was invalid diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 2368504baa..87688882a2 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -2,8 +2,10 @@ import java.net.URI; import java.net.URISyntaxException; +import java.util.Date; import java.util.HashSet; import java.util.Set; +import java.util.UUID; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -17,12 +19,14 @@ import javax.ws.rs.core.Response.ResponseBuilder; import org.apache.log4j.Logger; +import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.fgputil.web.LoginCookieFactory.LoginCookieParts; import org.gusdb.fgputil.web.SessionProxy; +import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.events.NewUserEvent; import org.gusdb.wdk.model.Utilities; @@ -36,8 +40,7 @@ import org.gusdb.wdk.service.request.LoginRequest; import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.gusdb.wdk.service.statustype.MethodNotAllowedStatusType; -import org.gusdb.wdk.session.OAuthClient; -import org.gusdb.wdk.session.OAuthUtil; +import org.gusdb.wdk.session.WdkOAuthClientWrapper; import org.json.JSONException; import org.json.JSONObject; @@ -67,6 +70,52 @@ public class SessionService extends AbstractWdkService { // redirect URLs private static final String OAUTH_ERROR_URL = "/app/user/message/login-error?requestUrl="; + // state token session property + public static final String STATE_TOKEN_KEY = "OAUTH_STATE_TOKEN"; + + // ===== OAuth 2.0 + OpenID Connect Support ===== + /** + * Create anti-forgery state token, add to session, and return. This is + * requested by the client when the user tries to log in using an OAuth2 + * server. The value generated will be used to check the state token passed + * to the oauth processing action. They must match for the login to succeed. + * We generate a new value each time; all but one of "overlapping" login + * attempts by the same user will thus fail. + * + * @return OAuth 2.0 state token + * @throws WdkModelException if unable to read WDK secret key from file + */ + @GET + @Path("oauth/state-token") + @Produces(MediaType.APPLICATION_JSON) + public Response getOauthStateToken() throws WdkModelException { + String newToken = generateStateToken(getWdkModel()); + getSession().setAttribute(STATE_TOKEN_KEY, newToken); + JSONObject json = new JSONObject(); + json.put(JsonKeys.OAUTH_STATE_TOKEN, newToken); + return Response.ok(json.toString()).build(); + } + + /** + * Generates a new state token based on the current time, a random UUID, and + * WDK model's current secret key value. This value should be added to the + * session when a user is about to attempt a login; it will then be checked + * against the state token accompanying the authentication token returned by + * the OAuth server after the user has been authenticated. This is to prevent + * cross-site request forgery attacks. + * + * @param wdkModel WDK Model (used to fetch secret key) + * @return generated state token + * @throws WdkModelException if unable to access secret key + */ + private static String generateStateToken(WdkModel wdkModel) throws WdkModelException { + String saltedString = + UUID.randomUUID() + ":::" + + String.valueOf(new Date().getTime()) + ":::" + + wdkModel.getModelConfig().getSecretKey(); + return EncryptionUtil.encrypt(saltedString); + } + @GET @Path("login") public Response processOauthLogin( @@ -75,8 +124,10 @@ public Response processOauthLogin( @QueryParam(OAUTH_CODE_KEY) String authCode, @QueryParam(OAUTH_REDIRECT_URL_KEY) String originalUrl) throws WdkModelException { + WdkModel wdkModel = getWdkModel(); ModelConfig modelConfig = wdkModel.getModelConfig(); + if (!AuthenticationMethod.OAUTH2.equals(modelConfig.getAuthenticationMethodEnum())) { return Response.status(new MethodNotAllowedStatusType()).build(); } @@ -99,23 +150,19 @@ public Response processOauthLogin( throw new WdkModelException(errorMessage); } - String storedStateToken = (String) getSession().getAttribute(OAuthUtil.STATE_TOKEN_KEY); - getSession().removeAttribute(OAuthUtil.STATE_TOKEN_KEY); + String storedStateToken = (String) getSession().getAttribute(STATE_TOKEN_KEY); + getSession().removeAttribute(STATE_TOKEN_KEY); // Is the state token present and does it match the session state token? if (stateToken == null || !stateToken.equals(storedStateToken)) { throw new WdkModelException("Unable to log in; state token missing, incorrect, or expired."); } - OAuthClient client = new OAuthClient(modelConfig); + WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); - // Is there a matching user id for the auth code provided? - long userId = client.getUserIdFromAuthCode(authCode, getContextUri()); - User newUser = wdkModel.getUserFactory().login(userId); - if (newUser == null) { - throw new WdkModelException("Unable to find user with ID " + userId + - ", returned by OAuth service with auth code " + authCode); - } + // Use auth code to get the bearer token, then convert to User + ValidatedToken bearerToken = client.getValidatedIdToken(authCode, appUrl); + User newUser = client.getUserFromBearerToken(bearerToken, wdkModel.getUserFactory()); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); From e49e138fecd0b1ed46e19e60287ddab591579a94 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 11 Oct 2023 23:33:24 -0400 Subject: [PATCH 03/25] checkpoint commit --- .../wdk/model/query/param/FlatVocabParam.java | 10 +- .../gusdb/wdk/model/user/RegisteredUser.java | 2 +- .../java/org/gusdb/wdk/model/user/User.java | 127 +----------------- .../wdk/session/WdkOAuthClientWrapper.java | 19 ++- .../wdk/service/service/SessionService.java | 41 ++++-- 5 files changed, 60 insertions(+), 139 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/model/query/param/FlatVocabParam.java b/Model/src/main/java/org/gusdb/wdk/model/query/param/FlatVocabParam.java index 42ccde0263..e9752a5ebd 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/query/param/FlatVocabParam.java +++ b/Model/src/main/java/org/gusdb/wdk/model/query/param/FlatVocabParam.java @@ -127,7 +127,7 @@ public EnumParamVocabInstance getVocabInstance(User user, Map de try { FlatVocabularyFetcher fetcher = new FlatVocabularyFetcher(user, this); return (vocabQuery.isCacheable() ? - CacheMgr.get().getVocabCache().getValue(fetcher.getCacheKey(dependedParamValues), fetcher) : + getCachedVocabInstance(fetcher, dependedParamValues) : fetcher.fetchItem(dependedParamValues)); } catch (ValueProductionException e) { @@ -135,6 +135,14 @@ public EnumParamVocabInstance getVocabInstance(User user, Map de } } + private EnumParamVocabInstance getCachedVocabInstance( + FlatVocabularyFetcher fetcher, + Map dependedParamValues) throws ValueProductionException { + String cacheKey = fetcher.getCacheKey(dependedParamValues); + EnumParamVocabInstance vocab = CacheMgr.get().getVocabCache().getValue(cacheKey, fetcher); + return vocab; + } + @Override public Param clone() { return new FlatVocabParam(this); diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java index f0f3b606b2..4c175e00fd 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java @@ -4,7 +4,7 @@ public class RegisteredUser extends User { - RegisteredUser(WdkModel wdkModel, long userId, String email, String signature, String stableId) { + public RegisteredUser(WdkModel wdkModel, long userId, String email, String signature, String stableId) { super(wdkModel, userId, email, signature, stableId); } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/User.java b/Model/src/main/java/org/gusdb/wdk/model/user/User.java index a0f89c326f..168fab7ce6 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/User.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/User.java @@ -1,17 +1,9 @@ package org.gusdb.wdk.model.user; -import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import org.apache.log4j.Logger; -import org.gusdb.fgputil.functional.FunctionalInterfaces.FunctionWithException; import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.record.RecordClass; /** * Represents a WDK user. @@ -23,8 +15,6 @@ */ public abstract class User { - private static final Logger LOG = Logger.getLogger(User.class); - protected WdkModel _wdkModel; protected long _userId; @@ -35,13 +25,8 @@ public abstract class User { // Holds key/value pairs associated with the user's profile (come from account db) protected Map _properties = new HashMap<>(); - // Holds key/value pairs associated with the user for this project (come from user db) - protected UserPreferences _preferences; - /** - * Temporarily provide display name value until Struts Actions are purged. - * After that, client will determine what to display - * TODO: remove once struts is purged + * Provides a "pretty" display name for this user * * @return display name for this user */ @@ -60,7 +45,6 @@ protected User(WdkModel wdkModel, long userId, String email, String signature, S _email = email; _signature = signature; _stableId = stableId; - _preferences = new UserPreferences(); } public long getUserId() { @@ -111,14 +95,6 @@ public void clearProfileProperties() { _properties.clear(); } - public void setPreferences(UserPreferences preferences) { - _preferences = preferences; - } - - public UserPreferences getPreferences() { - return _preferences; - } - public WdkModel getWdkModel() { return _wdkModel; } @@ -147,105 +123,4 @@ public boolean equals(Object obj) { ); } -/** - * This is a deprecated "trait" interface containing default methods previously - * supplied by UserBean. It is necessary since we now expose the User class - * to JSPs but its getters are limited in scope (callers in Java land should be - * calling the appropriate factory methods to get a user's data e.g. favorites). - * - * Thus this is (hopefully) a temporary interface and can be removed when the - * strategy-loading branch is merged back and we say good-bye to JSPs/beans. - */ - - public String getFirstName() { - return getProfileProperties().get("firstName"); - } - - public String getMiddleName() { - return getProfileProperties().get("middleName"); - } - - public String getLastName() { - return getProfileProperties().get("lastName"); - } - - public String getOrganization() { - return getProfileProperties().get("organization"); - } - - public Map getGlobalPreferences() { - return getPreferences().getGlobalPreferences(); - } - - public Map getProjectPreferences() { - return getPreferences().getProjectPreferences(); - } - - public int getStrategyCount() throws WdkModelException { - return getWdkModel().getStepFactory().getStrategyCount(getUserId()); - } - - public int getPublicCount() throws WdkModelException { - int count = getWdkModel().getStepFactory().getPublicStrategyCount(); - LOG.debug("Found number of public strats: " + count); - return count; - } - - public void logFoundStrategies(Map> strategies, String condition) { - if (LOG.isDebugEnabled()) { - LOG.debug("Loaded map of " + strategies.size() + " " + condition + " strategy categories:"); - int total = 0; - for (Entry> entry : strategies.entrySet()) { - LOG.debug(" " + entry.getKey() + ": " + entry.getValue().size() + " strategies."); - total += entry.getValue().size(); - } - LOG.debug(" Total: " + total); - } - } - - public Map getBasketCounts() throws WdkModelException { - Map counts = getWdkModel().getBasketFactory().getBasketCounts(this); - Map beans = new LinkedHashMap<>(); - for (RecordClass recordClass : counts.keySet()) { - int count = counts.get(recordClass); - beans.put(recordClass, count); - } - return beans; - } - - public int getBasketCount() throws WdkModelException { - Map baskets = getWdkModel().getBasketFactory().getBasketCounts(this); - int total = 0; - for (int count : baskets.values()) { - total += count; - } - return total; - } - - static Map exposeAsMap(FunctionWithException getter) { - return new HashMap() { - @Override - public T get(Object objectName) { - try { - return getter.apply((String)objectName); - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - }; - } - - static Map> convertMap(Map> strategies) { - Map> category = new LinkedHashMap<>(); - for (String type : strategies.keySet()) { - List list = strategies.get(type); - List beans = new ArrayList<>(); - for (Strategy strategy : list) { - beans.add(strategy); - } - category.put(type, beans); - } - return category; - } } diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index f7f75708ba..46e03a9a97 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -22,6 +22,7 @@ import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.IoUtil; import org.gusdb.oauth2.client.OAuthConfig; +import org.gusdb.oauth2.shared.token.IdTokenFields; import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; import org.gusdb.oauth2.client.KeyStoreTrustManager; import org.gusdb.oauth2.client.OAuthClient; @@ -57,16 +58,30 @@ public WdkOAuthClientWrapper(WdkModel wdkModel) throws WdkModelException { } } - public ValidatedToken getValidatedAuthToken(String authCode, String redirectUri) { + public ValidatedToken getValidatedIdTokenFromAuth(String authCode, String redirectUri) { return _client.getValidatedAuthToken(_config, authCode, redirectUri); } + public ValidatedToken getValidatedIdTokenFromCredentials(String email, String password, String redirectUrl) { + return _client.getValidatedAuthToken(_config, email, password, redirectUrl); + } + + public ValidatedToken getValidatedBearerTokenFromAuth(String authCode, String redirectUri) { + return _client.getValidatedBearerToken(_config, authCode, redirectUri); + } + + public ValidatedToken getValidatedBearerTokenFromCredentials(String email, String password, String redirectUrl) { + return _client.getValidatedBearerToken(_config, email, password, redirectUrl); + } + public User getUserFromValidatedToken(ValidatedToken token, UserFactory userFactory) { Claims claims = token.getTokenContents(); User user = new RegisteredUser(_wdkModel, Long.parseLong(claims.getSubject()), claims.get("email", String.class), - t, String signature, String stableId) + "deprecated", // user signature + claims.get(IdTokenFields.preferred_username.name(), String.class)); + user.set //long userId = client.getUserIdFromAuthCode(authCode, appUrl); //User newUser = wdkModel.getUserFactory().login(userId); if (newUser == null) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 87688882a2..6b1ee1f50d 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -49,6 +49,8 @@ public class SessionService extends AbstractWdkService { private static final Logger LOG = Logger.getLogger(SessionService.class); + private static final int EXPIRATION_3_YEARS_SECS = 3 * 365 * 24 * 60 * 60; + private static final String REFERRER_HEADER_KEY = "Referer"; // OAuth2 parameter keys @@ -150,6 +152,7 @@ public Response processOauthLogin( throw new WdkModelException(errorMessage); } + // get state token off session and remove; only needed for this request String storedStateToken = (String) getSession().getAttribute(STATE_TOKEN_KEY); getSession().removeAttribute(STATE_TOKEN_KEY); @@ -161,14 +164,14 @@ public Response processOauthLogin( WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); // Use auth code to get the bearer token, then convert to User - ValidatedToken bearerToken = client.getValidatedIdToken(authCode, appUrl); - User newUser = client.getUserFromBearerToken(bearerToken, wdkModel.getUserFactory()); + ValidatedToken bearerToken = client.getValidatedTokenFromAuth(authCode, appUrl); + User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); // login successful; create redirect response - return getSuccessResponse(newUser, oldUser, redirectUrl, true); + return getSuccessResponse(bearerToken, newUser, oldUser, redirectUrl, true); } catch (Exception ex) { LOG.error("Unsuccessful login attempt: " + ex.getMessage(), ex); @@ -203,13 +206,17 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer return createRedirectResponse(redirectUrl).build(); } - // log in the user - User newUser = wdkModel.getUserFactory().login(oldUser, request.getEmail(), request.getPassword()); + WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); + + // Use passed credentials to get the bearer token, then convert to User + ValidatedToken bearerToken = client.getValidatedTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); + + User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); - return getSuccessResponse(newUser, oldUser, redirectUrl, false); + return getSuccessResponse(bearerToken, newUser, oldUser, redirectUrl, false); } catch (JSONException e) { @@ -230,7 +237,9 @@ protected void transferOwnership(User oldUser, User newUser, WdkModel wdkModel) /** * Sets the passed user on the session and packages a successful login response with a login cookie + * @param bearerToken * + * @param bearerToken bearer token for the new user * @param newUser newly logged in user * @param oldUser user previously on session, if any * @param redirectUrl incoming original page @@ -238,16 +247,30 @@ protected void transferOwnership(User oldUser, User newUser, WdkModel wdkModel) * @return success response * @throws WdkModelException */ - private Response getSuccessResponse(User newUser, User oldUser, + private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, User oldUser, String redirectUrl, boolean isRedirectResponse) throws WdkModelException { SessionProxy session = getSession(); + + // synchronize on the underlying session object (SessionProxy is request-local) synchronized(session.getUnderlyingSession()) { + session.setAttribute(Utilities.WDK_USER_KEY, newUser); + Events.triggerAndWait(new NewUserEvent(newUser, oldUser, session), new WdkRuntimeException("Unable to complete WDK user assignement.")); - LoginCookieFactory baker = new LoginCookieFactory(getWdkModel().getModelConfig().getSecretKey()); - CookieBuilder loginCookie = baker.createLoginCookie(newUser.getEmail()); + + // TODO: leaving legacy code here for reference; remove when deemed appropriate + //LoginCookieFactory baker = new LoginCookieFactory(getWdkModel().getModelConfig().getSecretKey()); + //CookieBuilder loginCookie = baker.createLoginCookie(newUser.getEmail()); + + // 3-year expiration (should change secret key before then) + CookieBuilder loginCookie = new CookieBuilder( + LoginCookieFactory.WDK_LOGIN_COOKIE_NAME, + bearerToken.getTokenValue()); + loginCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); + redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, loginCookie); + return (isRedirectResponse ? createRedirectResponse(redirectUrl) : createJsonResponse(true, null, redirectUrl) From 628eb6aebcf1288fdfb8c8f0ee64915b8e3e5ec2 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Mon, 16 Oct 2023 20:52:45 -0400 Subject: [PATCH 04/25] checkpoint refactor commit --- Model/pom.xml | 5 - .../org/gusdb/wdk/model/user/UserFactory.java | 2 - .../wdk/model/user/UserPreferenceFactory.java | 96 ++++++++++--------- .../wdk/service/formatter/UserFormatter.java | 8 +- .../service/service/user/ProfileService.java | 10 +- 5 files changed, 61 insertions(+), 60 deletions(-) diff --git a/Model/pom.xml b/Model/pom.xml index 69ecb362df..8ccd9efc23 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -75,11 +75,6 @@ fgputil-core - - org.gusdb - fgputil-accountdb - - org.gusdb fgputil-server diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index caa5b3e004..ee6ffc978e 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -88,7 +88,6 @@ public class UserFactory { private final DatabaseInstance _userDb; private final String _userSchema; private final AccountManager _accountManager; - private final UserPreferenceFactory _preferenceFactory; // ------------------------------------------------------------------------- // constructor @@ -96,7 +95,6 @@ public class UserFactory { public UserFactory(WdkModel wdkModel) { _wdkModel = wdkModel; - _preferenceFactory = new UserPreferenceFactory(wdkModel); _userDb = wdkModel.getUserDb(); ModelConfig modelConfig = wdkModel.getModelConfig(); _userSchema = modelConfig.getUserDB().getUserSchema(); diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferenceFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferenceFactory.java index e5a75ba7f8..4cd071bde1 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferenceFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferenceFactory.java @@ -39,15 +39,14 @@ public UserPreferenceFactory(WdkModel wdkModel) { _userSchema = wdkModel.getModelConfig().getUserDB().getUserSchema(); } - public void savePreferences(User user) throws WdkModelException { + public void savePreferences(long userId, UserPreferences newPrefs) throws WdkModelException { // get old preferences and determine what to delete, update, insert - long userId = user.getUserId(); - UserPreferences oldPreferences = getPreferences(user); + UserPreferences oldPreferences = getPreferences(userId); Map oldGlobal = oldPreferences.getGlobalPreferences(); - Map newGlobal = user.getPreferences().getGlobalPreferences(); + Map newGlobal = newPrefs.getGlobalPreferences(); updatePreferences(userId, GLOBAL_PREFERENCE_KEY, oldGlobal, newGlobal); Map oldSpecific = oldPreferences.getProjectPreferences(); - Map newSpecific = user.getPreferences().getProjectPreferences(); + Map newSpecific = newPrefs.getProjectPreferences(); updatePreferences(userId, _wdkModel.getProjectId(), oldSpecific, newSpecific); } @@ -82,52 +81,59 @@ private void updatePreferences(long userId, String prefProjectId, PreparedStatement psDelete = null, psInsert = null, psUpdate = null; try { + // delete preferences - String sqlDelete = "DELETE FROM " + _userSchema + "preferences " - + " WHERE user_id = ? AND project_id = ? " - + " AND preference_name = ?"; - psDelete = SqlUtils.getPreparedStatement(_userDb.getDataSource(), sqlDelete); - long start = System.currentTimeMillis(); - for (String key : toDelete) { - psDelete.setLong(1, userId); - psDelete.setString(2, prefProjectId); - psDelete.setString(3, key); - psDelete.addBatch(); + if (!toDelete.isEmpty()) { + String sqlDelete = "DELETE FROM " + _userSchema + "preferences " + + " WHERE user_id = ? AND project_id = ? " + + " AND preference_name = ?"; + psDelete = SqlUtils.getPreparedStatement(_userDb.getDataSource(), sqlDelete); + long start = System.currentTimeMillis(); + for (String key : toDelete) { + psDelete.setLong(1, userId); + psDelete.setString(2, prefProjectId); + psDelete.setString(3, key); + psDelete.addBatch(); + } + psDelete.executeBatch(); + QueryLogger.logEndStatementExecution(sqlDelete, "wdk-user-delete-preference", start); } - psDelete.executeBatch(); - QueryLogger.logEndStatementExecution(sqlDelete, "wdk-user-delete-preference", start); // insert preferences - String sqlInsert = "INSERT INTO " + _userSchema + "preferences " - + " (user_id, project_id, preference_name, " + " preference_value)" - + " VALUES (?, ?, ?, ?)"; - psInsert = SqlUtils.getPreparedStatement(_userDb.getDataSource(), sqlInsert); - start = System.currentTimeMillis(); - for (String key : toInsert.keySet()) { - psInsert.setLong(1, userId); - psInsert.setString(2, prefProjectId); - psInsert.setString(3, key); - psInsert.setString(4, toInsert.get(key)); - psInsert.addBatch(); + if (!toInsert.isEmpty()) { + String sqlInsert = "INSERT INTO " + _userSchema + "preferences " + + " (user_id, project_id, preference_name, " + " preference_value)" + + " VALUES (?, ?, ?, ?)"; + psInsert = SqlUtils.getPreparedStatement(_userDb.getDataSource(), sqlInsert); + long start = System.currentTimeMillis(); + for (String key : toInsert.keySet()) { + psInsert.setLong(1, userId); + psInsert.setString(2, prefProjectId); + psInsert.setString(3, key); + psInsert.setString(4, toInsert.get(key)); + psInsert.addBatch(); + } + psInsert.executeBatch(); + QueryLogger.logEndStatementExecution(sqlInsert, "wdk-user-insert-preference", start); } - psInsert.executeBatch(); - QueryLogger.logEndStatementExecution(sqlInsert, "wdk-user-insert-preference", start); // update preferences - String sqlUpdate = "UPDATE " + _userSchema + "preferences " - + " SET preference_value = ? WHERE user_id = ? " - + " AND project_id = ? AND preference_name = ?"; - psUpdate = SqlUtils.getPreparedStatement(_userDb.getDataSource(), sqlUpdate); - start = System.currentTimeMillis(); - for (String key : toUpdate.keySet()) { - psUpdate.setString(1, toUpdate.get(key)); - psUpdate.setLong(2, userId); - psUpdate.setString(3, prefProjectId); - psUpdate.setString(4, key); - psUpdate.addBatch(); + if (!toUpdate.isEmpty()) { + String sqlUpdate = "UPDATE " + _userSchema + "preferences " + + " SET preference_value = ? WHERE user_id = ? " + + " AND project_id = ? AND preference_name = ?"; + psUpdate = SqlUtils.getPreparedStatement(_userDb.getDataSource(), sqlUpdate); + long start = System.currentTimeMillis(); + for (String key : toUpdate.keySet()) { + psUpdate.setString(1, toUpdate.get(key)); + psUpdate.setLong(2, userId); + psUpdate.setString(3, prefProjectId); + psUpdate.setString(4, key); + psUpdate.addBatch(); + } + psUpdate.executeBatch(); + QueryLogger.logEndStatementExecution(sqlUpdate, "wdk-user-update-preference", start); } - psUpdate.executeBatch(); - QueryLogger.logEndStatementExecution(sqlUpdate, "wdk-user-update-preference", start); } catch (SQLException e) { throw new WdkModelException("Unable to update user (id=" + userId @@ -145,12 +151,12 @@ private void updatePreferences(long userId, String prefProjectId, * @return a list of 2 elements, the first is a map of global preferences, the * second is a map of project-specific preferences. */ - public UserPreferences getPreferences(User user) throws WdkModelException { + public UserPreferences getPreferences(long userId) throws WdkModelException { try { String sql = "SELECT * FROM " + _userSchema + "preferences WHERE user_id = ?"; return new SQLRunner(_userDb.getDataSource(), sql, "wdk-user-select-preference") .executeQuery( - new Object[]{ user.getUserId() }, + new Object[]{ userId }, new Integer[]{ Types.BIGINT }, rs -> { UserPreferences prefs = new UserPreferences(); diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java index 7967c2978d..5d93a7d360 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.wdk.core.api.JsonKeys; @@ -29,7 +30,7 @@ public class UserFormatter { public static JSONObject getUserJson(User user, boolean isOwner, - boolean includePreferences, List propDefs) throws JSONException { + Optional userPreferences, List propDefs) throws JSONException { JSONObject json = new JSONObject() .put(JsonKeys.ID, user.getUserId()) .put(JsonKeys.IS_GUEST, user.isGuest()); @@ -37,9 +38,8 @@ public static JSONObject getUserJson(User user, boolean isOwner, if (isOwner) { json.put(JsonKeys.EMAIL, user.getEmail()); json.put(JsonKeys.PROPERTIES, getPropertiesJson(user.getProfileProperties(), propDefs, isOwner)); - if (includePreferences) { - json.put(JsonKeys.PREFERENCES, getPreferencesJson(user.getPreferences())); - } + userPreferences.ifPresent(prefs -> + json.put(JsonKeys.PREFERENCES, getPreferencesJson(prefs))); } return json; } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index 8a116fff41..b09ad6171e 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -2,6 +2,7 @@ import java.util.List; import java.util.Map.Entry; +import java.util.Optional; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; @@ -23,6 +24,8 @@ import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; +import org.gusdb.wdk.model.user.UserPreferenceFactory; +import org.gusdb.wdk.model.user.UserPreferences; import org.gusdb.wdk.service.UserBundle; import org.gusdb.wdk.service.annotation.PATCH; import org.gusdb.wdk.service.formatter.UserFormatter; @@ -34,9 +37,6 @@ import org.json.JSONException; import org.json.JSONObject; -/* - * TODO: rename this to UsersService when this branch is merged to trunk - */ public class ProfileService extends UserService { private static final String DUPLICATE_EMAIL = "This email is already in use by another account. Please choose another."; @@ -68,7 +68,9 @@ public JSONObject getById(@QueryParam("includePreferences") Boolean includePrefe * @throws WdkModelException if something goes wrong */ protected JSONObject formatUser(User user, boolean isSessionUser, boolean includePrefs, List propNames) throws WdkModelException { - return UserFormatter.getUserJson(user, isSessionUser, includePrefs, propNames); + Optional userPrefs = !includePrefs ? Optional.empty() : + Optional.of(new UserPreferenceFactory(getWdkModel()).getPreferences(user.getUserId())); + return UserFormatter.getUserJson(user, isSessionUser, userPrefs, propNames); } /** From ddd0f25d70773c2062b97d98af62a083733707e0 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 6 Dec 2023 01:45:51 -0500 Subject: [PATCH 05/25] Complete separation of user prefs from user --- Model/pom.xml | 1 - .../java/org/gusdb/wdk/model/user/User.java | 7 ---- .../wdk/model/user/UserCreationScript.java | 13 ++++--- .../org/gusdb/wdk/model/user/UserFactory.java | 37 +++++-------------- .../gusdb/wdk/model/user/UserPreferences.java | 14 ++++--- .../wdk/session/WdkOAuthClientWrapper.java | 37 +++++-------------- Service/pom.xml | 1 - .../wdk/service/service/SessionService.java | 4 +- .../service/user/PreferenceService.java | 18 +++++---- .../service/user/UserUtilityServices.java | 10 ++++- 10 files changed, 56 insertions(+), 86 deletions(-) diff --git a/Model/pom.xml b/Model/pom.xml index 8ccd9efc23..17ad15f118 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -88,7 +88,6 @@ org.gusdb oauth2-client - 2.0.0-SNAPSHOT diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/User.java b/Model/src/main/java/org/gusdb/wdk/model/user/User.java index 168fab7ce6..dc7a35894c 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/User.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/User.java @@ -88,13 +88,6 @@ public Map getProfileProperties() { return _properties; } - /** - * Removes all existing user profile properties - */ - public void clearProfileProperties() { - _properties.clear(); - } - public WdkModel getWdkModel() { return _wdkModel; } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java index 87093118a6..1e9750fc97 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java @@ -83,8 +83,11 @@ public static void main(String[] args) throws WdkModelException, IOException { else { // create new user and assign preferences user = model.getUserFactory().createUser( - parsedLine.getEmail(), parsedLine.getUserProperties(), - parsedLine.getGlobalUserPrefs(), Collections.emptyMap(), true, false); + parsedLine.getEmail(), parsedLine.getUserProperties(), true, false); + new UserPreferenceFactory(model).savePreferences(user.getUserId(), + new UserPreferences(parsedLine.getGlobalUserPrefs(), Collections.emptyMap())); + + System.out.println("Created user with ID " + user.getUserId() + " and email " + user.getEmail()); } } @@ -97,12 +100,12 @@ public static void main(String[] args) throws WdkModelException, IOException { } else { // user exists already; simply add preferences - UserPreferences userPrefs = user.getPreferences(); + UserPreferenceFactory prefsFactory = new UserPreferenceFactory(model); + UserPreferences userPrefs = prefsFactory.getPreferences(user.getUserId()); for (Entry newPref : parsedLine.getGlobalUserPrefs().entrySet()) { userPrefs.setGlobalPreference(newPref.getKey(), newPref.getValue()); } - user.setPreferences(userPrefs); - model.getUserFactory().savePreferences(user); + prefsFactory.savePreferences(user.getUserId(), userPrefs); System.out.println(String.format(message, "adding")); } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index ee6ffc978e..11b6518db2 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -108,19 +108,15 @@ public UserFactory(WdkModel wdkModel) { // ------------------------------------------------------------------------- public User createUser(String email, - Map profileProperties, - Map globalPreferences, - Map projectPreferences) + Map profileProperties) throws WdkModelException, InvalidUsernameOrEmailException { String dontEmailProp = _wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); boolean sendWelcomeEmail = (dontEmailProp == null || !dontEmailProp.equals("true")); - return createUser(email, profileProperties, globalPreferences, projectPreferences, true, sendWelcomeEmail); + return createUser(email, profileProperties, true, sendWelcomeEmail); } public User createUser(String email, Map profileProperties, - Map globalPreferences, - Map projectPreferences, boolean addUserDbReference, boolean sendWelcomeEmail) throws WdkModelException, InvalidUsernameOrEmailException { try { @@ -152,13 +148,6 @@ public User createUser(String email, profile.getEmail(), profile.getSignature(), profile.getStableId()); user.setProfileProperties(profile.getProperties()); - // set and save preferences - UserPreferences prefs = new UserPreferences(); - if (globalPreferences != null) prefs.setGlobalPreferences(globalPreferences); - if (projectPreferences != null) prefs.setProjectPreferences(projectPreferences); - user.setPreferences(prefs); - _preferenceFactory.savePreferences(user); - // if needed, send user temporary password via email if (sendWelcomeEmail) { emailTemporaryPassword(user, password, _wdkModel.getModelConfig()); @@ -176,10 +165,6 @@ public User createUser(String email, } } - public void savePreferences(User user) throws WdkModelException { - _preferenceFactory.savePreferences(user); - } - private void addUserReference(Long userId, boolean isGuest) { Timestamp insertedOn = new Timestamp(new Date().getTime()); String sql = INSERT_USER_REF_SQL @@ -233,6 +218,7 @@ private static void emailTemporaryPassword(User user, String password, Utilities.sendEmail(smtpServer, user.getEmail(), supportEmail, emailSubject, emailContent); } + // TODO: remove! This validation is now done on the OAuth server private static String validateAndFormatEmail(String email, AccountManager accountMgr) throws InvalidUsernameOrEmailException { // trim and validate passed email address and extract stable name if (email == null) @@ -246,7 +232,7 @@ private static String validateAndFormatEmail(String email, AccountManager accoun throw new InvalidUsernameOrEmailException("The user's email address is invalid."); // check whether the user exist in the database already; if email exists, the operation fails if (accountMgr.getUserProfileByEmail(email) != null) - throw new InvalidUsernameOrEmailException("The email '" + email + "' has already been registered. " + "Please choose another one."); + throw new InvalidUsernameOrEmailException("The email '" + email + "' has already been registered. " + "Please choose another."); return email; } @@ -300,7 +286,7 @@ private User completeLogin(User user) { } public User login(User guest, String email, String password) - throws WdkUserException, WdkModelException { + throws WdkUserException { // make sure the guest is really a guest if (!guest.isGuest()) throw new WdkUserException("User has been logged in."); @@ -330,7 +316,7 @@ public boolean isCorrectPassword(String usernameOrEmail, String password) throws return authenticate(usernameOrEmail, password) != null; } - private User authenticate(String usernameOrEmail, String password) throws WdkModelException { + private User authenticate(String usernameOrEmail, String password) { return populateRegisteredUser(_accountManager.getUserProfile(usernameOrEmail, password)); } @@ -364,15 +350,15 @@ public Optional getUserById(long userId) throws WdkModelException { } } - public User getUserByEmail(String email) throws WdkModelException { + public User getUserByEmail(String email) { return populateRegisteredUser(_accountManager.getUserProfileByEmail(email)); } - private User getUserProfileByUsernameOrEmail(String usernameOrEmail) throws WdkModelException { + private User getUserProfileByUsernameOrEmail(String usernameOrEmail) { return populateRegisteredUser(_accountManager.getUserProfileByUsernameOrEmail(usernameOrEmail)); } - public User getUserBySignature(String signature) throws WdkModelException, WdkUserException { + public User getUserBySignature(String signature) throws WdkUserException { User user = populateRegisteredUser(_accountManager.getUserProfileBySignature(signature)); if (user == null) { // signature is rarely sent in by user; if User cannot be found, it's probably an error @@ -381,12 +367,11 @@ public User getUserBySignature(String signature) throws WdkModelException, WdkUs return user; } - private User populateRegisteredUser(UserProfile profile) throws WdkModelException { + private User populateRegisteredUser(UserProfile profile) { if (profile == null) return null; User user = new RegisteredUser(_wdkModel, profile.getUserId(), profile.getEmail(), profile.getSignature(), profile.getStableId()); user.setProfileProperties(profile.getProperties()); - user.setPreferences(_preferenceFactory.getPreferences(user)); return user; } @@ -429,8 +414,6 @@ public void saveUser(User user) throws WdkModelException, InvalidUsernameOrEmail UserProfile newProfile = _accountManager.getUserProfile(user.getUserId()); Events.trigger(new UserProfileUpdateEvent(oldProfile, newProfile, _wdkModel)); - // save preferences - _preferenceFactory.savePreferences(user); } catch (InvalidUsernameOrEmailException e) { throw e; diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferences.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferences.java index 4c4633befd..c448c48cd2 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferences.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserPreferences.java @@ -23,12 +23,16 @@ public class UserPreferences { * the preferences for the user: . It only contains the preferences for the current * project */ - private final Map _globalPreferences; - private final Map _projectPreferences; + private final Map _globalPreferences = new LinkedHashMap<>(); + private final Map _projectPreferences = new LinkedHashMap<>(); - public UserPreferences() { - _globalPreferences = new LinkedHashMap(); - _projectPreferences = new LinkedHashMap(); + public UserPreferences() { } + + public UserPreferences( + Map globalPreferences, + Map projectPreferences) { + if (globalPreferences != null) setGlobalPreferences(globalPreferences); + if (projectPreferences != null) setProjectPreferences(projectPreferences); } public void setProjectPreference(String prefName, String prefValue) { diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index 46e03a9a97..6c42fa052b 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -1,43 +1,18 @@ package org.gusdb.wdk.session; -import static org.gusdb.fgputil.FormatUtil.NL; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.nio.file.Paths; -import java.util.List; -import java.util.Map.Entry; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedHashMap; -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; - import org.apache.log4j.Logger; -import org.glassfish.jersey.client.ClientConfig; -import org.gusdb.fgputil.FormatUtil; -import org.gusdb.fgputil.IoUtil; +import org.gusdb.oauth2.client.OAuthClient; +import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; import org.gusdb.oauth2.client.OAuthConfig; import org.gusdb.oauth2.shared.token.IdTokenFields; -import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; -import org.gusdb.oauth2.client.KeyStoreTrustManager; -import org.gusdb.oauth2.client.OAuthClient; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.config.ModelConfig; import org.gusdb.wdk.model.user.RegisteredUser; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; -import org.json.JSONException; import org.json.JSONObject; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.impl.TextCodec; public class WdkOAuthClientWrapper { @@ -76,12 +51,18 @@ public ValidatedToken getValidatedBearerTokenFromCredentials(String email, Strin public User getUserFromValidatedToken(ValidatedToken token, UserFactory userFactory) { Claims claims = token.getTokenContents(); + User user = new RegisteredUser(_wdkModel, Long.parseLong(claims.getSubject()), claims.get("email", String.class), "deprecated", // user signature claims.get(IdTokenFields.preferred_username.name(), String.class)); - user.set + + // have to hit the user data endpoint for this for user details; bearer token does not include all fields + JSONObject userData = _client.getUserData(_config, token); + + // user UserFactory to create user from the JSON object, adding to DB if necessary for FKs on other userlogins5 tables + User user = new UserFactory(wdkModel). //long userId = client.getUserIdFromAuthCode(authCode, appUrl); //User newUser = wdkModel.getUserFactory().login(userId); if (newUser == null) { diff --git a/Service/pom.xml b/Service/pom.xml index 132d102562..5e896d9cb0 100644 --- a/Service/pom.xml +++ b/Service/pom.xml @@ -54,7 +54,6 @@ org.gusdb oauth2-client - 2.0.0-SNAPSHOT diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 6b1ee1f50d..949251ab5d 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -164,7 +164,7 @@ public Response processOauthLogin( WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); // Use auth code to get the bearer token, then convert to User - ValidatedToken bearerToken = client.getValidatedTokenFromAuth(authCode, appUrl); + ValidatedToken bearerToken = client.getValidatedBearerTokenFromAuth(authCode, appUrl); User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); // transfer ownership from guest to logged-in user @@ -209,7 +209,7 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); // Use passed credentials to get the bearer token, then convert to User - ValidatedToken bearerToken = client.getValidatedTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); + ValidatedToken bearerToken = client.getValidatedBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/PreferenceService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/PreferenceService.java index 7c61696f09..c7286dc28f 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/PreferenceService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/PreferenceService.java @@ -12,6 +12,7 @@ import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.user.User; +import org.gusdb.wdk.model.user.UserPreferenceFactory; import org.gusdb.wdk.model.user.UserPreferences; import org.gusdb.wdk.service.annotation.InSchema; import org.gusdb.wdk.service.annotation.OutSchema; @@ -38,7 +39,8 @@ public PreferenceService(@PathParam(USER_ID_PATH_PARAM) String uid) { @OutSchema("wdk.users.preferences.get-response") public Response getUserPrefs() throws WdkModelException { User user = getUserBundle(Access.PRIVATE).getSessionUser(); - return Response.ok(UserFormatter.getPreferencesJson(user.getPreferences()).toString()).build(); + UserPreferences userPrefs = new UserPreferenceFactory(getWdkModel()).getPreferences(user.getUserId()); + return Response.ok(UserFormatter.getPreferencesJson(userPrefs).toString()).build(); } /** @@ -61,7 +63,7 @@ public Response getUserPrefs() throws WdkModelException { public Response patchGlobalUserPrefs(JSONObject body) throws WdkModelException, DataValidationException { return patchUserPrefs(body, Scope.GLOBAL); } - + @PATCH @Path("preferences/project") @Consumes(MediaType.APPLICATION_JSON) @@ -69,13 +71,14 @@ public Response patchGlobalUserPrefs(JSONObject body) throws WdkModelException, public Response patchProjectUserPrefs(JSONObject body) throws WdkModelException, DataValidationException { return patchUserPrefs(body, Scope.PROJECT); } - + private Response patchUserPrefs(JSONObject body, Scope scope) throws WdkModelException, DataValidationException { User user = getUserBundle(Access.PRIVATE).getSessionUser(); try { Action action = Action.valueOf(body.getString(JsonKeys.ACTION)); // IllegalArgumentException - UserPreferences prefs = user.getPreferences(); - + UserPreferenceFactory prefsFactory = new UserPreferenceFactory(getWdkModel()); + UserPreferences prefs = prefsFactory.getPreferences(user.getUserId()); + if (scope == Scope.GLOBAL) { if (action == Action.CLEAR) prefs.clearGlobalPreferences(); else { @@ -85,7 +88,6 @@ private Response patchUserPrefs(JSONObject body, Scope scope) throws WdkModelExc } } } - else { if (action == Action.CLEAR) prefs.clearProjectPreferences(); else { @@ -95,8 +97,8 @@ private Response patchUserPrefs(JSONObject body, Scope scope) throws WdkModelExc } } } - - getWdkModel().getUserFactory().savePreferences(user); + + prefsFactory.savePreferences(user.getUserId(), prefs); return Response.noContent().build(); } catch(JSONException | RequestMisformatException | IllegalArgumentException e) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java index 488930d45c..1d643076e8 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java @@ -25,6 +25,8 @@ import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; +import org.gusdb.wdk.model.user.UserPreferenceFactory; +import org.gusdb.wdk.model.user.UserPreferences; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.gusdb.wdk.service.request.user.UserCreationRequest; @@ -59,11 +61,15 @@ public Response createNewUser(String body) throws RequestMisformatException, Dat List configuredUserProps = getWdkModel().getModelConfig().getAccountDB().getUserPropertyNames(); UserCreationRequest request = UserCreationRequest.createFromJson(requestJson, configuredUserProps); + // create the user, saving to DB User newUser = getWdkModel().getUserFactory().createUser( request.getProfileRequest().getEmail(), - request.getProfileRequest().getProfileMap(), + request.getProfileRequest().getProfileMap()); + + // add user preferences to the new user + new UserPreferenceFactory(getWdkModel()).savePreferences(newUser.getUserId(), new UserPreferences( request.getGlobalPreferencesRequest().getPreferenceUpdates(), - request.getProjectPreferencesRequest().getPreferenceUpdates()); + request.getProjectPreferencesRequest().getPreferenceUpdates())); return Response.ok(new JSONObject().put(JsonKeys.ID, newUser.getUserId())) .location(getUriInfo().getAbsolutePathBuilder().build(newUser.getUserId())) From 42625c252382183e9a26b8fc2911b35c7384aa2f Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Thu, 14 Dec 2023 08:46:20 -0500 Subject: [PATCH 06/25] Remove incorrect documentation --- Service/doc/raml/includes/raml/record-types-api.raml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Service/doc/raml/includes/raml/record-types-api.raml b/Service/doc/raml/includes/raml/record-types-api.raml index 41ec2f8a7b..37fa823bfc 100644 --- a/Service/doc/raml/includes/raml/record-types-api.raml +++ b/Service/doc/raml/includes/raml/record-types-api.raml @@ -31,9 +31,7 @@ get: /records: post: description: > - (GET by POST) get the attributes and tables of a record instance. If - just the PK is supplied, gets all tables and attributes. Otherwise, - only get those that are explicitly specified. + (GET by POST) get the attributes and tables of a record instance queryParameters: source_id: project: From 72be5baaff5721f5f2e0fb0397145893f032e018 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Thu, 14 Dec 2023 08:46:59 -0500 Subject: [PATCH 07/25] Add signature back into OAuth API (not deprectated after all) --- .../wdk/session/WdkOAuthClientWrapper.java | 20 +++++++++---------- .../wdk/service/service/SessionService.java | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index 6c42fa052b..fd5a27734f 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -33,20 +33,20 @@ public WdkOAuthClientWrapper(WdkModel wdkModel) throws WdkModelException { } } - public ValidatedToken getValidatedIdTokenFromAuth(String authCode, String redirectUri) { - return _client.getValidatedAuthToken(_config, authCode, redirectUri); + public ValidatedToken getAuthTokenFromAuthCode(String authCode, String redirectUri) { + return _client.getAuthTokenFromAuthCode(_config, authCode, redirectUri); } - public ValidatedToken getValidatedIdTokenFromCredentials(String email, String password, String redirectUrl) { - return _client.getValidatedAuthToken(_config, email, password, redirectUrl); + public ValidatedToken getAuthTokenFromCredentials(String email, String password, String redirectUrl) { + return _client.getAuthTokenFromUsernamePassword(_config, email, password, redirectUrl); } - public ValidatedToken getValidatedBearerTokenFromAuth(String authCode, String redirectUri) { - return _client.getValidatedBearerToken(_config, authCode, redirectUri); + public ValidatedToken getBearerTokenFromAuth(String authCode, String redirectUri) { + return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); } - public ValidatedToken getValidatedBearerTokenFromCredentials(String email, String password, String redirectUrl) { - return _client.getValidatedBearerToken(_config, email, password, redirectUrl); + public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) { + return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); } public User getUserFromValidatedToken(ValidatedToken token, UserFactory userFactory) { @@ -54,8 +54,8 @@ public User getUserFromValidatedToken(ValidatedToken token, UserFactory userFact User user = new RegisteredUser(_wdkModel, Long.parseLong(claims.getSubject()), - claims.get("email", String.class), - "deprecated", // user signature + claims.get(IdTokenFields.email.name(), String.class), + claims.get(IdTokenFields.signature.name(), String.class), claims.get(IdTokenFields.preferred_username.name(), String.class)); // have to hit the user data endpoint for this for user details; bearer token does not include all fields diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 949251ab5d..1172ac2144 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -164,7 +164,7 @@ public Response processOauthLogin( WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); // Use auth code to get the bearer token, then convert to User - ValidatedToken bearerToken = client.getValidatedBearerTokenFromAuth(authCode, appUrl); + ValidatedToken bearerToken = client.getBearerTokenFromAuth(authCode, appUrl); User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); // transfer ownership from guest to logged-in user @@ -209,7 +209,7 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); // Use passed credentials to get the bearer token, then convert to User - ValidatedToken bearerToken = client.getValidatedBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); + ValidatedToken bearerToken = client.getBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); From cf0c5d300fa01fb65ced6b49fc621130500dcd10 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 17 Jan 2024 11:01:25 -0500 Subject: [PATCH 08/25] Make names more consistent --- .../org/gusdb/wdk/session/WdkOAuthClientWrapper.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index fd5a27734f..9158f8193f 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -33,15 +33,15 @@ public WdkOAuthClientWrapper(WdkModel wdkModel) throws WdkModelException { } } - public ValidatedToken getAuthTokenFromAuthCode(String authCode, String redirectUri) { - return _client.getAuthTokenFromAuthCode(_config, authCode, redirectUri); + public ValidatedToken getIdTokenFromAuthCode(String authCode, String redirectUri) { + return _client.getIdTokenFromAuthCode(_config, authCode, redirectUri); } - public ValidatedToken getAuthTokenFromCredentials(String email, String password, String redirectUrl) { - return _client.getAuthTokenFromUsernamePassword(_config, email, password, redirectUrl); + public ValidatedToken getIdTokenFromCredentials(String email, String password, String redirectUrl) { + return _client.getIdTokenFromUsernamePassword(_config, email, password, redirectUrl); } - public ValidatedToken getBearerTokenFromAuth(String authCode, String redirectUri) { + public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) { return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); } From ffff175625c2392ba45044b0d61694f55ad49348 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 31 Jan 2024 21:39:07 -0500 Subject: [PATCH 09/25] Changes to support bearer tokens and remove accountdb access from WDK --- .../conifer/templates/WDK/model-config.xml.j2 | 11 +- .../filter/CheckLoginFilterShared.java | 185 ------------------ .../java/org/gusdb/wdk/model/WdkModel.java | 3 +- .../model/config/ModelConfigAccountDB.java | 13 -- .../wdk/model/config/ModelConfigParser.java | 2 - .../gusdb/wdk/model/user/BearerTokenUser.java | 51 +++++ .../gusdb/wdk/model/user/RegisteredUser.java | 28 --- .../wdk/model/user/UnregisteredUser.java | 39 ---- .../java/org/gusdb/wdk/model/user/User.java | 176 +++++++++++------ .../org/gusdb/wdk/model/user/UserCache.java | 11 +- .../wdk/model/user/UserCreationScript.java | 165 ---------------- .../org/gusdb/wdk/model/user/UserFactory.java | 75 +++---- .../gusdb/wdk/model/user/WdkUserProperty.java | 32 +++ .../wdk/session/WdkOAuthClientWrapper.java | 89 +++------ .../model/user/UserCreationScriptTest.java | 34 ---- .../wdk/service/filter/CheckLoginFilter.java | 111 ++++++++--- .../service/formatter/ProjectFormatter.java | 8 +- .../service/formatter/StrategyFormatter.java | 2 +- .../wdk/service/formatter/UserFormatter.java | 14 +- .../service/service/AbstractWdkService.java | 20 +- .../wdk/service/service/AnswerService.java | 4 +- .../wdk/service/service/QuestionService.java | 12 +- .../wdk/service/service/RecordService.java | 2 +- .../wdk/service/service/SessionService.java | 58 +++--- .../service/TemporaryResultService.java | 6 +- .../service/search/ColumnReporterService.java | 4 +- .../service/service/user/BasketService.java | 4 +- .../service/service/user/ProfileService.java | 12 +- .../service/service/user/StrategyService.java | 4 +- .../service/user/UserDatasetService.java | 2 +- 30 files changed, 433 insertions(+), 744 deletions(-) delete mode 100644 Model/src/main/java/org/gusdb/wdk/controller/filter/CheckLoginFilterShared.java create mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/UnregisteredUser.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java create mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java delete mode 100644 Model/src/test/java/org/gusdb/wdk/model/user/UserCreationScriptTest.java diff --git a/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 b/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 index 3cd45abc51..e831a0dcc4 100644 --- a/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 +++ b/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 @@ -177,16 +177,7 @@ site_vars file: {{ site_vars }} {% if modelconfig_accountDb_driverInitClass is defined -%} driverInitClass="{{ modelconfig_accountDb_driverInitClass }}" {% endif %} - > - - - - - - - - - + /> {% if modelconfig_userDatasetStoreConfig is defined -%} {{ modelconfig_userDatasetStoreConfig|indent }} diff --git a/Model/src/main/java/org/gusdb/wdk/controller/filter/CheckLoginFilterShared.java b/Model/src/main/java/org/gusdb/wdk/controller/filter/CheckLoginFilterShared.java deleted file mode 100644 index 529c9ae58b..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/controller/filter/CheckLoginFilterShared.java +++ /dev/null @@ -1,185 +0,0 @@ -package org.gusdb.wdk.controller.filter; - -import java.util.Optional; - -import org.apache.log4j.Logger; -import org.gusdb.fgputil.Tuples.ThreeTuple; -import org.gusdb.fgputil.events.Events; -import org.gusdb.fgputil.web.CookieBuilder; -import org.gusdb.fgputil.web.LoginCookieFactory; -import org.gusdb.fgputil.web.LoginCookieFactory.LoginCookieParts; -import org.gusdb.fgputil.web.SessionProxy; -import org.gusdb.wdk.events.NewUserEvent; -import org.gusdb.wdk.model.Utilities; -import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.WdkRuntimeException; -import org.gusdb.wdk.model.user.UnregisteredUser.UnregisteredUserType; - -import io.prometheus.client.Counter; - -import org.gusdb.wdk.model.user.User; -import org.gusdb.wdk.model.user.UserFactory; - -public class CheckLoginFilterShared { - - private static final Logger LOG = Logger.getLogger(CheckLoginFilterShared.class); - - private static final Counter GUEST_CREATION_COUNTER = Counter.build() - .name("wdk_guest_creation_count") - .help("Number of guest users created by WDK services") - .register(); - - private enum CurrentState { - - REGISTERED_USER_MATCHING_COOKIE, // 1. registered user, matching wdk cookie = no action - REGISTERED_USER_BAD_COOKIE, // 2. registered user, unmatching, invalid, or missing wdk cookie = send expired cookie, new guest - GUEST_USER_COOKIE_PRESENT, // 3. guest user, any wdk cookie = send expired cookie, keep guest - GUEST_USER_COOKIE_MISSING, // 4. guest user, missing wdk cookie = no action - NO_USER_VALID_COOKIE, // 5. no user, valid wdk cookie = log user in, but do not send updated cookie, since doing so is a bug for GBrowse - NO_USER_INVALID_COOKIE, // 6. no user, invalid wdk cookie = send expired cookie, new guest - NO_USER_MISSING_COOKIE; // 7. no user, missing wdk cookie = new guest - - public boolean requiresAction() { - return !equals(REGISTERED_USER_MATCHING_COOKIE) && - !equals(GUEST_USER_COOKIE_MISSING); - } - - public static CurrentState calculateState( - boolean userPresent, - boolean isGuestUser, - boolean cookiePresent, - boolean cookieValid, - boolean cookieMatches) { - return - (userPresent ? - // user is present cases - (isGuestUser ? - // guest user cases - (cookiePresent ? GUEST_USER_COOKIE_PRESENT : GUEST_USER_COOKIE_MISSING) : - // logged user cases - (cookieMatches ? REGISTERED_USER_MATCHING_COOKIE : REGISTERED_USER_BAD_COOKIE)) : - // no user present cases - (cookiePresent ? - // cookie but no user - (cookieValid ? NO_USER_VALID_COOKIE : NO_USER_INVALID_COOKIE) : - // no cookie, no user - NO_USER_MISSING_COOKIE)); - } - } - - private static class StateBundle extends ThreeTuple { - - public StateBundle(CurrentState state, User sessionUser, String cookieEmail) { - super(state, sessionUser, cookieEmail); - } - - public CurrentState getCurrentState() { return getFirst(); } - public User getSessionUser() { return getSecond(); } - public String getCookieEmail() { return getThird(); } - - } - - public static Optional calculateUserActions( - Optional currentCookie, - WdkModel wdkModel, - SessionProxy session, - String requestPath) { - - UserFactory userFactory = wdkModel.getUserFactory(); - - // three-tuple is: caseNumber, sessionUser, cookieEmail - StateBundle stateBundle = calculateCurrentState(wdkModel, session, currentCookie); - - // only enter synchronized block if action is required - // (i.e. a new user must be added to the session or a logout cookie returned - if (stateBundle.getCurrentState().requiresAction()) { - - // since action cases require creation of a user and assignment on session, synchronize on session - synchronized(session.getUnderlyingSession()) { // must sync on shared object - - // recalculate in case something changed outside the synchronized block - stateBundle = calculateCurrentState(wdkModel, session, currentCookie); - - try { - // determine actions based on state - CookieBuilder cookieToSend = null; - User userToSet = null; - switch (stateBundle.getCurrentState()) { - case REGISTERED_USER_BAD_COOKIE: - cookieToSend = LoginCookieFactory.createLogoutCookie(); - userToSet = createGuest(requestPath, userFactory); - break; - case GUEST_USER_COOKIE_PRESENT: - cookieToSend = LoginCookieFactory.createLogoutCookie(); - // guest user present in session is sufficient - break; - case NO_USER_VALID_COOKIE: - // do not want to update max age on cookie since we have no way to tell GBrowse to also update - userToSet = userFactory.getUserByEmail(stateBundle.getCookieEmail()); - break; - case NO_USER_INVALID_COOKIE: - cookieToSend = LoginCookieFactory.createLogoutCookie(); - userToSet = createGuest(requestPath, userFactory); - break; - case NO_USER_MISSING_COOKIE: - // no cookie necessary - userToSet = createGuest(requestPath, userFactory); - break; - default: - // other cases require no action - break; - } - - // take action as needed - if (userToSet != null) { - session.setAttribute(Utilities.WDK_USER_KEY, userToSet); - Events.triggerAndWait(new NewUserEvent(userToSet, stateBundle.getSessionUser(), session), - new WdkRuntimeException("Unable to complete WDK user assignement.")); - } - return Optional.ofNullable(cookieToSend); - } - catch (Exception ex) { - LOG.error("Caught exception while checking login cookie", ex); - return Optional.of(LoginCookieFactory.createLogoutCookie()); - } - } - } - return Optional.empty(); - } - - private static User createGuest(String requestPath, UserFactory userFactory) { - User guest = userFactory.createUnregistedUser(UnregisteredUserType.GUEST); - LOG.info("Created new guest user [" + guest.getUserId() + "] for request to path: /" + requestPath); - GUEST_CREATION_COUNTER.inc(); - return guest; - } - - private static StateBundle calculateCurrentState(WdkModel wdkModel, SessionProxy session, Optional loginCookie) { - - // get the current user in session and determine type - User wdkUser = (User)session.getAttribute(Utilities.WDK_USER_KEY); - boolean userPresent = (wdkUser != null); - boolean isGuestUser = (userPresent ? wdkUser.isGuest() : false); - - // figure out what's going on with the cookie - boolean cookiePresent = loginCookie.isPresent(); - LoginCookieParts cookieParts = null; - boolean cookieValid = false, cookieMatches = false; - try { - if (cookiePresent) { - LoginCookieFactory auth = new LoginCookieFactory(wdkModel.getModelConfig().getSecretKey()); - cookieParts = LoginCookieFactory.parseCookieValue(loginCookie.get().getValue()); - cookieValid = auth.isValidCookie(cookieParts); - cookieMatches = cookieValid ? (wdkUser != null && cookieParts.getUsername().equals(wdkUser.getEmail())) : false; - } - } - catch (IllegalArgumentException e) { - /* negative values already set */ - } - - CurrentState state = CurrentState.calculateState(userPresent, isGuestUser, cookiePresent, cookieValid, cookieMatches); - - return new StateBundle(state, wdkUser, cookieValid ? cookieParts.getUsername() : null); - } - -} diff --git a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java index 444a423e57..bb30e38ac3 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java +++ b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java @@ -69,7 +69,6 @@ import org.gusdb.wdk.model.user.BasketFactory; import org.gusdb.wdk.model.user.FavoriteFactory; import org.gusdb.wdk.model.user.StepFactory; -import org.gusdb.wdk.model.user.UnregisteredUser.UnregisteredUserType; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.model.user.analysis.StepAnalysisFactory; @@ -1365,7 +1364,7 @@ public User getSystemUser() { try { systemUserLock.lock(); if (systemUser == null) { - systemUser = userFactory.createUnregistedUser(UnregisteredUserType.SYSTEM); + systemUser = userFactory.createUnregistedUser(); } } finally { diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java index 0307ff8e61..1b1b621b2f 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java @@ -1,15 +1,10 @@ package org.gusdb.wdk.model.config; -import java.util.ArrayList; -import java.util.List; - -import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.fgputil.db.platform.DBPlatform; public class ModelConfigAccountDB extends ModelConfigDB { private String _accountSchema; - private List _userPropertyNames = new ArrayList(); public void setAccountSchema(String accountSchema) { _accountSchema = DBPlatform.normalizeSchema(accountSchema); @@ -18,12 +13,4 @@ public void setAccountSchema(String accountSchema) { public String getAccountSchema() { return _accountSchema; } - - public void addUserPropertyName(UserPropertyName property) { - _userPropertyNames.add(property); - } - - public List getUserPropertyNames() { - return _userPropertyNames; - } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java index 874f876ca1..ee874200ec 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java @@ -4,7 +4,6 @@ import java.net.URL; import org.apache.commons.digester3.Digester; -import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.fgputil.xml.XmlParser; import org.gusdb.fgputil.xml.XmlValidator; import org.gusdb.wdk.model.WdkModelException; @@ -65,7 +64,6 @@ private static Digester configureDigester() { // load user db configureNode(digester, "modelConfig/accountDb", ModelConfigAccountDB.class, "setAccountDB"); - configureNode(digester, "modelConfig/accountDb/userProperty", UserPropertyName.class, "addUserPropertyName"); // userdatasetstore configureNode(digester, "modelConfig/userDatasetStore", ModelConfigUserDatasetStore.class, "setUserDatasetStore"); diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java new file mode 100644 index 0000000000..fb05a0ec48 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java @@ -0,0 +1,51 @@ +package org.gusdb.wdk.model.user; + +import org.apache.log4j.Logger; +import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.shared.token.IdTokenFields; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.session.WdkOAuthClientWrapper; +import org.json.JSONObject; + +public class BearerTokenUser extends User { + + private static final Logger LOG = Logger.getLogger(BearerTokenUser.class); + + private final WdkOAuthClientWrapper _client; + private final ValidatedToken _token; + private boolean _userInfoFetched = false; + + public BearerTokenUser(WdkModel wdkModel, WdkOAuthClientWrapper client, ValidatedToken token) { + // parent constructor sets immutable fields provided on the token + super(wdkModel, + Long.valueOf(token.getUserId()), + token.isGuest(), + token.getTokenContents().get(IdTokenFields.signature.name(), String.class), + token.getTokenContents().get(IdTokenFields.preferred_username.name(), String.class)); + _client = client; + _token = token; + } + + @Override + protected void fetchUserInfo() { + // return if already fetched + if (_userInfoFetched) return; + + LOG.info("User data fetch requested for user " + getUserId() + "; querying OAuth server."); + // fetch user info from OAuth server where it is stored (but only on demand, and only once for this object's lifetime) + JSONObject userInfo = _client.getUserData(_token); + + // set email (standard property but mutable so set on user profile and not token + setEmail(userInfo.getString(IdTokenFields.email.name())); + + // set other user properties found only on user profile object + for (WdkUserProperty userProp : User.USER_PROPERTIES.values()) { + userProp.setValue(this, userInfo.optString(userProp.getName(), null)); + } + + LOG.info("User data successfully fetched for " + getDisplayName() + " / " + getOrganization()); + _userInfoFetched = true; + } + +} + diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java deleted file mode 100644 index 4c175e00fd..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.gusdb.wdk.model.user; - -import org.gusdb.wdk.model.WdkModel; - -public class RegisteredUser extends User { - - public RegisteredUser(WdkModel wdkModel, long userId, String email, String signature, String stableId) { - super(wdkModel, userId, email, signature, stableId); - } - - @Override - public boolean isGuest() { - return false; - } - - @Override - public String getDisplayName() { - return ( - formatNamePart(_properties.get("firstName")) + - formatNamePart(_properties.get("middleName")) + - formatNamePart(_properties.get("lastName"))).trim(); - } - - private static String formatNamePart(String namePart) { - return (namePart == null || namePart.isEmpty() ? "" : " " + namePart.trim()); - } - -} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UnregisteredUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/UnregisteredUser.java deleted file mode 100644 index a84b08ad44..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UnregisteredUser.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.gusdb.wdk.model.user; - -import org.gusdb.wdk.model.WdkModel; - -public class UnregisteredUser extends User { - - // ------------------------------------------------------------------------- - // types of unregistered users - // ------------------------------------------------------------------------- - - public enum UnregisteredUserType { - GUEST("WDK_GUEST_"), - SYSTEM("WDK_GUEST_SYSTEM_"); - - private final String _stableIdPrefix; - - private UnregisteredUserType(String stableIdPrefix) { - _stableIdPrefix = stableIdPrefix; - } - - public String getStableIdPrefix() { - return _stableIdPrefix; - } - } - - UnregisteredUser(WdkModel wdkModel, long userId, String email, String signature, String stableId) { - super(wdkModel, userId, email, signature, stableId); - } - - @Override - public boolean isGuest() { - return true; - } - - @Override - public String getDisplayName() { - return "WDK Guest"; - } -} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/User.java b/Model/src/main/java/org/gusdb/wdk/model/user/User.java index dc7a35894c..af44903e2c 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/User.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/User.java @@ -1,58 +1,69 @@ package org.gusdb.wdk.model.user; -import java.util.HashMap; +import java.util.Collections; +import java.util.List; import java.util.Map; +import org.gusdb.fgputil.accountdb.UserPropertyName; +import org.gusdb.fgputil.functional.Functions; import org.gusdb.wdk.model.WdkModel; /** * Represents a WDK user. - * - * @see UnregisteredUser for subclass representing guest user - * @see RegisteredUser for subclass representing registered user - * + * * @author rdoherty */ -public abstract class User { - - protected WdkModel _wdkModel; - - protected long _userId; - protected String _email; - protected String _signature; - protected String _stableId; +public class User { + + public static final Map USER_PROPERTIES = createUserPropertyDefs(); + + private static Map createUserPropertyDefs() { + List userProps = List.of( + new WdkUserProperty("username", "Username", "username", false, false, false, User::getUsername, User::setUsername), + new WdkUserProperty("firstName", "First Name", "first_name", true, true, false, User::getFirstName, User::setFirstName), + new WdkUserProperty("middleName", "Middle Name", "middle_name", false, true, false, User::getMiddleName, User::setMiddleName), + new WdkUserProperty("lastName", "Last Name", "last_name", true, true, false, User::getLastName, User::setLastName), + new WdkUserProperty("organization", "Organization", "organization", true, true, false, User::getOrganization, User::setOrganization), + new WdkUserProperty("interests", "Interests", "interests", false, false, true, User::getInterests, User::setInterests) + ); + return Collections.unmodifiableMap(Functions.getMapFromValues(userProps, UserPropertyName::getName)); + } - // Holds key/value pairs associated with the user's profile (come from account db) - protected Map _properties = new HashMap<>(); + private final WdkModel _wdkModel; - /** - * Provides a "pretty" display name for this user - * - * @return display name for this user - */ - public abstract String getDisplayName(); + // immutable fields supplied by bearer token + private final long _userId; + private final boolean _isGuest; + private final String _signature; + private final String _stableId; - /** - * Tells whether this user is a guest - * - * @return true if guest, else false - */ - public abstract boolean isGuest(); + // mutable fields that may need to be fetched + private String _email; // standard; not a user property + private String _username; + private String _firstName; + private String _middleName; + private String _lastName; + private String _organization; + private String _interests; - protected User(WdkModel wdkModel, long userId, String email, String signature, String stableId) { + public User(WdkModel wdkModel, long userId, boolean isGuest, String signature, String stableId) { _wdkModel = wdkModel; _userId = userId; - _email = email; + _isGuest = isGuest; _signature = signature; _stableId = stableId; } + public WdkModel getWdkModel() { + return _wdkModel; + } + public long getUserId() { return _userId; } - public String getEmail() { - return _email; + public boolean isGuest() { + return _isGuest; } public String getSignature() { @@ -63,33 +74,94 @@ public String getStableId() { return _stableId; } - public void setEmail(String email) { + protected void fetchUserInfo() { + // nothing to do in this base class; all info must be explicitly set + } + + public String getEmail() { + fetchUserInfo(); + return _email; + } + + public User setEmail(String email) { _email = email; + return this; } - /** - * Sets the value of the profile property given by the UserProfileProperty enum - * @param key - * @param value - */ - public void setProfileProperty(String key, String value) { - _properties.put(key, value); + public String getUsername() { + fetchUserInfo(); + return _username; } - public void setProfileProperties(Map properties) { - _properties = properties; + public User setUsername(String username) { + _username = username; + return this; + } + + public String getFirstName() { + fetchUserInfo(); + return _firstName; + } + + public User setFirstName(String firstName) { + _firstName = firstName; + return this; + } + + public String getMiddleName() { + fetchUserInfo(); + return _middleName; + } + + public User setMiddleName(String middleName) { + _middleName = middleName; + return this; + } + + public String getLastName() { + fetchUserInfo(); + return _lastName; + } + + public User setLastName(String lastName) { + _lastName = lastName; + return this; + } + + public String getOrganization() { + fetchUserInfo(); + return _organization; + } + + public User setOrganization(String organization) { + _organization = organization; + return this; + } + + public String getInterests() { + fetchUserInfo(); + return _interests; + } + + public User setInterests(String interests) { + _interests = interests; + return this; } /** - * Return the entire user profile property map - * @return + * Provides a "pretty" display name for this user + * + * @return display name for this user */ - public Map getProfileProperties() { - return _properties; + public String getDisplayName() { + return isGuest() ? "WDK Guest" : ( + formatNamePart(getFirstName()) + + formatNamePart(getMiddleName()) + + formatNamePart(getLastName())).trim(); } - public WdkModel getWdkModel() { - return _wdkModel; + private static String formatNamePart(String namePart) { + return (namePart == null || namePart.isEmpty() ? "" : " " + namePart.trim()); } @Override @@ -99,7 +171,7 @@ public String toString() { @Override public int hashCode() { - return (int)getUserId(); + return String.valueOf(getUserId()).hashCode(); } @Override @@ -107,13 +179,7 @@ public boolean equals(Object obj) { if (!(obj instanceof User)) { return false; } - User other = (User)obj; - return ( - getUserId() == other.getUserId() && - getEmail().equals(other.getEmail()) && - getSignature().equals(other.getSignature()) && - getStableId().equals(other.getStableId()) - ); + return getUserId() == ((User)obj).getUserId(); } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserCache.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserCache.java index 326ff7687a..519739b8eb 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserCache.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserCache.java @@ -28,17 +28,20 @@ public UserCache(User user) { @Override public User get(Object id) { try { - Long userId = (Long)id; - if (userId == null) { + if (id == null) { throw new WdkRuntimeException("User ID cannot be null."); } + if (!(id instanceof Long)) { + throw new IllegalArgumentException("Only Long objects should be passed to this method, not " + id.getClass().getName()); + } + Long userId = (Long)id; if (!containsKey(userId)) { if (_userFactory != null) { put(userId, _userFactory.getUserById(userId) - .orElseThrow(() -> new WdkRuntimeException("User with ID " + id + " does not exist."))); + .orElseThrow(() -> new WdkRuntimeException("User with ID " + userId + " does not exist."))); } else { - throw new WdkRuntimeException("No-lookup cache does not contain the requested user (" + id + ")."); + throw new WdkRuntimeException("No-lookup cache does not contain the requested user (" + userId + ")."); } } return super.get(userId); diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java deleted file mode 100644 index 1e9750fc97..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java +++ /dev/null @@ -1,165 +0,0 @@ -package org.gusdb.wdk.model.user; - -import static org.gusdb.fgputil.FormatUtil.NL; -import static org.gusdb.fgputil.FormatUtil.TAB; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.gusdb.fgputil.FormatUtil; -import org.gusdb.fgputil.accountdb.UserPropertyName; -import org.gusdb.fgputil.runtime.GusHome; -import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.WdkModelException; - -public class UserCreationScript { - - public static class UserLine { - - private final boolean _shouldWriteUser; - private final String _email; - private final Map _globalUserPrefs; - private final Map _userProperties; - - public UserLine(boolean shouldWriteUser, String email, - Map globalUserPrefs, Map userProperties) { - _shouldWriteUser = shouldWriteUser; - _email = email; - _globalUserPrefs = globalUserPrefs; - _userProperties = userProperties; - } - - public boolean shouldWriteUser() { return _shouldWriteUser; } - public String getEmail() { return _email; } - public Map getGlobalUserPrefs() { return _globalUserPrefs; } - public Map getUserProperties() { return _userProperties; } - - public String getAttributesString() { return getEmail() + ", " + - FormatUtil.prettyPrint(_userProperties) + ", " + - FormatUtil.prettyPrint(_globalUserPrefs); } - - @Override - public String toString() { - return shouldWriteUser() + ", " + getAttributesString(); - } - } - - public static void main(String[] args) throws WdkModelException, IOException { - if (!(args.length == 1 || (args.length == 2 && args[1].equalsIgnoreCase("test")))) { - System.err.println(NL + - "USAGE: fgpJava " + UserCreationScript.class.getName() + " [test]" + NL + NL + - "This script will read tab-delimited user properties from stdin" + NL + - "Passed project_id value is used only to look up account-db access information in gus_home" + NL + - "If 'test' is specified as a second argument, no records will be written to the DB; " + - "instead diagnostics will be printed to stdout" + NL); - System.exit(1); - } - boolean testOnly = args.length == 2; - try (WdkModel model = WdkModel.construct(args[0], GusHome.getGusHome()); - BufferedReader in = new BufferedReader(new InputStreamReader(System.in))) { - List userProps = model.getModelConfig().getAccountDB().getUserPropertyNames(); - int newUserCount = 0, modifiedUserCount = 0, invalidLineCount = 0; - String line; - while ((line = in.readLine()) != null) { - UserLine parsedLine = parseLine(line, userProps); - if (parsedLine.shouldWriteUser()) { - try { - // create or edit user - User user = model.getUserFactory().getUserByEmail(parsedLine.getEmail()); - if (user == null) { - newUserCount++; - if (testOnly) { - System.out.println("Would create user: " + parsedLine.getAttributesString()); - } - else { - // create new user and assign preferences - user = model.getUserFactory().createUser( - parsedLine.getEmail(), parsedLine.getUserProperties(), true, false); - new UserPreferenceFactory(model).savePreferences(user.getUserId(), - new UserPreferences(parsedLine.getGlobalUserPrefs(), Collections.emptyMap())); - - - System.out.println("Created user with ID " + user.getUserId() + " and email " + user.getEmail()); - } - } - else { - modifiedUserCount++; - String message = "User with email " + user.getEmail() + - " exists; %s preferences " + FormatUtil.prettyPrint(parsedLine.getGlobalUserPrefs()); - if (testOnly) { - System.out.println(String.format(message, "would add")); - } - else { - // user exists already; simply add preferences - UserPreferenceFactory prefsFactory = new UserPreferenceFactory(model); - UserPreferences userPrefs = prefsFactory.getPreferences(user.getUserId()); - for (Entry newPref : parsedLine.getGlobalUserPrefs().entrySet()) { - userPrefs.setGlobalPreference(newPref.getKey(), newPref.getValue()); - } - prefsFactory.savePreferences(user.getUserId(), userPrefs); - System.out.println(String.format(message, "adding")); - } - } - } - catch (InvalidUsernameOrEmailException e) { - System.err.println("Invalid email '" + parsedLine.getEmail() + "': " + e.getMessage()); - invalidLineCount++; - } - } - else { - invalidLineCount++; - } - } - System.out.println("Number of new users: " + newUserCount); - System.out.println("Number of existing users we added preferences to (could be more than one project): " + modifiedUserCount); - System.out.println("Number of lines with invalid input: " + invalidLineCount); - } - } - - static UserLine parseLine( - String line, List userProps) { - String[] tokens = line.split(TAB); - if (tokens.length == 0 || tokens[0].trim().isEmpty()) { - System.err.println("Required value [email] missing on line: " + line); - return new UserLine(false, null, null, null); - } - String email = tokens[0]; - - // next token is project_ids user wants emails from - Map globalUserPrefs = (tokens.length > 1 ? - getEmailPrefsFromProjectIds(tokens[1]) : Collections.emptyMap()); - - boolean valid = true; - Map propertyMap = new LinkedHashMap<>(); - for (int i = 0; i < userProps.size(); i++) { - UserPropertyName propName = userProps.get(i); - // split will trim off trailing empty tokens, so backfill - String nextValue = tokens.length > i + 2 ? tokens[i + 2].trim() : ""; - if (propName.isRequired() && nextValue.isEmpty()) { - System.err.println("Required value [" + propName.getName() + "] missing on line: " + line); - valid = false; - } - propertyMap.put(userProps.get(i).getName(), nextValue); - } - return new UserLine(valid, email, globalUserPrefs, propertyMap); - } - - private static Map getEmailPrefsFromProjectIds(String commaDelimitedListOfProjectIds) { - String[] projectIds = commaDelimitedListOfProjectIds.trim().isEmpty() ? - new String[0] : commaDelimitedListOfProjectIds.split(","); - return Arrays.stream(projectIds) - .filter(projectId -> !projectId.trim().isEmpty()) - .map(projectId -> "preference_global_email_" + projectId.trim().toLowerCase()) - .collect(Collectors.toMap(Function.identity(), val -> "on")); - - } -} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index 11b6518db2..5e1ce04f26 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -9,6 +9,7 @@ import java.util.regex.Matcher; import org.apache.log4j.Logger; +import org.gusdb.fgputil.MapBuilder; import org.gusdb.fgputil.accountdb.AccountManager; import org.gusdb.fgputil.accountdb.UserProfile; import org.gusdb.fgputil.db.pool.DatabaseInstance; @@ -24,7 +25,6 @@ import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.config.ModelConfig; import org.gusdb.wdk.model.config.ModelConfigAccountDB; -import org.gusdb.wdk.model.user.UnregisteredUser.UnregisteredUserType; /** * Manages persistence of user profile and preferences and creation and @@ -87,20 +87,18 @@ public class UserFactory { private final WdkModel _wdkModel; private final DatabaseInstance _userDb; private final String _userSchema; - private final AccountManager _accountManager; // ------------------------------------------------------------------------- // constructor // ------------------------------------------------------------------------- public UserFactory(WdkModel wdkModel) { + // save model for populating new users _wdkModel = wdkModel; _userDb = wdkModel.getUserDb(); ModelConfig modelConfig = wdkModel.getModelConfig(); _userSchema = modelConfig.getUserDB().getUserSchema(); ModelConfigAccountDB accountDbConfig = modelConfig.getAccountDB(); - _accountManager = new AccountManager(wdkModel.getAccountDb(), - accountDbConfig.getAccountSchema(), accountDbConfig.getUserPropertyNames()); } // ------------------------------------------------------------------------- @@ -144,9 +142,7 @@ public User createUser(String email, } // create new user object and add profile properties - RegisteredUser user = new RegisteredUser(_wdkModel, profile.getUserId(), - profile.getEmail(), profile.getSignature(), profile.getStableId()); - user.setProfileProperties(profile.getProperties()); + User user = getUserFromProfile(profile); // if needed, send user temporary password via email if (sendWelcomeEmail) { @@ -165,6 +161,18 @@ public User createUser(String email, } } + private User getUserFromProfile(UserProfile profile) { + if (profile == null) return null; + Map properties = profile.getProperties(); + return new User(_wdkModel, profile.getUserId(), + false, profile.getSignature(), profile.getStableId()) + .setEmail(profile.getEmail()) + .setFirstName(properties.get("firstName")) + .setMiddleName(properties.get("middleName")) + .setLastName(properties.get("lastName")) + .setOrganization(properties.get("organization")); + } + private void addUserReference(Long userId, boolean isGuest) { Timestamp insertedOn = new Timestamp(new Date().getTime()); String sql = INSERT_USER_REF_SQL @@ -259,11 +267,11 @@ private static String generateTemporaryPassword() { * @return new unregistered user with remaining fields populated * @throws WdkRuntimeException if unable to persist temporary user */ - public UnregisteredUser createUnregistedUser(UnregisteredUserType userType) throws WdkRuntimeException { + public User createUnregistedUser() throws WdkRuntimeException { try { - UserProfile profile = _accountManager.createGuestAccount(userType.getStableIdPrefix()); + UserProfile profile = _accountManager.createGuestAccount("GUEST_"); addUserReference(profile.getUserId(), true); - return new UnregisteredUser(_wdkModel, profile.getUserId(), profile.getEmail(), profile.getSignature(), profile.getStableId()); + return new User(_wdkModel, profile.getUserId(), true, profile.getSignature(), profile.getStableId()).setEmail(profile.getEmail()); } catch (Exception e) { throw new WdkRuntimeException("Unable to save temporary user", e); @@ -317,7 +325,7 @@ public boolean isCorrectPassword(String usernameOrEmail, String password) throws } private User authenticate(String usernameOrEmail, String password) { - return populateRegisteredUser(_accountManager.getUserProfile(usernameOrEmail, password)); + return getUserFromProfile(_accountManager.getUserProfile(usernameOrEmail, password)); } /** @@ -331,16 +339,16 @@ public Optional getUserById(long userId) throws WdkModelException { UserProfile profile = _accountManager.getUserProfile(userId); if (profile != null) { // found registered user in account DB; create RegisteredUser and populate - return Optional.of(populateRegisteredUser(profile)); + return Optional.of(getUserFromProfile(profile)); } else { // cannot find user in account DB; however, the passed ID may represent a guest local to this userDb Date accessDate = getGuestUserRefFirstAccess(userId); if (accessDate != null) { // guest user was found in local user Db; create UnregisteredUser and populate - profile = AccountManager.createGuestProfile(UnregisteredUserType.GUEST.getStableIdPrefix(), userId, accessDate); - return Optional.of(new UnregisteredUser(_wdkModel, profile.getUserId(), - profile.getEmail(), profile.getSignature(), profile.getStableId())); + profile = AccountManager.createGuestProfile("GUEST_", userId, accessDate); + return Optional.of(new User(_wdkModel, profile.getUserId(), true, + profile.getSignature(), profile.getStableId()).setEmail(profile.getEmail())); } else { @@ -351,28 +359,11 @@ public Optional getUserById(long userId) throws WdkModelException { } public User getUserByEmail(String email) { - return populateRegisteredUser(_accountManager.getUserProfileByEmail(email)); + return getUserFromProfile(_accountManager.getUserProfileByEmail(email)); } private User getUserProfileByUsernameOrEmail(String usernameOrEmail) { - return populateRegisteredUser(_accountManager.getUserProfileByUsernameOrEmail(usernameOrEmail)); - } - - public User getUserBySignature(String signature) throws WdkUserException { - User user = populateRegisteredUser(_accountManager.getUserProfileBySignature(signature)); - if (user == null) { - // signature is rarely sent in by user; if User cannot be found, it's probably an error - throw new WdkUserException("Unable to find user with signature: " + signature); - } - return user; - } - - private User populateRegisteredUser(UserProfile profile) { - if (profile == null) return null; - User user = new RegisteredUser(_wdkModel, profile.getUserId(), profile.getEmail(), - profile.getSignature(), profile.getStableId()); - user.setProfileProperties(profile.getProperties()); - return user; + return getUserFromProfile(_accountManager.getUserProfileByUsernameOrEmail(usernameOrEmail)); } /** @@ -408,7 +399,13 @@ public void saveUser(User user) throws WdkModelException, InvalidUsernameOrEmail } // save off other data to user profile - _accountManager.saveUserProfile(user.getUserId(), user.getEmail(), user.getProfileProperties()); + _accountManager.saveUserProfile(user.getUserId(), user.getEmail(), new MapBuilder() + .put("firstName", user.getFirstName()) + .put("middleName", user.getMiddleName()) + .put("lastName", user.getLastName()) + .put("organization", user.getOrganization()) + .toMap() + ); // get updated profile and trigger profile update event UserProfile newProfile = _accountManager.getUserProfile(user.getUserId()); @@ -440,4 +437,12 @@ public void resetPassword(String emailOrLoginName) throws WdkUserException, WdkM public void changePassword(long userId, String newPassword) { _accountManager.updatePassword(userId, newPassword); } + + // FIXME: should be atomic + public void insertUserToUserDb(User user) { + // make sure user has reference in this user DB (needs to happen before merging) + if (!hasUserReference(user.getUserId())) { + addUserReference(user.getUserId(), user.isGuest()); + } + } } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java b/Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java new file mode 100644 index 0000000000..276ed80c03 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java @@ -0,0 +1,32 @@ +package org.gusdb.wdk.model.user; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.gusdb.fgputil.accountdb.UserPropertyName; + +/** + * Convenience class to work around accountdb API and interface with WDK User class + */ +public class WdkUserProperty extends UserPropertyName { + + private final Function _getter; + private final BiConsumer _setter; + + public WdkUserProperty(String name, String displayName, String dbKey, boolean isRequired, boolean isPublic, boolean isMultiLine, Function getter, BiConsumer setter) { + UserPropertyName userProp = new UserPropertyName(name, dbKey, isRequired); + userProp.setDisplayName(displayName); + userProp.setPublic(isPublic); + userProp.setMultiLine(isMultiLine); + _getter = getter; + _setter = setter; + } + + public String getValue(User user) { + return _getter.apply(user); + } + + public void setValue(User user, String value) { + _setter.accept(user, value); + } +} diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index 9158f8193f..52ea09d7bb 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -1,44 +1,24 @@ package org.gusdb.wdk.session; -import org.apache.log4j.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + import org.gusdb.oauth2.client.OAuthClient; -import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; import org.gusdb.oauth2.client.OAuthConfig; -import org.gusdb.oauth2.shared.token.IdTokenFields; +import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.user.RegisteredUser; -import org.gusdb.wdk.model.user.User; -import org.gusdb.wdk.model.user.UserFactory; +import org.json.JSONArray; import org.json.JSONObject; -import io.jsonwebtoken.Claims; - public class WdkOAuthClientWrapper { - private static final Logger LOG = Logger.getLogger(WdkOAuthClientWrapper.class); - - private final WdkModel _wdkModel; private final OAuthConfig _config; private final OAuthClient _client; - public WdkOAuthClientWrapper(WdkModel wdkModel) throws WdkModelException { - try { - _wdkModel = wdkModel; - _config = wdkModel.getModelConfig(); - _client = new OAuthClient(OAuthClient.getTrustManager(wdkModel.getModelConfig())); - } - catch (Exception e) { - throw new WdkModelException("Unable to instantiate OAuthClient from config", e); - } - } - - public ValidatedToken getIdTokenFromAuthCode(String authCode, String redirectUri) { - return _client.getIdTokenFromAuthCode(_config, authCode, redirectUri); - } - - public ValidatedToken getIdTokenFromCredentials(String email, String password, String redirectUrl) { - return _client.getIdTokenFromUsernamePassword(_config, email, password, redirectUrl); + public WdkOAuthClientWrapper(WdkModel wdkModel) { + _config = wdkModel.getModelConfig(); + _client = new OAuthClient(OAuthClient.getTrustManager(wdkModel.getModelConfig())); } public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) { @@ -49,46 +29,25 @@ public ValidatedToken getBearerTokenFromCredentials(String email, String passwor return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); } - public User getUserFromValidatedToken(ValidatedToken token, UserFactory userFactory) { - Claims claims = token.getTokenContents(); - - User user = new RegisteredUser(_wdkModel, - Long.parseLong(claims.getSubject()), - claims.get(IdTokenFields.email.name(), String.class), - claims.get(IdTokenFields.signature.name(), String.class), - claims.get(IdTokenFields.preferred_username.name(), String.class)); - - // have to hit the user data endpoint for this for user details; bearer token does not include all fields - JSONObject userData = _client.getUserData(_config, token); + public ValidatedToken validateBearerToken(String rawToken) { + return _client.getValidatedEcdsaSignedToken(_config.getOauthUrl(), rawToken); + } - // user UserFactory to create user from the JSON object, adding to DB if necessary for FKs on other userlogins5 tables - User user = new UserFactory(wdkModel). - //long userId = client.getUserIdFromAuthCode(authCode, appUrl); - //User newUser = wdkModel.getUserFactory().login(userId); - if (newUser == null) { - throw new WdkModelException("Unable to find user with ID " + userId + - ", returned by OAuth service with auth code " + authCode); - } + public JSONObject getUserData(ValidatedToken token) { + return _client.getUserData(_config.getOauthUrl(), token); } - /* Leaving here temporarily as a reference - private static long getUserIdFromIdToken(String idToken, String clientSecret) throws WdkModelException { - try { - LOG.debug("Attempting parse of id token [" + idToken + "] using client secret '" + clientSecret +"'"); - String encodedKey = TextCodec.BASE64.encode(clientSecret); - Claims claims = Jwts.parser().setSigningKey(encodedKey).parseClaimsJws(idToken).getBody(); - // TODO: verify additional claims for security - String userIdStr = claims.getSubject(); - LOG.debug("Received token for sub '" + userIdStr + "' and preferred_username '" + claims.get("preferred_username")); - if (FormatUtil.isInteger(userIdStr)) { - return Long.valueOf(userIdStr); - } - throw new WdkModelException("Subject returned by OAuth server [" + userIdStr + "] is not a valid user ID."); - } - catch (JSONException e) { - throw new WdkModelException("JWT body returned is not a valid JSON object."); + public List getUserData(List userIds) { + List stringIds = userIds.stream().map(String::valueOf).collect(Collectors.toList()); + JSONArray usersJson = _client.getUserData(_config, stringIds); + List users = new ArrayList<>(); + for (int i = 0; i < usersJson.length(); i++) { + users.add(usersJson.getJSONObject(i)); } + return users; + } + public ValidatedToken getNewGuestToken() { + return _client.getNewGuestToken(_config); } - */ } diff --git a/Model/src/test/java/org/gusdb/wdk/model/user/UserCreationScriptTest.java b/Model/src/test/java/org/gusdb/wdk/model/user/UserCreationScriptTest.java deleted file mode 100644 index 3fece5e659..0000000000 --- a/Model/src/test/java/org/gusdb/wdk/model/user/UserCreationScriptTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.gusdb.wdk.model.user; - -import java.util.Arrays; -import java.util.List; - -import org.gusdb.fgputil.accountdb.UserPropertyName; -import org.gusdb.wdk.model.user.UserCreationScript.UserLine; -import org.junit.Test; - -public class UserCreationScriptTest { - - private static final String[] TEST_CASES = { - "\ta\tb\tc\td", - "email\ta\t\tc\td", - "email\t\tb\tc\td", - "email\ta\tb\t\t\t\t" - }; - - private static final UserPropertyName[] USER_PROPS = { - new UserPropertyName("firstName", null, true), - new UserPropertyName("middleName", null, false), - new UserPropertyName("lastName", null, true), - new UserPropertyName("organization", null, true) - }; - - @Test - public void testParsing() { - List userProps = Arrays.asList(USER_PROPS); - for (String testCase : TEST_CASES) { - UserLine userLine = UserCreationScript.parseLine(testCase, userProps); - System.out.println(userLine); - } - } -} diff --git a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java index d4ea31b860..0358b6db1b 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java @@ -1,8 +1,6 @@ package org.gusdb.wdk.service.filter; import java.io.IOException; -import java.util.Map; -import java.util.Optional; import javax.annotation.Priority; import javax.inject.Inject; @@ -16,24 +14,42 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedMap; +import org.apache.log4j.Logger; import org.glassfish.grizzly.http.server.Request; import org.gusdb.fgputil.web.ApplicationContext; import org.gusdb.fgputil.web.CookieBuilder; -import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.fgputil.web.RequestData; +import org.gusdb.oauth2.client.OAuthClient; +import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.wdk.controller.ContextLookup; -import org.gusdb.wdk.controller.filter.CheckLoginFilterShared; +import org.gusdb.wdk.model.Utilities; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.user.BearerTokenUser; +import org.gusdb.wdk.model.user.User; +import org.gusdb.wdk.service.service.SessionService; import org.gusdb.wdk.service.service.SystemService; +import org.gusdb.wdk.session.WdkOAuthClientWrapper; + +import io.prometheus.client.Counter; @Priority(30) public class CheckLoginFilter implements ContainerRequestFilter, ContainerResponseFilter { - public static final String SESSION_COOKIE_TO_SET = "sessionCookieToSet"; + private static final Logger LOG = Logger.getLogger(CheckLoginFilter.class); + + private static final Counter GUEST_CREATION_COUNTER = Counter.build() + .name("wdk_guest_creation_count") + .help("Number of guest users created by WDK services") + .register(); + + private static final String TOKEN_COOKIE_VALUE_TO_SET = "tokenCookieValueToSet"; + + private static final String LEGACY_WDK_LOGIN_COOKIE_NAME = "wdk_check_auth"; @Context - protected - ServletContext _servletContext; + protected ServletContext _servletContext; @Inject protected Provider _servletRequest; @@ -43,23 +59,62 @@ public class CheckLoginFilter implements ContainerRequestFilter, ContainerRespon @Override public void filter(ContainerRequestContext requestContext) throws IOException { - - // skip certain endpoints to avoid a guest user being created for those endpoints + // skip endpoints which do not require a user; prevents guests from being unnecessarily created String requestPath = requestContext.getUriInfo().getPath(); if (isPathToSkip(requestPath)) return; ApplicationContext context = ContextLookup.getApplicationContext(_servletContext); RequestData request = ContextLookup.getRequest(_servletRequest.get(), _grizzlyRequest.get()); + WdkModel wdkModel = ContextLookup.getWdkModel(context); + WdkOAuthClientWrapper oauth = new WdkOAuthClientWrapper(wdkModel); + + // try to find submitted bearer token + String rawToken = findRawBearerToken(request, requestContext); + + try { + ValidatedToken token; + User user; + if (rawToken != null) { + // validate submitted token + token = oauth.validateBearerToken(rawToken); + user = new BearerTokenUser(wdkModel, oauth, token); + + LOG.info("Validated successfully. Request will be processed for user " + user.getUserId() + " / " + user.getEmail()); + } + else { + // no credentials submitted; automatically create a guest to use on this request + token = oauth.getNewGuestToken(); + user = new BearerTokenUser(wdkModel, oauth, token); + + LOG.info("Created new guest user [" + user.getUserId() + "] for request to path: /" + requestPath); + GUEST_CREATION_COUNTER.inc(); + + // set flag indicating that cookies should be added to response containing the new token + requestContext.setProperty(TOKEN_COOKIE_VALUE_TO_SET, token.getTokenValue()); + } + + // insert reference to this user into user DB for tracking and for foreign keys on other user DB tables + wdkModel.getUserFactory().insertUserToUserDb(user); + + // set user on the request object for use by this request's processing + request.setAttribute(Utilities.WDK_USER_KEY, user); + } + catch (Exception e) { + // for now, log and let this go, deferring to legacy authentication + LOG.error("Unable to authenticate with Authorization header " + rawToken, e); + throw e; + } + } - Optional newCookie = - CheckLoginFilterShared.calculateUserActions( - findLoginCookie(requestContext.getCookies()), - ContextLookup.getWdkModel(context), - request.getSession(), requestPath); - - if (newCookie.isPresent()) { - requestContext.setProperty(SESSION_COOKIE_TO_SET, newCookie.get().toJaxRsCookie().toString()); + private String findRawBearerToken(RequestData request, ContainerRequestContext requestContext) { + String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + if (authHeader != null) { + LOG.info("Recieved Authorization header with value: " + authHeader + "; trying bearer token validation."); + return OAuthClient.getTokenFromAuthHeader(authHeader); } + // otherwise try Authorization cookie + Cookie cookie = requestContext.getCookies().get(HttpHeaders.AUTHORIZATION); + return cookie == null ? null : cookie.getValue(); } protected boolean isPathToSkip(String path) { @@ -67,16 +122,24 @@ protected boolean isPathToSkip(String path) { return SystemService.PROMETHEUS_ENDPOINT_PATH.equals(path); } - protected Optional findLoginCookie(Map cookies) { - return Optional.ofNullable(cookies.get(LoginCookieFactory.WDK_LOGIN_COOKIE_NAME)) - .map(cookie -> new CookieBuilder(cookie.getName(), cookie.getValue())); - } - @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { - if (requestContext.getPropertyNames().contains(SESSION_COOKIE_TO_SET)) { - responseContext.getHeaders().add(HttpHeaders.SET_COOKIE, requestContext.getProperty(SESSION_COOKIE_TO_SET)); + MultivaluedMap headers = responseContext.getHeaders(); + if (requestContext.getPropertyNames().contains(TOKEN_COOKIE_VALUE_TO_SET)) { + String tokenValue = (String)requestContext.getProperty(TOKEN_COOKIE_VALUE_TO_SET); + + // set cookie value for both Authorization cookie + CookieBuilder cookie = new CookieBuilder(HttpHeaders.AUTHORIZATION, tokenValue); + cookie.setMaxAge(SessionService.EXPIRATION_3_YEARS_SECS); + cookie.setPath("/"); + headers.add(HttpHeaders.SET_COOKIE, cookie.toJaxRsCookie().toString()); } + + // unset legacy WDK check auth cookie in case it is present + CookieBuilder cookie = new CookieBuilder(LEGACY_WDK_LOGIN_COOKIE_NAME, ""); + cookie.setMaxAge(0); + cookie.setPath("/"); + headers.add(HttpHeaders.SET_COOKIE, cookie.toJaxRsCookie().toString()); } } diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java index 466fcae614..ac12e63a1e 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java @@ -1,9 +1,12 @@ package org.gusdb.wdk.service.formatter; +import java.util.Optional; + import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.config.ModelConfig; +import org.gusdb.wdk.model.user.User; import org.json.JSONArray; import org.json.JSONObject; @@ -47,7 +50,7 @@ public static JSONObject getWdkProjectInfo(WdkModel wdkModel, String serviceEndp // create profile property config sub-array JSONArray userProfileProps = new JSONArray(); - for (UserPropertyName prop : wdkModel.getModelConfig().getAccountDB().getUserPropertyNames()) { + for (UserPropertyName prop : User.USER_PROPERTIES.values()) { userProfileProps.put(new JSONObject() .put(JsonKeys.NAME, prop.getName()) .put(JsonKeys.DISPLAY_NAME, prop.getDisplayName()) @@ -57,8 +60,7 @@ public static JSONObject getWdkProjectInfo(WdkModel wdkModel, String serviceEndp } return new JSONObject() - .put(JsonKeys.DESCRIPTION, wdkModel.getIntroduction() == null ? - WELCOME_MESSAGE : wdkModel.getIntroduction()) + .put(JsonKeys.DESCRIPTION, Optional.ofNullable(wdkModel.getIntroduction()).orElse(WELCOME_MESSAGE)) .put(JsonKeys.DISPLAY_NAME, wdkModel.getDisplayName()) .put(JsonKeys.PROJECT_ID, wdkModel.getProjectId()) .put(JsonKeys.BUILD_NUMBER, wdkModel.getBuildNumber()) diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/StrategyFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/StrategyFormatter.java index 4eac4458b1..9d27a70947 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/StrategyFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/StrategyFormatter.java @@ -45,7 +45,7 @@ private static JSONObject getListingStrategyJson(Strategy strategy, boolean incl .put(JsonKeys.IS_VALID, strategy.isValid()) .put(JsonKeys.IS_DELETED, strategy.isDeleted()) .put(JsonKeys.IS_EXAMPLE, strategy.isExample()) - .put(JsonKeys.ORGANIZATION, strategy.getUser().getProfileProperties().get("organization")) + .put(JsonKeys.ORGANIZATION, strategy.getUser().getOrganization()) .put(JsonKeys.ESTIMATED_SIZE, StepFormatter.translateEstimatedSize(strategy.getEstimatedSize())) .put(JsonKeys.NAME_OF_FIRST_STEP, strategy.getMostPrimaryLeafStep().getDisplayName()) .put(JsonKeys.LEAF_AND_TRANSFORM_STEP_COUNT, strategy.getLeafAndTransformStepCount()) diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java index 5d93a7d360..96f4e0a381 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java @@ -1,13 +1,11 @@ package org.gusdb.wdk.service.formatter; -import java.util.List; -import java.util.Map; import java.util.Optional; -import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserPreferences; +import org.gusdb.wdk.model.user.WdkUserProperty; import org.json.JSONException; import org.json.JSONObject; @@ -30,26 +28,26 @@ public class UserFormatter { public static JSONObject getUserJson(User user, boolean isOwner, - Optional userPreferences, List propDefs) throws JSONException { + Optional userPreferences) throws JSONException { JSONObject json = new JSONObject() .put(JsonKeys.ID, user.getUserId()) .put(JsonKeys.IS_GUEST, user.isGuest()); // private fields viewable only by owner if (isOwner) { json.put(JsonKeys.EMAIL, user.getEmail()); - json.put(JsonKeys.PROPERTIES, getPropertiesJson(user.getProfileProperties(), propDefs, isOwner)); + json.put(JsonKeys.PROPERTIES, getPropertiesJson(user, isOwner)); userPreferences.ifPresent(prefs -> json.put(JsonKeys.PREFERENCES, getPreferencesJson(prefs))); } return json; } - private static JSONObject getPropertiesJson(Map props, List propDefs, boolean isOwner) { + private static JSONObject getPropertiesJson(User user, boolean isOwner) { JSONObject propsJson = new JSONObject(); - for (UserPropertyName definedProperty : propDefs) { + for (WdkUserProperty definedProperty : User.USER_PROPERTIES.values()) { if (isOwner || definedProperty.isPublic()) { String key = definedProperty.getName(); - String value = (props.containsKey(key) ? props.get(key) : ""); + String value = Optional.ofNullable(definedProperty.getValue(user)).orElse(""); propsJson.put(key, value); } } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java index 392b132dd9..473840151c 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java @@ -38,7 +38,6 @@ import org.gusdb.wdk.model.record.RecordClass; import org.gusdb.wdk.model.record.attribute.AttributeField; import org.gusdb.wdk.model.record.attribute.AttributeFieldContainer; -import org.gusdb.wdk.model.user.UnregisteredUser.UnregisteredUserType; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.service.UserBundle; @@ -99,10 +98,6 @@ public static String paramToSegment(String pathParamName) { private WdkModel _testWdkModel; - // used to cache a guest user ONLY if one is not present in the session - // NOTE: this is a temporary hack to support Grizzly use of the WDK service - private User _cachedSessionUser; - // public setter for unit tests public void testSetup(WdkModel wdkModel) { _testWdkModel = wdkModel; @@ -145,23 +140,18 @@ protected SessionProxy getSession() { return getRequest().getSession(); } - protected User getSessionUser() { - User user = (User) getRequest().getSession().getAttribute(Utilities.WDK_USER_KEY); + protected User getRequestingUser() { + User user = (User) getRequest().getAttributeMap().get(Utilities.WDK_USER_KEY); if (user != null) { // NOTE: user should ALWAYS be non-null in servlet containers with CheckLoginFilter active return user; } - // used to cache a guest user ONLY if one is not present in the session - // NOTE: this is a temporary hack to support Grizzly use of the WDK service - if (_cachedSessionUser == null) { - _cachedSessionUser = getWdkModel().getUserFactory().createUnregistedUser(UnregisteredUserType.GUEST); - } - return _cachedSessionUser; + throw new IllegalStateException("No user present on request."); } protected boolean isSessionUserAdmin() { List adminEmails = getWdkModel().getModelConfig().getAdminEmails(); - return adminEmails.contains(getSessionUser().getEmail()); + return adminEmails.contains(getRequestingUser().getEmail()); } protected void assertAdmin() { @@ -178,7 +168,7 @@ protected void assertAdmin() { * @throws WdkModelException if error occurs while accessing user data (probably a DB problem) */ protected UserBundle parseTargetUserId(String userIdStr) throws WdkModelException { - return UserBundle.createFromTargetId(userIdStr, getSessionUser(), getWdkModel().getUserFactory(), isSessionUserAdmin()); + return UserBundle.createFromTargetId(userIdStr, getRequestingUser(), getWdkModel().getUserFactory(), isSessionUserAdmin()); } /** diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/AnswerService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AnswerService.java index 74497498be..9010d70ce5 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/AnswerService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/AnswerService.java @@ -193,8 +193,8 @@ public Response createStandardReportAnswerFromForm(@FormParam("data") String dat public Response createCustomReportAnswer(@PathParam(REPORT_NAME_PATH_PARAM) String reportName, JSONObject body) throws WdkModelException, DataValidationException, RequestMisformatException { AnswerRequest request = parseAnswerRequest(getQuestionOrNotFound(_recordClassUrlSegment, _questionUrlSegment), reportName, - body, getWdkModel(), getSessionUser(), _avoidCacheHit); - return getAnswerResponse(getSessionUser(), request).getSecond(); + body, getWdkModel(), getRequestingUser(), _avoidCacheHit); + return getAnswerResponse(getRequestingUser(), request).getSecond(); } /** diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java index 4daf4acee7..fc80bf44be 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/QuestionService.java @@ -126,7 +126,7 @@ public JSONObject getQuestionNew( @PathParam(SEARCH_PATH_PARAM) String questionUrlSegment) throws WdkModelException { DisplayablyValid validSpec = getDisplayableAnswerSpec( - questionUrlSegment, getWdkModel(), getSessionUser(), name -> getQuestionOrNotFound(_recordClassUrlSegment, name)); + questionUrlSegment, getWdkModel(), getRequestingUser(), name -> getQuestionOrNotFound(_recordClassUrlSegment, name)); JSONObject result = QuestionFormatter.getQuestionJsonWithParams(validSpec, validSpec.get().getValidationBundle()); if (LOG.isDebugEnabled()) LOG.debug("Returning JSON: " + result.toString(2)); return result; @@ -187,7 +187,7 @@ public JSONObject getQuestionRevise( .setQuestionFullName(question.getFullName()) .setParamValues(request.getContextParamValues()) .build( - getSessionUser(), + getRequestingUser(), StepContainer.emptyContainer(), ValidationLevel.SEMANTIC, FillStrategy.NO_FILL); @@ -202,7 +202,7 @@ public JSONObject getQuestionRevise( .setQuestionFullName(question.getFullName()) .setParamValues(request.getContextParamValues()) .build( - getSessionUser(), + getRequestingUser(), StepContainer.emptyContainer(), ValidationLevel.DISPLAYABLE, FillStrategy.FILL_PARAM_IF_MISSING_OR_INVALID) @@ -258,7 +258,7 @@ public JSONArray getQuestionChange(@PathParam(SEARCH_PATH_PARAM) String question .setQuestionFullName(question.getFullName()) .setParamValues(contextParams) .build( - getSessionUser(), + getRequestingUser(), StepContainer.emptyContainer(), ValidationLevel.DISPLAYABLE, FillStrategy.FILL_PARAM_IF_MISSING_OR_INVALID) @@ -337,7 +337,7 @@ public JSONObject getFilterParamOntologyTermSummary( .builder() .putAll(contextParamValues) .buildValidated( - getSessionUser(), + getRequestingUser(), question.getQuery(), StepContainer.emptyContainer(), ValidationLevel.DISPLAYABLE, @@ -392,7 +392,7 @@ public JSONObject getFilterParamSummaryCounts( .builder() .putAll(contextParamValues) .buildValidated( - getSessionUser(), + getRequestingUser(), question.getQuery(), StepContainer.emptyContainer(), ValidationLevel.DISPLAYABLE, diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java b/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java index c322e3e76e..cb66df3959 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/RecordService.java @@ -111,7 +111,7 @@ public Response buildResult(@PathParam(RECORD_TYPE_PATH_PARAM) String recordClas RecordRequest request = RecordRequest.createFromJson(recordClass, new JSONObject(body)); // fetch a list of record instances the passed primary key maps to - List records = RecordClass.getRecordInstances(getSessionUser(), request.getPrimaryKey()); + List records = RecordClass.getRecordInstances(getRequestingUser(), request.getPrimaryKey()); // if no mapping exists, return not found if (records.isEmpty()) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 1172ac2144..4544ed0cfd 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -24,18 +24,16 @@ import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; -import org.gusdb.fgputil.web.LoginCookieFactory.LoginCookieParts; import org.gusdb.fgputil.web.SessionProxy; -import org.gusdb.oauth2.client.OAuthClient.ValidatedToken; +import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.wdk.core.api.JsonKeys; -import org.gusdb.wdk.events.NewUserEvent; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.config.ModelConfig; import org.gusdb.wdk.model.config.ModelConfig.AuthenticationMethod; -import org.gusdb.wdk.model.user.UnregisteredUser.UnregisteredUserType; +import org.gusdb.wdk.model.user.BearerTokenUser; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.service.request.LoginRequest; import org.gusdb.wdk.service.request.exception.RequestMisformatException; @@ -44,12 +42,14 @@ import org.json.JSONException; import org.json.JSONObject; +import com.google.common.net.HttpHeaders; + @Path("/") public class SessionService extends AbstractWdkService { private static final Logger LOG = Logger.getLogger(SessionService.class); - private static final int EXPIRATION_3_YEARS_SECS = 3 * 365 * 24 * 60 * 60; + public static final int EXPIRATION_3_YEARS_SECS = 3 * 365 * 24 * 60 * 60; private static final String REFERRER_HEADER_KEY = "Referer"; @@ -137,8 +137,8 @@ public Response processOauthLogin( String appUrl = getContextUri(); String redirectUrl = isEmpty(originalUrl) ? appUrl : originalUrl; - // Is the user already logged in? - User oldUser = getSessionUser(); + // Was existing bearer token submitted with this request? + User oldUser = getRequestingUser(); if (!oldUser.isGuest()) { return createRedirectResponse(redirectUrl).build(); } @@ -161,11 +161,11 @@ public Response processOauthLogin( throw new WdkModelException("Unable to log in; state token missing, incorrect, or expired."); } - WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); - // Use auth code to get the bearer token, then convert to User - ValidatedToken bearerToken = client.getBearerTokenFromAuth(authCode, appUrl); - User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); + WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); + ValidatedToken bearerToken = client.getBearerTokenFromAuthCode(authCode, appUrl); + User newUser = new BearerTokenUser(wdkModel, client, bearerToken); + wdkModel.getUserFactory().insertUserToUserDb(newUser); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); @@ -200,18 +200,17 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer String originalUrl = request.getRedirectUrl(); String redirectUrl = !isEmpty(originalUrl) ? originalUrl : !isEmpty(referrer) ? referrer : appUrl; - // Is the user already logged in? - User oldUser = getSessionUser(); + // Was existing bearer token submitted with this request? + User oldUser = getRequestingUser(); if (!oldUser.isGuest()) { return createRedirectResponse(redirectUrl).build(); } - WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); - // Use passed credentials to get the bearer token, then convert to User + WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); ValidatedToken bearerToken = client.getBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); - - User newUser = client.getUserFromValidatedToken(bearerToken, wdkModel.getUserFactory()); + User newUser = new BearerTokenUser(wdkModel, client, bearerToken); + wdkModel.getUserFactory().insertUserToUserDb(newUser); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); @@ -259,22 +258,25 @@ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, Us Events.triggerAndWait(new NewUserEvent(newUser, oldUser, session), new WdkRuntimeException("Unable to complete WDK user assignement.")); - // TODO: leaving legacy code here for reference; remove when deemed appropriate - //LoginCookieFactory baker = new LoginCookieFactory(getWdkModel().getModelConfig().getSecretKey()); - //CookieBuilder loginCookie = baker.createLoginCookie(newUser.getEmail()); + // TODO: until client is updated, must still return WDK login cookie + LoginCookieFactory baker = new LoginCookieFactory(getWdkModel().getModelConfig().getSecretKey()); + CookieBuilder loginCookie = baker.createLoginCookie(newUser.getEmail()); + loginCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); // 3-year expiration (should change secret key before then) - CookieBuilder loginCookie = new CookieBuilder( - LoginCookieFactory.WDK_LOGIN_COOKIE_NAME, + CookieBuilder bearerTokenCookie = new CookieBuilder( + HttpHeaders.AUTHORIZATION, bearerToken.getTokenValue()); - loginCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); + bearerTokenCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); - redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, loginCookie); + redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, loginCookie, bearerTokenCookie); return (isRedirectResponse ? createRedirectResponse(redirectUrl) : createJsonResponse(true, null, redirectUrl) - ).cookie(loginCookie.toJaxRsCookie()).build(); + ) + .cookie(loginCookie.toJaxRsCookie(), bearerTokenCookie.toJaxRsCookie()) + .build(); } } @@ -286,7 +288,7 @@ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, Us * @param cookie login cookie to be sent to the browser * @return page user should be redirected to after successful login */ - protected String getSuccessRedirectUrl(String redirectUrl, User user, CookieBuilder cookie) { + protected String getSuccessRedirectUrl(String redirectUrl, User user, CookieBuilder cookie, CookieBuilder bearerTokenCookie) { return redirectUrl; } @@ -295,12 +297,12 @@ protected String getSuccessRedirectUrl(String redirectUrl, User user, CookieBuil public Response processLogout() throws WdkModelException { // get the current session's user, then invalidate the session - User oldUser = getSessionUser(); + User oldUser = getRequestingUser(); getSession().invalidate(); // get a new session and add new guest user to it SessionProxy session = getSession(); - User newUser = getWdkModel().getUserFactory().createUnregistedUser(UnregisteredUserType.GUEST); + User newUser = getWdkModel().getUserFactory().createUnregistedUser(); session.setAttribute(Utilities.WDK_USER_KEY, newUser); // throw new user event diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryResultService.java b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryResultService.java index 850839f246..c9ed3d22d4 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryResultService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryResultService.java @@ -71,7 +71,7 @@ public class TemporaryResultService extends AbstractWdkService { public Response setTemporaryResult(JSONObject requestJson) throws RequestMisformatException, DataValidationException, WdkModelException, ValidationException { AnswerRequest request = parseRequest(requestJson); - String id = TemporaryResultFactory.insertTemporaryResult(getSessionUser().getUserId(), request); + String id = TemporaryResultFactory.insertTemporaryResult(getRequestingUser().getUserId(), request); return Response.ok(new JSONObject().put(ID, id).toString()) .location(getUriInfo().getAbsolutePathBuilder().build(id)).build(); } @@ -109,7 +109,7 @@ private AnswerRequest parseRequest(JSONObject requestJson) .getStepFactory() .getStepById(stepId, ValidationLevel.RUNNABLE) .orElseThrow(() -> new DataValidationException("No step found with ID " + stepId)); - if (step.getUser().getUserId() != getSessionUser().getUserId()) { + if (step.getUser().getUserId() != getRequestingUser().getUserId()) { throw new DataValidationException("No step found with ID " + stepId + " for this user."); } RunnableObj answerSpec = Step.getRunnableAnswerSpec(step.getRunnable() @@ -133,7 +133,7 @@ else if (requestJson.has(SEARCH_NAME) && requestJson.has(SEARCH_CONFIG)){ // parse answer request as we would in answer service return AnswerService.parseAnswerRequest( - question, reporterName, requestJson, getWdkModel(), getSessionUser(), false); + question, reporterName, requestJson, getWdkModel(), getRequestingUser(), false); } else { throw new RequestMisformatException("Either " + STEP_ID + " or (" + diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/search/ColumnReporterService.java b/Service/src/main/java/org/gusdb/wdk/service/service/search/ColumnReporterService.java index 18ea93b8b5..dc2ce835df 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/search/ColumnReporterService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/search/ColumnReporterService.java @@ -144,7 +144,7 @@ public StreamingOutput runReporter( requestJson.getJSONObject(JsonKeys.SEARCH_CONFIG), toolName, viewFilters); // build answer value - AnswerValue answerValue = AnswerValueFactory.makeAnswer(getSessionUser(), answerSpec); + AnswerValue answerValue = AnswerValueFactory.makeAnswer(getRequestingUser(), answerSpec); // finish configuring the reporter reporter @@ -208,7 +208,7 @@ private RunnableObj makeAnswerSpec(JSONObject answerSpecJson, String // build the spec and return return specBuilder - .build(getSessionUser(), emptyContainer(), RUNNABLE, FILL_PARAM_IF_MISSING) + .build(getRequestingUser(), emptyContainer(), RUNNABLE, FILL_PARAM_IF_MISSING) .getRunnable() .getOrThrow(spec -> new DataValidationException(spec.getValidationBundle())); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/BasketService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/BasketService.java index 67fc3dfb23..8c8bf99265 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/BasketService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/BasketService.java @@ -283,7 +283,7 @@ public Response createCustomReportAnswer( .builder(getWdkModel()) .setQuestionFullName(recordClass.getRealtimeBasketQuestion().getFullName()) .setViewFilterOptions(AnswerSpecServiceFormat.parseViewFilters(requestJson)) - .buildRunnable(getSessionUser(), StepContainer.emptyContainer()); + .buildRunnable(getRequestingUser(), StepContainer.emptyContainer()); AnswerRequest request = new AnswerRequest(basketAnswerSpec, new AnswerFormatting(reportName, requestJson.getJSONObject(JsonKeys.REPORT_CONFIG)), false); return AnswerService.getAnswerResponse(user, request).getSecond(); @@ -339,7 +339,7 @@ public StreamingOutput getColumnReporterResponse( .builder(getWdkModel()) .setQuestionFullName(recordClass.getRealtimeBasketQuestion().getFullName()) .setViewFilterOptions(AnswerSpecServiceFormat.parseViewFilters(requestJson)) - .buildRunnable(getSessionUser(), StepContainer.emptyContainer()); + .buildRunnable(getRequestingUser(), StepContainer.emptyContainer()); // build and configure the column reporter and stream its result return AnswerService.getAnswerAsStream( diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index b09ad6171e..48fd2ef25d 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -51,10 +51,8 @@ public ProfileService(@PathParam(USER_ID_PATH_PARAM) String uid) { // @OutSchema("wdk.users.get-by-id") public JSONObject getById(@QueryParam("includePreferences") Boolean includePreferences) throws WdkModelException { UserBundle userBundle = getUserBundle(Access.PUBLIC); - List propDefs = getWdkModel().getModelConfig() - .getAccountDB().getUserPropertyNames(); return formatUser(userBundle.getTargetUser(), userBundle.isSessionUser(), - getFlag(includePreferences), propDefs); + getFlag(includePreferences)); } /** @@ -67,10 +65,10 @@ public JSONObject getById(@QueryParam("includePreferences") Boolean includePrefe * @return formatted user object * @throws WdkModelException if something goes wrong */ - protected JSONObject formatUser(User user, boolean isSessionUser, boolean includePrefs, List propNames) throws WdkModelException { + protected JSONObject formatUser(User user, boolean isSessionUser, boolean includePrefs) throws WdkModelException { Optional userPrefs = !includePrefs ? Optional.empty() : Optional.of(new UserPreferenceFactory(getWdkModel()).getPreferences(user.getUserId())); - return UserFormatter.getUserJson(user, isSessionUser, userPrefs, propNames); + return UserFormatter.getUserJson(user, isSessionUser, userPrefs); } /** @@ -151,10 +149,6 @@ private Response getProfileUpdateResponse(NewCookie loginCookie) { Response.noContent().cookie(loginCookie).build(); } - private List getPropertiesConfig() { - return getWdkModel().getModelConfig().getAccountDB().getUserPropertyNames(); - } - @PUT @Path("password") @Consumes(MediaType.APPLICATION_JSON) diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/StrategyService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/StrategyService.java index d6f2957aa8..f4f04ac2b1 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/StrategyService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/StrategyService.java @@ -287,7 +287,7 @@ public JSONObject duplicateAsBranch(@PathParam(ID_PARAM) long stratId) return new JSONObject().put(JsonKeys.STEP_TREE, StepFormatter.formatAsStepTree( getWdkModel().getStepFactory().copyStrategyToBranch( - getSessionUser(), + getRequestingUser(), getStrategyForCurrentUser(stratId, ValidationLevel.NONE) ), new HashSet() // we don't need to consume the list of step IDs found in the tree @@ -433,7 +433,7 @@ private Strategy applyOverwriteChanges(Strategy strategyToBeOverwritten, long so // copy the source strat's step tree into new, unattached steps JSONObject copiedStepTree = StepFormatter.formatAsStepTree( - getWdkModel().getStepFactory().copyStrategyToBranch(getSessionUser(),sourceStrategy), + getWdkModel().getStepFactory().copyStrategyToBranch(getRequestingUser(),sourceStrategy), new HashSet() // we don't need to consume the list of step IDs found in the tree ); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserDatasetService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserDatasetService.java index f7720d5230..92d3e8d0a3 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserDatasetService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserDatasetService.java @@ -299,7 +299,7 @@ private long parseLongId(String idStr, RuntimeException exception) { */ private void validateTargetUserIds(Set targetUserIds) throws WdkModelException { for(Long targetUserId : targetUserIds) { - UserBundle targetUserBundle = UserBundle.createFromTargetId(targetUserId.toString(), getSessionUser(), getWdkModel().getUserFactory(), isSessionUserAdmin()); + UserBundle targetUserBundle = UserBundle.createFromTargetId(targetUserId.toString(), getRequestingUser(), getWdkModel().getUserFactory(), isSessionUserAdmin()); if (!targetUserBundle.isValidUserId()) { LOG.error("This user dataset share service request contains the following invalid user: " + targetUserId); throw new NotFoundException(formatNotFound(UserService.USER_RESOURCE + targetUserBundle.getTargetUserIdString())); From 23c59fc7e51a04fd8b285c254ca9a8c25d2b64f1 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Thu, 8 Feb 2024 00:00:14 -0500 Subject: [PATCH 10/25] checkpoint commit --- .../conifer/templates/WDK/model-config.xml.j2 | 37 --- .../org/gusdb/wdk/jmx/mbeans/ModelConfig.java | 2 - .../org/gusdb/wdk/model/ModelXmlParser.java | 3 - .../java/org/gusdb/wdk/model/Utilities.java | 96 ++---- .../java/org/gusdb/wdk/model/WdkModel.java | 20 +- .../gusdb/wdk/model/WdkSqlScriptRunner.java | 5 +- .../gusdb/wdk/model/answer/AnswerValue.java | 2 +- .../gusdb/wdk/model/answer/TransformUtil.java | 2 +- .../gusdb/wdk/model/config/ModelConfig.java | 53 +--- .../model/config/ModelConfigAccountDB.java | 16 - .../wdk/model/config/ModelConfigBuilder.java | 11 - .../wdk/model/config/ModelConfigParser.java | 3 - .../model/query/param/AbstractEnumParam.java | 1 - .../query/param/FilterParamNewHandler.java | 14 +- .../wdk/model/test/ParamValuesFactory.java | 2 +- .../org/gusdb/wdk/model/test/QueryTester.java | 2 +- .../gusdb/wdk/model/test/RecordTester.java | 2 +- .../gusdb/wdk/model/test/SummaryTester.java | 2 +- .../model/test/sanity/tests/QuestionTest.java | 2 +- .../org/gusdb/wdk/model/user/BasicUser.java | 31 ++ .../gusdb/wdk/model/user/BearerTokenUser.java | 43 +-- .../java/org/gusdb/wdk/model/user/User.java | 201 +------------ .../org/gusdb/wdk/model/user/UserFactory.java | 280 +++++++----------- .../gusdb/wdk/model/user/WdkUserProperty.java | 32 -- .../wdk/session/WdkOAuthClientWrapper.java | 24 ++ .../wdk/service/filter/CheckLoginFilter.java | 24 +- .../service/formatter/ProjectFormatter.java | 3 +- .../wdk/service/formatter/UserFormatter.java | 4 +- .../request/user/UserCreationRequest.java | 4 +- .../request/user/UserDatasetShareRequest.java | 2 - .../request/user/UserProfileRequest.java | 20 +- .../wdk/service/service/SessionService.java | 21 +- .../service/service/user/ProfileService.java | 32 +- .../wdk/service/service/user/UserService.java | 2 +- .../service/user/UserUtilityServices.java | 5 +- 35 files changed, 270 insertions(+), 733 deletions(-) delete mode 100644 Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java create mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java diff --git a/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 b/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 index e831a0dcc4..ebb2626861 100644 --- a/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 +++ b/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 @@ -29,9 +29,6 @@ site_vars file: {{ site_vars }} {% if modelconfig_useWeights is defined -%} assetsUrl="{{ modelconfig_useWeights }}" {% endif -%} - {% if modelconfig_secretKeyFile is defined -%} - secretKeyFile="{{ modelconfig_secretKeyFile }}" - {% endif -%} {% if modelconfig_blockedThreshold is defined -%} monitorBlockedThreads="{{ modelconfig_monitorBlockedThreads }}" {% endif -%} @@ -145,40 +142,6 @@ site_vars file: {{ site_vars }} {% endif -%} /> - - {% if modelconfig_userDatasetStoreConfig is defined -%} {{ modelconfig_userDatasetStoreConfig|indent }} {% endif -%} diff --git a/Model/src/main/java/org/gusdb/wdk/jmx/mbeans/ModelConfig.java b/Model/src/main/java/org/gusdb/wdk/jmx/mbeans/ModelConfig.java index d4ce7a0a72..b47b5abac6 100644 --- a/Model/src/main/java/org/gusdb/wdk/jmx/mbeans/ModelConfig.java +++ b/Model/src/main/java/org/gusdb/wdk/jmx/mbeans/ModelConfig.java @@ -24,7 +24,6 @@ protected void init() { org.gusdb.wdk.model.config.QueryMonitor queryMonitor = modelConfig.getQueryMonitor(); org.gusdb.wdk.model.config.ModelConfigUserDB modelConfigUserDB = modelConfig.getUserDB(); org.gusdb.wdk.model.config.ModelConfigAppDB modelConfigAppDB = modelConfig.getAppDB(); - org.gusdb.wdk.model.config.ModelConfigAccountDB modelConfigAccountDB = modelConfig.getAccountDB(); org.gusdb.wdk.model.config.ModelConfigUserDatasetStore modelConfigUserDatasetStore = modelConfig.getUserDatasetStoreConfig(); @@ -32,7 +31,6 @@ protected void init() { setValuesFromGetters("queryMonitor", queryMonitor); setValuesFromGetters("userDb", modelConfigUserDB); setValuesFromGetters("appDb", modelConfigAppDB); - setValuesFromGetters("accountDb", modelConfigAccountDB); setValuesFromGetters("userDatasetStore", modelConfigUserDatasetStore); } diff --git a/Model/src/main/java/org/gusdb/wdk/model/ModelXmlParser.java b/Model/src/main/java/org/gusdb/wdk/model/ModelXmlParser.java index 7f595e505b..c71218bf23 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/ModelXmlParser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/ModelXmlParser.java @@ -436,9 +436,6 @@ private Map loadProperties(String projectId, URL modelPropURL, M if (!propMap.containsKey("USER_SCHEMA")) { propMap.put("USER_SCHEMA", config.getUserDB().getUserSchema()); } - if (!propMap.containsKey("ACCT_SCHEMA")) { - propMap.put("ACCT_SCHEMA", config.getAccountDB().getAccountSchema()); - } return propMap; } diff --git a/Model/src/main/java/org/gusdb/wdk/model/Utilities.java b/Model/src/main/java/org/gusdb/wdk/model/Utilities.java index c5c22e1dc5..921c68038f 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/Utilities.java +++ b/Model/src/main/java/org/gusdb/wdk/model/Utilities.java @@ -1,17 +1,11 @@ package org.gusdb.wdk.model; -import org.apache.log4j.Logger; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import javax.activation.DataHandler; -import javax.mail.*; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.sql.Clob; import java.sql.SQLException; import java.util.Date; @@ -22,6 +16,20 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import javax.activation.DataHandler; +import javax.mail.Address; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +import org.apache.log4j.Logger; + /** * This class provided constants that are shared among different WDK model * classes. Furthermore, it also provides utility functions to send @@ -31,16 +39,10 @@ */ public class Utilities { - private static final Logger logger = Logger.getLogger(Utilities.class); + private static final Logger LOG = Logger.getLogger(Utilities.class); public static final int TRUNCATE_DEFAULT = 100; - /** - * The maximum size for parameter values that will be displayed in thr URL as - * plain values - */ - public static final int MAX_PARAM_VALUE_SIZE = 100; - /** * The maximum number of attributes used in sorting an answer */ @@ -56,10 +58,6 @@ public class Utilities { */ public static final String SYSTEM_PROPERTY_GUS_HOME = "GUS_HOME"; - public static final int MAXIMUM_RECORD_INSTANCES = 10000; - - public static final int DEFAULT_PAGE_SIZE = 20; - public static final int DEFAULT_SUMMARY_ATTRIBUTE_SIZE = 6; public static final int DEFAULT_WEIGHT = 10; @@ -71,8 +69,6 @@ public class Utilities { */ public static final int MAX_PK_COLUMN_COUNT = 4; - public static final int MAX_PK_COLUMN_VALUE_SIZE = 1999; - public static final String INTERNAL_PARAM_SET = "InternalParams"; public static final String INTERNAL_QUERY_SET = "InternalQueries"; public static final String INTERNAL_QUESTION_SET = "InternalQuestions"; @@ -99,12 +95,12 @@ public class Utilities { public static final String COLUMN_DIVIDER = ","; public static final String WDK_MODEL_KEY = "wdk_model"; - public static final String WDK_MODEL_BEAN_KEY = "wdkModel"; // cannot change this because of JSPs public static final String WDK_USER_KEY = "wdk_user"; - public static final String WDK_USER_BEAN_KEY = "wdkUser"; + public static final String BEARER_TOKEN_KEY = "bearer-token"; public static final String WDK_SERVICE_ENDPOINT_KEY = "wdkServiceEndpoint"; + /* * Inner class to act as a JAF DataSource to send HTML e-mail content */ @@ -164,22 +160,6 @@ public static String[] toArray(String data) { return data.trim().split("\\s+"); } - public static String fromArray(String[] data) { - return fromArray(data, ","); - } - - public static String fromArray(String[] data, String delimiter) { - if (data == null) - return null; - StringBuffer sb = new StringBuffer(); - for (String value : data) { - if (sb.length() > 0) - sb.append(delimiter); - sb.append(value); - } - return sb.toString(); - } - public static String parseValue(Object objValue) { if (objValue == null) return null; @@ -197,21 +177,6 @@ private static String parseClob(Clob clobValue) { } } - public static String[][] convertContent(String content) throws JSONException { - JSONArray jsResult = new JSONArray(content); - JSONArray jsRow = (JSONArray) jsResult.get(0); - String[][] result = new String[jsResult.length()][jsRow.length()]; - for (int row = 0; row < result.length; row++) { - jsRow = (JSONArray) jsResult.get(row); - for (int col = 0; col < result[row].length; col++) { - Object cell = jsRow.get(col); - result[row][col] = (cell == null || cell == JSONObject.NULL) ? null - : cell.toString(); - } - } - return result; - } - // sendEmail() method overloading: different number of parameters (max 8), different type for attachments // 7 parameters (missing bcc, datahandlers instead of attachments) @@ -258,7 +223,7 @@ public static void sendEmail(String smtpServer, String sendTos, String reply, String subject, String content, String ccAddresses, String bccAddresses, Attachment[] attachments) throws WdkModelException { - logger.debug("Sending message to: " + sendTos + ", bcc to: " + bccAddresses + + LOG.debug("Sending message to: " + sendTos + ", bcc to: " + bccAddresses + ",reply: " + reply + ", using SMPT: " + smtpServer); // create properties and get the session @@ -317,7 +282,6 @@ public static void sendEmail(String smtpServer, String sendTos, String reply, } } - public static byte[] readFile(File file) throws IOException { byte[] buffer = new byte[(int) file.length()]; InputStream stream = new FileInputStream(file); @@ -326,18 +290,6 @@ public static byte[] readFile(File file) throws IOException { return buffer; } - public static int createHashFromValueMap(Map map) { - StringBuilder buffer = new StringBuilder("{"); - for (S key : map.keySet()) { - if (buffer.length() > 1) { - buffer.append(";"); - } - buffer.append(key).append(":").append(map.get(key)); - } - buffer.append("}"); - return buffer.toString().hashCode(); - } - public static Map parseSortList(String sortList) throws WdkModelException { Map sortingMap = new LinkedHashMap(); String[] attrCombines = sortList.split(","); diff --git a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java index bb30e38ac3..a87dbcfb2f 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java +++ b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java @@ -23,7 +23,6 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Collectors; import org.apache.log4j.Logger; @@ -38,13 +37,13 @@ import org.gusdb.fgputil.functional.FunctionalInterfaces.SupplierWithException; import org.gusdb.fgputil.runtime.InstanceManager; import org.gusdb.fgputil.runtime.Manageable; +import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.analysis.StepAnalysis; import org.gusdb.wdk.model.analysis.StepAnalysisPlugins; import org.gusdb.wdk.model.answer.single.SingleRecordQuestion; import org.gusdb.wdk.model.columntool.ColumnToolBundleMap; import org.gusdb.wdk.model.columntool.DefaultColumnToolBundleRef; import org.gusdb.wdk.model.config.ModelConfig; -import org.gusdb.wdk.model.config.ModelConfigAccountDB; import org.gusdb.wdk.model.config.ModelConfigAppDB; import org.gusdb.wdk.model.config.ModelConfigUserDB; import org.gusdb.wdk.model.config.ModelConfigUserDatasetStore; @@ -69,7 +68,6 @@ import org.gusdb.wdk.model.user.BasketFactory; import org.gusdb.wdk.model.user.FavoriteFactory; import org.gusdb.wdk.model.user.StepFactory; -import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.model.user.analysis.StepAnalysisFactory; import org.gusdb.wdk.model.user.analysis.StepAnalysisFactoryImpl; @@ -203,7 +201,6 @@ public static WdkModel construct(String projectId, String gusHome) throws WdkMod private ColumnToolBundleMap columnToolBundleMap = new ColumnToolBundleMap(); private String defaultColumnToolBundleRef; - private ReentrantLock systemUserLock = new ReentrantLock(); private User systemUser; private String buildNumber; @@ -551,14 +548,12 @@ public void configure(ModelConfig modelConfig) throws WdkModelException { ModelConfigAppDB appDbConfig = modelConfig.getAppDB(); ModelConfigUserDB userDbConfig = modelConfig.getUserDB(); - ModelConfigAccountDB accountDbConfig = modelConfig.getAccountDB(); ModelConfigUserDatasetStore udsConfig = modelConfig.getUserDatasetStoreConfig(); QueryLogger.initialize(modelConfig.getQueryMonitor()); appDb = new DatabaseInstance(appDbConfig, DB_INSTANCE_APP, true); userDb = new DatabaseInstance(userDbConfig, DB_INSTANCE_USER, true); - accountDb = new DatabaseInstance(accountDbConfig, DB_INSTANCE_ACCOUNT, true); // set true to avoid a broken dev irods at build time if ( udsConfig == null ) { @@ -599,6 +594,8 @@ public void configure(ModelConfig modelConfig) throws WdkModelException { new UnconfiguredStepAnalysisFactory(this) : new StepAnalysisFactoryImpl(this)); + systemUser = userFactory.createUnregisteredUser().getSecond(); + LOG.info("WDK Model configured."); } @@ -1360,17 +1357,6 @@ public String queryParamDisplayName(String paramName) { } public User getSystemUser() { - if (systemUser == null) { - try { - systemUserLock.lock(); - if (systemUser == null) { - systemUser = userFactory.createUnregistedUser(); - } - } - finally { - systemUserLock.unlock(); - } - } return systemUser; } diff --git a/Model/src/main/java/org/gusdb/wdk/model/WdkSqlScriptRunner.java b/Model/src/main/java/org/gusdb/wdk/model/WdkSqlScriptRunner.java index fc37486c44..8ee52be404 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/WdkSqlScriptRunner.java +++ b/Model/src/main/java/org/gusdb/wdk/model/WdkSqlScriptRunner.java @@ -22,11 +22,11 @@ */ public class WdkSqlScriptRunner { - private static enum DbType { APP, USER, ACCT } + private static enum DbType { APP, USER } public static void main(String[] args) { if (args.length != 3 && args.length != 5) { - System.err.println("USAGE: fgpJava " + WdkSqlScriptRunner.class.getName() + " [APP|USER|ACCT] [ ]"); + System.err.println("USAGE: fgpJava " + WdkSqlScriptRunner.class.getName() + " [APP|USER] [ ]"); System.exit(1); } BufferedReader sqlReader = null; @@ -46,7 +46,6 @@ public static void main(String[] args) { ModelConfigDB dbConfig = ( whichDb.equals(DbType.APP) ? modelConf.getAppDB() : whichDb.equals(DbType.USER) ? modelConf.getUserDB() : - whichDb.equals(DbType.ACCT) ? modelConf.getAccountDB() : null ); // should never happen; value already validated db = new DatabaseInstance(dbConfig); diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java b/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java index 3024bda4f6..7974eb99f4 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/AnswerValue.java @@ -53,9 +53,9 @@ import org.gusdb.wdk.model.record.Field; import org.gusdb.wdk.model.record.PrimaryKeyDefinition; import org.gusdb.wdk.model.record.PrimaryKeyIterator; -import org.gusdb.wdk.model.record.ResultSetPrimaryKeyIterator; import org.gusdb.wdk.model.record.RecordClass; import org.gusdb.wdk.model.record.RecordInstance; +import org.gusdb.wdk.model.record.ResultSetPrimaryKeyIterator; import org.gusdb.wdk.model.record.TableField; import org.gusdb.wdk.model.record.attribute.AttributeField; import org.gusdb.wdk.model.record.attribute.ColumnAttributeField; diff --git a/Model/src/main/java/org/gusdb/wdk/model/answer/TransformUtil.java b/Model/src/main/java/org/gusdb/wdk/model/answer/TransformUtil.java index 88e4ad740c..bbd855c512 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/answer/TransformUtil.java +++ b/Model/src/main/java/org/gusdb/wdk/model/answer/TransformUtil.java @@ -12,10 +12,10 @@ import org.gusdb.wdk.model.query.spec.QueryInstanceSpec; import org.gusdb.wdk.model.question.Question; import org.gusdb.wdk.model.user.Step; +import org.gusdb.wdk.model.user.StepContainer.ListStepContainer; import org.gusdb.wdk.model.user.Strategy; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserCache; -import org.gusdb.wdk.model.user.StepContainer.ListStepContainer; /** * Provides a utility to use transform questions to convert an answer value of diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java index 6409c37e1a..6bb84b1236 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java @@ -1,15 +1,10 @@ package org.gusdb.wdk.model.config; -import java.io.FileReader; -import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Optional; -import org.apache.log4j.Logger; -import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.FormatUtil; -import org.gusdb.fgputil.IoUtil; import org.gusdb.fgputil.Named.NamedObject; import org.gusdb.oauth2.client.KeyStoreTrustManager.KeyStoreConfig; import org.gusdb.oauth2.client.OAuthConfig; @@ -22,8 +17,6 @@ */ public class ModelConfig implements OAuthConfig, KeyStoreConfig { - private static final Logger LOG = Logger.getLogger(ModelConfig.class); - public static final String WSF_LOCAL = "local"; public enum AuthenticationMethod implements NamedObject { @@ -71,7 +64,6 @@ public String getName() { private final ModelConfigUserDB _userDB; private final ModelConfigAppDB _appDB; - private final ModelConfigAccountDB _accountDB; private final ModelConfigUserDatasetStore _userDatasetStoreConfig; @@ -83,17 +75,6 @@ public String getName() { private final String _projectId; private final Path _gusHome; - /** - * location of secret key file - */ - private final Optional _secretKeyFile; - - /** - * cached secret key; value is assigned at construction or as soon as the - * secretKeyFile is present and readable when the secret key is requested - */ - private String _secretKey; - /** * enable/disable weight feature in the steps. default enabled. */ @@ -148,7 +129,7 @@ public String getName() { public ModelConfig(String modelName, String projectId, Path gusHome, boolean caching, boolean useWeights, String paramRegex, Optional secretKeyFile, Path wdkTempDir, String webServiceUrl, String assetsUrl, String smtpServer, String supportEmail, List adminEmails, String emailSubject, - String emailContent, ModelConfigUserDB userDB, ModelConfigAppDB appDB, ModelConfigAccountDB accountDB, + String emailContent, ModelConfigUserDB userDB, ModelConfigAppDB appDB, ModelConfigUserDatasetStore userDatasetStoreConfig, QueryMonitor queryMonitor, boolean monitorBlockedThreads, int blockedThreshold, AuthenticationMethod authenticationMethod, String oauthUrl, String oauthClientId, String oauthClientSecret, String changePasswordUrl, @@ -165,7 +146,6 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac _paramRegex = paramRegex; // file locations - _secretKeyFile = secretKeyFile; _wdkTempDir = wdkTempDir; // network locations @@ -182,7 +162,6 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac // databases _userDB = userDB; _appDB = appDB; - _accountDB = accountDB; // user dataset config _userDatasetStoreConfig = userDatasetStoreConfig; @@ -204,9 +183,6 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac // SSL key store information _keyStoreFile = keyStoreFile; _keyStorePassPhrase = keyStorePassPhrase; - - // get secret key at object creation time if available - getSecretKey(); } /** @@ -295,33 +271,6 @@ public ModelConfigAppDB getAppDB() { return _appDB; } - /** - * @return the accountDB - */ - public ModelConfigAccountDB getAccountDB() { - return _accountDB; - } - - /** - * Returns a cached secret key, generated by encrypting the value in the - * configured secret key file. If the configured filename is null or the contents - * of the file cannot be read for any reason, null is returned. - * - * @return secret key - */ - public String getSecretKey() { - if (_secretKey == null && _secretKeyFile.isPresent()) { - try (FileReader in = new FileReader(_secretKeyFile.get().toFile())) { - _secretKey = EncryptionUtil.md5(IoUtil.readAllChars(in).strip()); - } - catch (IOException e) { - // log error but otherwise ignore so null is returned; problem may be remedied in the future - LOG.warn("Unable to read secret key value from file: " + _secretKeyFile, e); - } - } - return _secretKey; - } - /** * @return configured authentication method */ diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java deleted file mode 100644 index 1b1b621b2f..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.gusdb.wdk.model.config; - -import org.gusdb.fgputil.db.platform.DBPlatform; - -public class ModelConfigAccountDB extends ModelConfigDB { - - private String _accountSchema; - - public void setAccountSchema(String accountSchema) { - _accountSchema = DBPlatform.normalizeSchema(accountSchema); - } - - public String getAccountSchema() { - return _accountSchema; - } -} diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigBuilder.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigBuilder.java index a94f09e3ba..ae414d5906 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigBuilder.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigBuilder.java @@ -45,7 +45,6 @@ public class ModelConfigBuilder { // databases private ModelConfigUserDB _userDB; private ModelConfigAppDB _appDB; - private ModelConfigAccountDB _accountDB; // user dataset config private ModelConfigUserDatasetStore _userDatasetStoreConfig; @@ -91,7 +90,6 @@ public ModelConfig build() throws WdkModelException { assertNonNull("supportEmail", _supportEmail); assertNonNull("userDb", _userDB); assertNonNull("appDb", _appDB); - assertNonNull("accountDb", _accountDB); // TODO: should probably have a default stub for this to avoid NPEs //assertNonNull("userDatasetStoreConfig", _userDatasetStoreConfig); @@ -125,7 +123,6 @@ public ModelConfig build() throws WdkModelException { // databases _userDB, _appDB, - _accountDB, // user dataset config _userDatasetStoreConfig, @@ -248,14 +245,6 @@ public void setAppDB(ModelConfigAppDB appDB) { _appDB = appDB; } - /** - * @param accountDB - * the accountDB to set - */ - public void setAccountDB(ModelConfigAccountDB accountDB) { - _accountDB = accountDB; - } - /** * @param secretKeyFile * the secretKeyFile to set diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java index ee874200ec..9a363d7f6e 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigParser.java @@ -62,9 +62,6 @@ private static Digester configureDigester() { // load user db configureNode(digester, "modelConfig/userDb", ModelConfigUserDB.class, "setUserDB"); - // load user db - configureNode(digester, "modelConfig/accountDb", ModelConfigAccountDB.class, "setAccountDB"); - // userdatasetstore configureNode(digester, "modelConfig/userDatasetStore", ModelConfigUserDatasetStore.class, "setUserDatasetStore"); configureNode(digester, "modelConfig/userDatasetStore/property", WdkModelText.class, "addProperty"); diff --git a/Model/src/main/java/org/gusdb/wdk/model/query/param/AbstractEnumParam.java b/Model/src/main/java/org/gusdb/wdk/model/query/param/AbstractEnumParam.java index 0b26907dc3..dd65553168 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/query/param/AbstractEnumParam.java +++ b/Model/src/main/java/org/gusdb/wdk/model/query/param/AbstractEnumParam.java @@ -5,7 +5,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; - import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/Model/src/main/java/org/gusdb/wdk/model/query/param/FilterParamNewHandler.java b/Model/src/main/java/org/gusdb/wdk/model/query/param/FilterParamNewHandler.java index c76242921f..37147634f0 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/query/param/FilterParamNewHandler.java +++ b/Model/src/main/java/org/gusdb/wdk/model/query/param/FilterParamNewHandler.java @@ -1,19 +1,23 @@ package org.gusdb.wdk.model.query.param; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.MapBuilder; import org.gusdb.fgputil.validation.ValidObjectFactory.RunnableObj; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.query.*; +import org.gusdb.wdk.model.query.Column; +import org.gusdb.wdk.model.query.Query; +import org.gusdb.wdk.model.query.QueryInstance; +import org.gusdb.wdk.model.query.QuerySet; +import org.gusdb.wdk.model.query.SqlQuery; import org.gusdb.wdk.model.query.spec.QueryInstanceSpec; import org.gusdb.wdk.model.user.User; import org.json.JSONException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - /** * @author jerric */ diff --git a/Model/src/main/java/org/gusdb/wdk/model/test/ParamValuesFactory.java b/Model/src/main/java/org/gusdb/wdk/model/test/ParamValuesFactory.java index 06f0e87326..61658146af 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/test/ParamValuesFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/test/ParamValuesFactory.java @@ -15,9 +15,9 @@ import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.query.Query; import org.gusdb.wdk.model.query.param.AbstractDependentParam; +import org.gusdb.wdk.model.query.param.AbstractEnumParam.SelectMode; import org.gusdb.wdk.model.query.param.Param; import org.gusdb.wdk.model.query.param.ParamValuesSet; -import org.gusdb.wdk.model.query.param.AbstractEnumParam.SelectMode; import org.gusdb.wdk.model.user.User; public class ParamValuesFactory { diff --git a/Model/src/main/java/org/gusdb/wdk/model/test/QueryTester.java b/Model/src/main/java/org/gusdb/wdk/model/test/QueryTester.java index 1d1210bd83..f03ae38d83 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/test/QueryTester.java +++ b/Model/src/main/java/org/gusdb/wdk/model/test/QueryTester.java @@ -4,9 +4,9 @@ import java.util.List; import java.util.Map; -import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.OptionGroup; diff --git a/Model/src/main/java/org/gusdb/wdk/model/test/RecordTester.java b/Model/src/main/java/org/gusdb/wdk/model/test/RecordTester.java index ae5f34c53d..daf799317a 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/test/RecordTester.java +++ b/Model/src/main/java/org/gusdb/wdk/model/test/RecordTester.java @@ -3,9 +3,9 @@ import java.util.LinkedHashMap; import java.util.Map; -import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; diff --git a/Model/src/main/java/org/gusdb/wdk/model/test/SummaryTester.java b/Model/src/main/java/org/gusdb/wdk/model/test/SummaryTester.java index 132341d8e1..9eb1024d1c 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/test/SummaryTester.java +++ b/Model/src/main/java/org/gusdb/wdk/model/test/SummaryTester.java @@ -10,9 +10,9 @@ import java.util.Optional; import java.util.Properties; -import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.Option; import org.apache.commons.cli.Options; diff --git a/Model/src/main/java/org/gusdb/wdk/model/test/sanity/tests/QuestionTest.java b/Model/src/main/java/org/gusdb/wdk/model/test/sanity/tests/QuestionTest.java index 2a1b1d85c3..4c4153f124 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/test/sanity/tests/QuestionTest.java +++ b/Model/src/main/java/org/gusdb/wdk/model/test/sanity/tests/QuestionTest.java @@ -10,9 +10,9 @@ import org.gusdb.wdk.model.question.Question; import org.gusdb.wdk.model.record.RecordInstance; import org.gusdb.wdk.model.record.attribute.AttributeField; +import org.gusdb.wdk.model.test.sanity.RangeCountTestUtil; import org.gusdb.wdk.model.test.sanity.SanityTester.ElementTest; import org.gusdb.wdk.model.test.sanity.SanityTester.Statistics; -import org.gusdb.wdk.model.test.sanity.RangeCountTestUtil; import org.gusdb.wdk.model.test.sanity.TestResult; import org.gusdb.wdk.model.user.StepContainer; import org.gusdb.wdk.model.user.User; diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java new file mode 100644 index 0000000000..a7b877d7ba --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java @@ -0,0 +1,31 @@ +package org.gusdb.wdk.model.user; + +import org.gusdb.wdk.model.WdkModel; +import org.json.JSONObject; + +/** + * Represents a VEupathDB user + * + * @author rdoherty + */ +public class BasicUser extends org.gusdb.oauth2.client.veupathdb.BasicUser implements User { + + private final WdkModel _wdkModel; + + public BasicUser(WdkModel wdkModel, long userId, boolean isGuest, String signature, String stableId) { + super(userId, isGuest, signature, stableId); + _wdkModel = wdkModel; + } + + public BasicUser(WdkModel wdkModel, JSONObject json) { + super(json); + _wdkModel = wdkModel; + } + + @Override + public WdkModel getWdkModel() { + return _wdkModel; + } + + +} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java index fb05a0ec48..040c3dc941 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java @@ -1,51 +1,20 @@ package org.gusdb.wdk.model.user; -import org.apache.log4j.Logger; import org.gusdb.oauth2.client.ValidatedToken; -import org.gusdb.oauth2.shared.token.IdTokenFields; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.session.WdkOAuthClientWrapper; -import org.json.JSONObject; -public class BearerTokenUser extends User { +public class BearerTokenUser extends org.gusdb.oauth2.client.veupathdb.BearerTokenUser implements User { - private static final Logger LOG = Logger.getLogger(BearerTokenUser.class); - - private final WdkOAuthClientWrapper _client; - private final ValidatedToken _token; - private boolean _userInfoFetched = false; + private final WdkModel _wdkModel; public BearerTokenUser(WdkModel wdkModel, WdkOAuthClientWrapper client, ValidatedToken token) { - // parent constructor sets immutable fields provided on the token - super(wdkModel, - Long.valueOf(token.getUserId()), - token.isGuest(), - token.getTokenContents().get(IdTokenFields.signature.name(), String.class), - token.getTokenContents().get(IdTokenFields.preferred_username.name(), String.class)); - _client = client; - _token = token; + super(client.getOAuthClient(), client.getOAuthConfig().getOauthUrl(), token); + _wdkModel = wdkModel; } @Override - protected void fetchUserInfo() { - // return if already fetched - if (_userInfoFetched) return; - - LOG.info("User data fetch requested for user " + getUserId() + "; querying OAuth server."); - // fetch user info from OAuth server where it is stored (but only on demand, and only once for this object's lifetime) - JSONObject userInfo = _client.getUserData(_token); - - // set email (standard property but mutable so set on user profile and not token - setEmail(userInfo.getString(IdTokenFields.email.name())); - - // set other user properties found only on user profile object - for (WdkUserProperty userProp : User.USER_PROPERTIES.values()) { - userProp.setValue(this, userInfo.optString(userProp.getName(), null)); - } - - LOG.info("User data successfully fetched for " + getDisplayName() + " / " + getOrganization()); - _userInfoFetched = true; + public WdkModel getWdkModel() { + return _wdkModel; } - } - diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/User.java b/Model/src/main/java/org/gusdb/wdk/model/user/User.java index af44903e2c..266107cfdd 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/User.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/User.java @@ -1,185 +1,16 @@ -package org.gusdb.wdk.model.user; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.gusdb.fgputil.accountdb.UserPropertyName; -import org.gusdb.fgputil.functional.Functions; -import org.gusdb.wdk.model.WdkModel; - -/** - * Represents a WDK user. - * - * @author rdoherty - */ -public class User { - - public static final Map USER_PROPERTIES = createUserPropertyDefs(); - - private static Map createUserPropertyDefs() { - List userProps = List.of( - new WdkUserProperty("username", "Username", "username", false, false, false, User::getUsername, User::setUsername), - new WdkUserProperty("firstName", "First Name", "first_name", true, true, false, User::getFirstName, User::setFirstName), - new WdkUserProperty("middleName", "Middle Name", "middle_name", false, true, false, User::getMiddleName, User::setMiddleName), - new WdkUserProperty("lastName", "Last Name", "last_name", true, true, false, User::getLastName, User::setLastName), - new WdkUserProperty("organization", "Organization", "organization", true, true, false, User::getOrganization, User::setOrganization), - new WdkUserProperty("interests", "Interests", "interests", false, false, true, User::getInterests, User::setInterests) - ); - return Collections.unmodifiableMap(Functions.getMapFromValues(userProps, UserPropertyName::getName)); - } - - private final WdkModel _wdkModel; - - // immutable fields supplied by bearer token - private final long _userId; - private final boolean _isGuest; - private final String _signature; - private final String _stableId; - - // mutable fields that may need to be fetched - private String _email; // standard; not a user property - private String _username; - private String _firstName; - private String _middleName; - private String _lastName; - private String _organization; - private String _interests; - - public User(WdkModel wdkModel, long userId, boolean isGuest, String signature, String stableId) { - _wdkModel = wdkModel; - _userId = userId; - _isGuest = isGuest; - _signature = signature; - _stableId = stableId; - } - - public WdkModel getWdkModel() { - return _wdkModel; - } - - public long getUserId() { - return _userId; - } - - public boolean isGuest() { - return _isGuest; - } - - public String getSignature() { - return _signature; - } - - public String getStableId() { - return _stableId; - } - - protected void fetchUserInfo() { - // nothing to do in this base class; all info must be explicitly set - } - - public String getEmail() { - fetchUserInfo(); - return _email; - } - - public User setEmail(String email) { - _email = email; - return this; - } - - public String getUsername() { - fetchUserInfo(); - return _username; - } - - public User setUsername(String username) { - _username = username; - return this; - } - - public String getFirstName() { - fetchUserInfo(); - return _firstName; - } - - public User setFirstName(String firstName) { - _firstName = firstName; - return this; - } - - public String getMiddleName() { - fetchUserInfo(); - return _middleName; - } - - public User setMiddleName(String middleName) { - _middleName = middleName; - return this; - } - - public String getLastName() { - fetchUserInfo(); - return _lastName; - } - - public User setLastName(String lastName) { - _lastName = lastName; - return this; - } - - public String getOrganization() { - fetchUserInfo(); - return _organization; - } - - public User setOrganization(String organization) { - _organization = organization; - return this; - } - - public String getInterests() { - fetchUserInfo(); - return _interests; - } - - public User setInterests(String interests) { - _interests = interests; - return this; - } - - /** - * Provides a "pretty" display name for this user - * - * @return display name for this user - */ - public String getDisplayName() { - return isGuest() ? "WDK Guest" : ( - formatNamePart(getFirstName()) + - formatNamePart(getMiddleName()) + - formatNamePart(getLastName())).trim(); - } - - private static String formatNamePart(String namePart) { - return (namePart == null || namePart.isEmpty() ? "" : " " + namePart.trim()); - } - - @Override - public String toString() { - return "User #" + getUserId() + " - " + getEmail(); - } - - @Override - public int hashCode() { - return String.valueOf(getUserId()).hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof User)) { - return false; - } - return getUserId() == ((User)obj).getUserId(); - } - -} +package org.gusdb.wdk.model.user; + +import java.util.Collection; + +import org.gusdb.oauth2.client.veupathdb.UserProperty; +import org.gusdb.wdk.model.WdkModel; + +public interface User extends org.gusdb.oauth2.client.veupathdb.User { + + static Collection getPropertyDefs() { + return BasicUser.USER_PROPERTIES.values(); + } + + WdkModel getWdkModel(); + +} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index 5e1ce04f26..c815767749 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -10,13 +10,18 @@ import org.apache.log4j.Logger; import org.gusdb.fgputil.MapBuilder; +import org.gusdb.fgputil.Tuples.ThreeTuple; +import org.gusdb.fgputil.Tuples.TwoTuple; import org.gusdb.fgputil.accountdb.AccountManager; import org.gusdb.fgputil.accountdb.UserProfile; import org.gusdb.fgputil.db.pool.DatabaseInstance; import org.gusdb.fgputil.db.runner.SQLRunner; +import org.gusdb.fgputil.db.runner.SQLRunnerException; import org.gusdb.fgputil.db.runner.SingleLongResultSetHandler; import org.gusdb.fgputil.db.runner.SingleLongResultSetHandler.Status; import org.gusdb.fgputil.events.Events; +import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.shared.IdTokenFields; import org.gusdb.wdk.events.UserProfileUpdateEvent; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; @@ -24,7 +29,10 @@ import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.config.ModelConfig; -import org.gusdb.wdk.model.config.ModelConfigAccountDB; +import org.gusdb.wdk.session.WdkOAuthClientWrapper; +import org.json.JSONObject; + +import io.prometheus.client.Counter; /** * Manages persistence of user profile and preferences and creation and @@ -38,6 +46,11 @@ public class UserFactory { @SuppressWarnings("unused") private static Logger LOG = Logger.getLogger(UserFactory.class); + private static final Counter GUEST_CREATION_COUNTER = Counter.build() + .name("wdk_guest_creation_count") + .help("Number of guest users created by WDK services") + .register(); + // ------------------------------------------------------------------------- // database table and column definitions // ------------------------------------------------------------------------- @@ -54,23 +67,32 @@ public class UserFactory { static final String USER_SCHEMA_MACRO = "$$USER_SCHEMA$$"; private static final String IS_GUEST_VALUE_MACRO = "$$IS_GUEST$$"; - private static final String COUNT_USER_REF_BY_ID_SQL = - "select count(*)" + + // SQL and types to insert previously unknown user refs into the users table + private static final String INSERT_USER_REF_SQL = + "insert" + + " when not exists (select 1 from " + USER_SCHEMA_MACRO + TABLE_USERS + " where " + COL_USER_ID + " = ?)" + + " then" + + " into " + USER_SCHEMA_MACRO + TABLE_USERS + " (" + COL_USER_ID + "," + COL_IS_GUEST + "," + COL_FIRST_ACCESS +")" + + " select ?, " + IS_GUEST_VALUE_MACRO + ", ? from dual"; + + private static final Integer[] INSERT_USER_REF_PARAM_TYPES = { Types.BIGINT, Types.BIGINT, Types.TIMESTAMP }; + + // SQL and types to select user ref by ID + private static final String SELECT_USER_REF_BY_ID_SQL = + "select " + COL_USER_ID + ", " + COL_IS_GUEST + ", " + COL_FIRST_ACCESS + " from " + USER_SCHEMA_MACRO + TABLE_USERS + " where " + COL_USER_ID + " = ?"; - private static final Integer[] COUNT_USER_REF_BY_ID_PARAM_TYPES = { Types.BIGINT }; - private static final String INSERT_USER_REF_SQL = - "insert into " + USER_SCHEMA_MACRO + TABLE_USERS + - " (" + COL_USER_ID + "," + COL_IS_GUEST + "," + COL_FIRST_ACCESS +")" + - " values (?, " + IS_GUEST_VALUE_MACRO + ", ?)"; - private static final Integer[] INSERT_USER_REF_PARAM_TYPES = { Types.BIGINT, Types.TIMESTAMP }; + private static final Integer[] SELECT_USER_REF_BY_ID_PARAM_TYPES = { Types.BIGINT }; - private static final String SELECT_GUEST_USER_REF_BY_ID_SQL = - "select " + COL_FIRST_ACCESS + - " from " + USER_SCHEMA_MACRO + TABLE_USERS + - " where " + COL_IS_GUEST + " = " + IS_GUEST_VALUE_MACRO + " and " + COL_USER_ID + " = ?"; - private static final Integer[] SELECT_GUEST_USER_REF_BY_ID_PARAM_TYPES = { Types.BIGINT }; + private static class UserReference extends ThreeTuple { + public UserReference(Long userId, Boolean isGuest, Date firstAccess) { + super(userId, isGuest, firstAccess); + } + public Long getUserId() { return getFirst(); } + public Boolean isGuest() { return getSecond(); } + public Date getFirstAccess() { return getThird(); } + } // ------------------------------------------------------------------------- // the macros used by the registration email @@ -87,6 +109,7 @@ public class UserFactory { private final WdkModel _wdkModel; private final DatabaseInstance _userDb; private final String _userSchema; + private final WdkOAuthClientWrapper _client; // ------------------------------------------------------------------------- // constructor @@ -96,115 +119,51 @@ public UserFactory(WdkModel wdkModel) { // save model for populating new users _wdkModel = wdkModel; _userDb = wdkModel.getUserDb(); - ModelConfig modelConfig = wdkModel.getModelConfig(); - _userSchema = modelConfig.getUserDB().getUserSchema(); - ModelConfigAccountDB accountDbConfig = modelConfig.getAccountDB(); + _userSchema = wdkModel.getModelConfig().getUserDB().getUserSchema(); + _client = new WdkOAuthClientWrapper(wdkModel); } // ------------------------------------------------------------------------- // methods // ------------------------------------------------------------------------- - public User createUser(String email, - Map profileProperties) - throws WdkModelException, InvalidUsernameOrEmailException { - String dontEmailProp = _wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); - boolean sendWelcomeEmail = (dontEmailProp == null || !dontEmailProp.equals("true")); - return createUser(email, profileProperties, true, sendWelcomeEmail); - } - - public User createUser(String email, - Map profileProperties, - boolean addUserDbReference, boolean sendWelcomeEmail) - throws WdkModelException, InvalidUsernameOrEmailException { - try { - // check email for uniqueness and format - email = validateAndFormatEmail(email, _accountManager); - - // if user supplied a username, make sure it is unique - if (profileProperties.containsKey(AccountManager.USERNAME_PROPERTY_KEY)) { - String username = profileProperties.get(AccountManager.USERNAME_PROPERTY_KEY); - // check whether the username exists in the database already; if so, the operation fails - if (_accountManager.getUserProfileByUsername(username) != null) { - throw new InvalidUsernameOrEmailException("The username '" + username + "' is already in use. " + "Please choose another one."); - } - } - - // generate temporary password for user - String password = generateTemporaryPassword(); + public User createUser(String email, Map profileProperties) + throws WdkModelException, WdkUserException { - // add user to account DB - UserProfile profile = _accountManager.createAccount(email, password, profileProperties); + // contact OAuth server to create a new user with the passed props + TwoTuple userTuple = parseExpandedUserJson(_client.createUser(email, profileProperties)); + User user = userTuple.getFirst(); - // add user to this user DB (will be added to other user DBs as needed during login) - if (addUserDbReference) { - addUserReference(profile.getUserId(), false); - } - - // create new user object and add profile properties - User user = getUserFromProfile(profile); + // add user to this user DB (will be added to other user DBs as needed during login) + addUserReference(user); - // if needed, send user temporary password via email - if (sendWelcomeEmail) { - emailTemporaryPassword(user, password, _wdkModel.getModelConfig()); - } - - return user; - } - catch (WdkModelException | InvalidUsernameOrEmailException e) { - // do not wrap known exceptions - throw e; + // if needed, send user temporary password via email + if (isSendWelcomeEmail()) { + emailTemporaryPassword(user, userTuple.getSecond(), _wdkModel.getModelConfig()); } - catch (Exception e) { - // wrap unknown exceptions with WdkModelException - throw new WdkModelException("Could not completely create new user", e); - } - } - private User getUserFromProfile(UserProfile profile) { - if (profile == null) return null; - Map properties = profile.getProperties(); - return new User(_wdkModel, profile.getUserId(), - false, profile.getSignature(), profile.getStableId()) - .setEmail(profile.getEmail()) - .setFirstName(properties.get("firstName")) - .setMiddleName(properties.get("middleName")) - .setLastName(properties.get("lastName")) - .setOrganization(properties.get("organization")); - } + return user; - private void addUserReference(Long userId, boolean isGuest) { - Timestamp insertedOn = new Timestamp(new Date().getTime()); - String sql = INSERT_USER_REF_SQL - .replace(USER_SCHEMA_MACRO, _userSchema) - .replace(IS_GUEST_VALUE_MACRO, _userDb.getPlatform().convertBoolean(isGuest).toString()); - new SQLRunner(_userDb.getDataSource(), sql, "insert-user-ref") - .executeStatement(new Object[]{ userId, insertedOn }, INSERT_USER_REF_PARAM_TYPES); } - private boolean hasUserReference(long userId) { - String sql = COUNT_USER_REF_BY_ID_SQL.replace(USER_SCHEMA_MACRO, _userSchema); - SingleLongResultSetHandler handler = new SingleLongResultSetHandler(); - new SQLRunner(_userDb.getDataSource(), sql, "check-user-ref") - .executeQuery(new Object[]{ userId }, COUNT_USER_REF_BY_ID_PARAM_TYPES, handler); - if (handler.getStatus().equals(Status.NON_NULL_VALUE)) { - switch (handler.getRetrievedValue().intValue()) { - case 0: return false; - case 1: return true; - default: throw new IllegalStateException("More than one user reference in userDb for user " + userId); - } - } - throw new WdkRuntimeException("User reference count query did not return count for user id " + - userId + ". Status = " + handler.getStatus() + ", sql = " + sql); + private TwoTuple parseExpandedUserJson(JSONObject userJson) { + User user = new BasicUser(_wdkModel, + Long.valueOf(userJson.getString(IdTokenFields.sub.name())), + userJson.getBoolean(IdTokenFields.is_guest.name()), + userJson.getString(IdTokenFields.signature.name()), + userJson.getString(IdTokenFields.preferred_username.name()) + ); + user.setPropertyValues(userJson); + String password = userJson.getString(IdTokenFields.password.name()); + return new TwoTuple<>(user, password); } - private Date getGuestUserRefFirstAccess(long userId) { - String sql = SELECT_GUEST_USER_REF_BY_ID_SQL - .replace(USER_SCHEMA_MACRO, _userSchema) - .replace(IS_GUEST_VALUE_MACRO, _userDb.getPlatform().convertBoolean(true).toString()); - return new SQLRunner(_userDb.getDataSource(), sql, "get-guest-user-ref") - .executeQuery(new Object[]{ userId }, SELECT_GUEST_USER_REF_BY_ID_PARAM_TYPES, rs -> - !rs.next() ? null : new Date(rs.getTimestamp(COL_FIRST_ACCESS).getTime())); + /** + * @return whether or not WDK is configured to send a welcome email to new registered users (defaults to true) + */ + private boolean isSendWelcomeEmail() { + String dontEmailProp = _wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); + return dontEmailProp == null || !dontEmailProp.equals("true"); } private static void emailTemporaryPassword(User user, String password, @@ -226,73 +185,57 @@ private static void emailTemporaryPassword(User user, String password, Utilities.sendEmail(smtpServer, user.getEmail(), supportEmail, emailSubject, emailContent); } - // TODO: remove! This validation is now done on the OAuth server - private static String validateAndFormatEmail(String email, AccountManager accountMgr) throws InvalidUsernameOrEmailException { - // trim and validate passed email address and extract stable name - if (email == null) - throw new InvalidUsernameOrEmailException("The user's email cannot be empty."); - // format the info - email = AccountManager.trimAndLowercase(email); - if (email.isEmpty()) - throw new InvalidUsernameOrEmailException("The user's email cannot be empty."); - int atSignIndex = email.indexOf("@"); - if (atSignIndex < 1) // must be present and not the first char - throw new InvalidUsernameOrEmailException("The user's email address is invalid."); - // check whether the user exist in the database already; if email exists, the operation fails - if (accountMgr.getUserProfileByEmail(email) != null) - throw new InvalidUsernameOrEmailException("The email '" + email + "' has already been registered. " + "Please choose another."); - return email; - } - - private static String generateTemporaryPassword() { - // generate a random password of 8 characters long, the range will be - // [0-9A-Za-z] - StringBuilder buffer = new StringBuilder(); - Random rand = new Random(); - for (int i = 0; i < 8; i++) { - int value = rand.nextInt(36); - if (value < 10) { // number - buffer.append(value); - } else { // lower case letters - buffer.append((char) ('a' + value - 10)); - } - } - return buffer.toString(); - } - /** - * Create an unregistered user of the specified type and persist in the database - * - * @param userType unregistered user type to persist - * @return new unregistered user with remaining fields populated - * @throws WdkRuntimeException if unable to persist temporary user + * Adds a user reference row to the UserDB users table if one does not exist. + * Note is_guest and first_access are immutable fields and once set will not be + * changed by this code. + * + * @param user user to add + * @throws WdkModelException */ - public User createUnregistedUser() throws WdkRuntimeException { + public int addUserReference(User user) throws WdkModelException { try { - UserProfile profile = _accountManager.createGuestAccount("GUEST_"); - addUserReference(profile.getUserId(), true); - return new User(_wdkModel, profile.getUserId(), true, profile.getSignature(), profile.getStableId()).setEmail(profile.getEmail()); + long userId = user.getUserId(); + boolean isGuest = user.isGuest(); + Timestamp insertedOn = new Timestamp(new Date().getTime()); + String sql = INSERT_USER_REF_SQL + .replace(USER_SCHEMA_MACRO, _userSchema) + .replace(IS_GUEST_VALUE_MACRO, _userDb.getPlatform().convertBoolean(isGuest).toString()); + return new SQLRunner(_userDb.getDataSource(), sql, "insert-user-ref") + .executeUpdate(new Object[]{ userId, userId, insertedOn }, INSERT_USER_REF_PARAM_TYPES); } - catch (Exception e) { - throw new WdkRuntimeException("Unable to save temporary user", e); + catch (SQLRunnerException e) { + throw WdkModelException.translateFrom(e); } } - private User completeLogin(User user) { - if (user == null) - return user; - - // make sure user has reference in this user DB (needs to happen before merging) - if (!hasUserReference(user.getUserId())) { - addUserReference(user.getUserId(), false); + private Optional getUserReference(long userId) throws WdkModelException { + try { + String sql = SELECT_USER_REF_BY_ID_SQL.replace(USER_SCHEMA_MACRO, _userSchema); + return new SQLRunner(_userDb.getDataSource(), sql, "get-user-ref").executeQuery( + new Object[]{ userId }, + SELECT_USER_REF_BY_ID_PARAM_TYPES, + rs -> + !rs.next() + ? Optional.empty() + : Optional.of(new UserReference( + rs.getLong(COL_USER_ID), + rs.getBoolean(COL_IS_GUEST), + new Date(rs.getTimestamp(COL_FIRST_ACCESS).getTime())))); } + catch (SQLRunnerException e) { + throw WdkModelException.translateFrom(e); + } + } - // update user active timestamp - _accountManager.updateLastLogin(user.getUserId()); - - return user; + public TwoTuple createUnregisteredUser() { + ValidatedToken token = _client.getNewGuestToken(); + User user = new BearerTokenUser(_wdkModel, _client, token); + GUEST_CREATION_COUNTER.inc(); + return new TwoTuple<>(token, user); } + public User login(User guest, String email, String password) throws WdkUserException { // make sure the guest is really a guest @@ -434,15 +377,6 @@ public void resetPassword(String emailOrLoginName) throws WdkUserException, WdkM emailTemporaryPassword(user, newPassword, _wdkModel.getModelConfig()); } - public void changePassword(long userId, String newPassword) { - _accountManager.updatePassword(userId, newPassword); - } - // FIXME: should be atomic - public void insertUserToUserDb(User user) { - // make sure user has reference in this user DB (needs to happen before merging) - if (!hasUserReference(user.getUserId())) { - addUserReference(user.getUserId(), user.isGuest()); - } - } + } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java b/Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java deleted file mode 100644 index 276ed80c03..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/user/WdkUserProperty.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.gusdb.wdk.model.user; - -import java.util.function.BiConsumer; -import java.util.function.Function; - -import org.gusdb.fgputil.accountdb.UserPropertyName; - -/** - * Convenience class to work around accountdb API and interface with WDK User class - */ -public class WdkUserProperty extends UserPropertyName { - - private final Function _getter; - private final BiConsumer _setter; - - public WdkUserProperty(String name, String displayName, String dbKey, boolean isRequired, boolean isPublic, boolean isMultiLine, Function getter, BiConsumer setter) { - UserPropertyName userProp = new UserPropertyName(name, dbKey, isRequired); - userProp.setDisplayName(displayName); - userProp.setPublic(isPublic); - userProp.setMultiLine(isMultiLine); - _getter = getter; - _setter = setter; - } - - public String getValue(User user) { - return _getter.apply(user); - } - - public void setValue(User user, String value) { - _setter.accept(user, value); - } -} diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index 52ea09d7bb..709b71f046 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -1,13 +1,18 @@ package org.gusdb.wdk.session; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import org.gusdb.oauth2.client.InvalidPropertiesException; import org.gusdb.oauth2.client.OAuthClient; import org.gusdb.oauth2.client.OAuthConfig; import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.shared.IdTokenFields; import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkUserException; import org.json.JSONArray; import org.json.JSONObject; @@ -46,8 +51,27 @@ public List getUserData(List userIds) { } return users; } + public ValidatedToken getNewGuestToken() { return _client.getNewGuestToken(_config); } + public JSONObject createUser(String email, Map profileProperties) throws WdkUserException { + try { + Map allProps = new HashMap<>(profileProperties); + allProps.put(IdTokenFields.email.name(), email); + return _client.createNewUser(_config, allProps); + } + catch (InvalidPropertiesException e) { + throw new WdkUserException(e.getMessage()); + } + } + + public OAuthClient getOAuthClient() { + return _client; + } + + public OAuthConfig getOAuthConfig() { + return _config; + } } diff --git a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java index 0358b6db1b..98ea9a7d9c 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java @@ -18,6 +18,7 @@ import org.apache.log4j.Logger; import org.glassfish.grizzly.http.server.Request; +import org.gusdb.fgputil.Tuples.TwoTuple; import org.gusdb.fgputil.web.ApplicationContext; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.RequestData; @@ -26,24 +27,19 @@ import org.gusdb.wdk.controller.ContextLookup; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.user.BearerTokenUser; import org.gusdb.wdk.model.user.User; +import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.service.service.SessionService; import org.gusdb.wdk.service.service.SystemService; import org.gusdb.wdk.session.WdkOAuthClientWrapper; -import io.prometheus.client.Counter; - @Priority(30) public class CheckLoginFilter implements ContainerRequestFilter, ContainerResponseFilter { private static final Logger LOG = Logger.getLogger(CheckLoginFilter.class); - private static final Counter GUEST_CREATION_COUNTER = Counter.build() - .name("wdk_guest_creation_count") - .help("Number of guest users created by WDK services") - .register(); - private static final String TOKEN_COOKIE_VALUE_TO_SET = "tokenCookieValueToSet"; private static final String LEGACY_WDK_LOGIN_COOKIE_NAME = "wdk_check_auth"; @@ -83,26 +79,28 @@ public void filter(ContainerRequestContext requestContext) throws IOException { } else { // no credentials submitted; automatically create a guest to use on this request - token = oauth.getNewGuestToken(); - user = new BearerTokenUser(wdkModel, oauth, token); + UserFactory factory = wdkModel.getUserFactory(); + TwoTuple guestPair = factory.createUnregisteredUser(); + token = guestPair.getFirst(); + user = guestPair.getSecond(); LOG.info("Created new guest user [" + user.getUserId() + "] for request to path: /" + requestPath); - GUEST_CREATION_COUNTER.inc(); // set flag indicating that cookies should be added to response containing the new token requestContext.setProperty(TOKEN_COOKIE_VALUE_TO_SET, token.getTokenValue()); } // insert reference to this user into user DB for tracking and for foreign keys on other user DB tables - wdkModel.getUserFactory().insertUserToUserDb(user); + wdkModel.getUserFactory().addUserReference(user); - // set user on the request object for use by this request's processing + // set creds and user on the request object for use by this request's processing + request.setAttribute(Utilities.BEARER_TOKEN_KEY, token); request.setAttribute(Utilities.WDK_USER_KEY, user); } catch (Exception e) { // for now, log and let this go, deferring to legacy authentication LOG.error("Unable to authenticate with Authorization header " + rawToken, e); - throw e; + throw e instanceof RuntimeException ? (RuntimeException)e : new WdkRuntimeException(e); } } diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java index ac12e63a1e..01bc586a59 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java @@ -3,6 +3,7 @@ import java.util.Optional; import org.gusdb.fgputil.accountdb.UserPropertyName; +import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.config.ModelConfig; @@ -50,7 +51,7 @@ public static JSONObject getWdkProjectInfo(WdkModel wdkModel, String serviceEndp // create profile property config sub-array JSONArray userProfileProps = new JSONArray(); - for (UserPropertyName prop : User.USER_PROPERTIES.values()) { + for (UserProperty prop : User.getPropertyDefs()) { userProfileProps.put(new JSONObject() .put(JsonKeys.NAME, prop.getName()) .put(JsonKeys.DISPLAY_NAME, prop.getDisplayName()) diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java index 96f4e0a381..f4b89f0cf8 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java @@ -2,10 +2,10 @@ import java.util.Optional; +import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserPreferences; -import org.gusdb.wdk.model.user.WdkUserProperty; import org.json.JSONException; import org.json.JSONObject; @@ -44,7 +44,7 @@ public static JSONObject getUserJson(User user, boolean isOwner, private static JSONObject getPropertiesJson(User user, boolean isOwner) { JSONObject propsJson = new JSONObject(); - for (WdkUserProperty definedProperty : User.USER_PROPERTIES.values()) { + for (UserProperty definedProperty : User.getPropertyDefs()) { if (isOwner || definedProperty.isPublic()) { String key = definedProperty.getName(); String value = Optional.ofNullable(definedProperty.getValue(user)).orElse(""); diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java index 96d5a6a96f..2d3e5f55f3 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java @@ -1,8 +1,10 @@ package org.gusdb.wdk.service.request.user; +import java.util.Collection; import java.util.List; import org.gusdb.fgputil.accountdb.UserPropertyName; +import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; @@ -15,7 +17,7 @@ public class UserCreationRequest { private final UserPreferencesRequest _globalPreferencesRequest; private final UserPreferencesRequest _projectPreferencesRequest; - public static UserCreationRequest createFromJson(JSONObject requestJson, List configuredProps) + public static UserCreationRequest createFromJson(JSONObject requestJson, Collection configuredProps) throws RequestMisformatException, DataValidationException { try { JSONObject userRequest = requestJson.getJSONObject(JsonKeys.USER); diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java index e3a1968567..fafa4c742a 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java @@ -15,7 +15,6 @@ import org.gusdb.fgputil.accountdb.AccountManager; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.config.ModelConfig; -import org.gusdb.wdk.model.config.ModelConfigAccountDB; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.json.JSONArray; @@ -47,7 +46,6 @@ public class UserDatasetShareRequest { public UserDatasetShareRequest(WdkModel wdkModel) { ModelConfig modelConfig = wdkModel.getModelConfig(); - ModelConfigAccountDB accountDbConfig = modelConfig.getAccountDB(); _accountManager = new AccountManager(wdkModel.getAccountDb(), accountDbConfig.getAccountSchema(), accountDbConfig.getUserPropertyNames()); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserProfileRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserProfileRequest.java index 9f810eee3d..42728cb8a5 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserProfileRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserProfileRequest.java @@ -1,18 +1,18 @@ package org.gusdb.wdk.service.request.user; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.function.Function; import org.apache.log4j.Logger; import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.accountdb.UserProfile; -import org.gusdb.fgputil.accountdb.UserPropertyName; -import org.gusdb.fgputil.json.JsonUtil; import org.gusdb.fgputil.functional.Functions; +import org.gusdb.fgputil.json.JsonUtil; +import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; @@ -70,7 +70,7 @@ public void setProfileMap(Map profileMap) { * @throws DataValidationException if data contained in request is invalid for another reason */ public static UserProfileRequest createFromJson(JSONObject json, - List configuredProps, boolean requireRequiredProperties) + Collection configuredProps, boolean requireRequiredProperties) throws RequestMisformatException, DataValidationException { try { UserProfileRequest request = new UserProfileRequest(); @@ -100,10 +100,8 @@ private static String validateEmail(JSONObject json, boolean isRequired) return email; } - private static Map getPropMap(List configuredProps) { - return Functions.getMapFromValues(configuredProps, new Function() { - @Override public String apply(UserPropertyName obj) { return obj.getName(); } - }); + private static Map getPropMap(Collection configuredProps) { + return Functions.getMapFromValues(configuredProps, UserProperty::getName); } /** @@ -117,7 +115,7 @@ private static Map getPropMap(List c * @throws DataValidationException if JSON format and types are ok, but further data validation fails */ private static Map parseProperties(JSONObject json, - Map configuredProps, boolean requireRequiredProperties) throws JSONException, DataValidationException { + Map configuredProps, boolean requireRequiredProperties) throws JSONException, DataValidationException { if (!json.has(JsonKeys.PROPERTIES)) { if (requireRequiredProperties) { throw new JSONException("'properties' property is required."); @@ -154,7 +152,7 @@ private static Map parseProperties(JSONObject json, } private static void validateRequiredPropsPresent(Map parsedProps, - Map configuredProps) throws DataValidationException { + Map configuredProps) throws DataValidationException { List missingProps = new ArrayList<>(); for (String propKey : configuredProps.keySet()) { if (!parsedProps.containsKey(propKey) && configuredProps.get(propKey).isRequired()) { @@ -167,7 +165,7 @@ private static void validateRequiredPropsPresent(Map parsedProps } private static void validateNonEmptyRequirement(Map parsedProps, - Map configuredProps) throws DataValidationException { + Map configuredProps) throws DataValidationException { // make sure any required (i.e. value must be non-empty) properties are not being modified to be empty List emptyRequiredValues = new ArrayList(); for (String propKey : configuredProps.keySet()) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 4544ed0cfd..5cd709df20 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -27,6 +27,7 @@ import org.gusdb.fgputil.web.SessionProxy; import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.wdk.core.api.JsonKeys; +import org.gusdb.wdk.events.NewUserEvent; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; @@ -113,8 +114,7 @@ public Response getOauthStateToken() throws WdkModelException { private static String generateStateToken(WdkModel wdkModel) throws WdkModelException { String saltedString = UUID.randomUUID() + ":::" + - String.valueOf(new Date().getTime()) + ":::" + - wdkModel.getModelConfig().getSecretKey(); + String.valueOf(new Date().getTime()); return EncryptionUtil.encrypt(saltedString); } @@ -165,7 +165,7 @@ public Response processOauthLogin( WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); ValidatedToken bearerToken = client.getBearerTokenFromAuthCode(authCode, appUrl); User newUser = new BearerTokenUser(wdkModel, client, bearerToken); - wdkModel.getUserFactory().insertUserToUserDb(newUser); + wdkModel.getUserFactory().addUserReference(newUser); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); @@ -210,7 +210,7 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); ValidatedToken bearerToken = client.getBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); User newUser = new BearerTokenUser(wdkModel, client, bearerToken); - wdkModel.getUserFactory().insertUserToUserDb(newUser); + wdkModel.getUserFactory().addUserReference(newUser); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); @@ -258,24 +258,19 @@ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, Us Events.triggerAndWait(new NewUserEvent(newUser, oldUser, session), new WdkRuntimeException("Unable to complete WDK user assignement.")); - // TODO: until client is updated, must still return WDK login cookie - LoginCookieFactory baker = new LoginCookieFactory(getWdkModel().getModelConfig().getSecretKey()); - CookieBuilder loginCookie = baker.createLoginCookie(newUser.getEmail()); - loginCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); - // 3-year expiration (should change secret key before then) CookieBuilder bearerTokenCookie = new CookieBuilder( HttpHeaders.AUTHORIZATION, bearerToken.getTokenValue()); bearerTokenCookie.setMaxAge(EXPIRATION_3_YEARS_SECS); - redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, loginCookie, bearerTokenCookie); + redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, bearerTokenCookie); return (isRedirectResponse ? createRedirectResponse(redirectUrl) : createJsonResponse(true, null, redirectUrl) ) - .cookie(loginCookie.toJaxRsCookie(), bearerTokenCookie.toJaxRsCookie()) + .cookie(bearerTokenCookie.toJaxRsCookie()) .build(); } } @@ -288,7 +283,7 @@ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, Us * @param cookie login cookie to be sent to the browser * @return page user should be redirected to after successful login */ - protected String getSuccessRedirectUrl(String redirectUrl, User user, CookieBuilder cookie, CookieBuilder bearerTokenCookie) { + protected String getSuccessRedirectUrl(String redirectUrl, User user, CookieBuilder bearerTokenCookie) { return redirectUrl; } @@ -302,7 +297,7 @@ public Response processLogout() throws WdkModelException { // get a new session and add new guest user to it SessionProxy session = getSession(); - User newUser = getWdkModel().getUserFactory().createUnregistedUser(); + User newUser = getWdkModel().getUserFactory().createUnregisteredUser(); session.setAttribute(Utilities.WDK_USER_KEY, newUser); // throw new user event diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index 48fd2ef25d..f7c8c2a2ca 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -1,15 +1,12 @@ package org.gusdb.wdk.service.service.user; -import java.util.List; import java.util.Map.Entry; import java.util.Optional; import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; -import javax.ws.rs.ForbiddenException; import javax.ws.rs.GET; import javax.ws.rs.PUT; -import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; @@ -17,13 +14,11 @@ import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; -import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; import org.gusdb.wdk.model.user.User; -import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.model.user.UserPreferenceFactory; import org.gusdb.wdk.model.user.UserPreferences; import org.gusdb.wdk.service.UserBundle; @@ -32,7 +27,6 @@ import org.gusdb.wdk.service.request.exception.ConflictException; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; -import org.gusdb.wdk.service.request.user.PasswordChangeRequest; import org.gusdb.wdk.service.request.user.UserProfileRequest; import org.json.JSONException; import org.json.JSONObject; @@ -120,7 +114,7 @@ public Response updateUserProfile(String body) try { User user = getPrivateRegisteredUser(); UserProfileRequest request = UserProfileRequest.createFromJson( - new JSONObject(body), getPropertiesConfig(), false); + new JSONObject(body), User.USER_PROPERTIES, false); NewCookie loginCookie = processEmail(user, request.getEmail()); // overwrite any provided properties if (request.getEmail() != null) { @@ -149,30 +143,6 @@ private Response getProfileUpdateResponse(NewCookie loginCookie) { Response.noContent().cookie(loginCookie).build(); } - @PUT - @Path("password") - @Consumes(MediaType.APPLICATION_JSON) - public Response changeUserPassword(String body) - throws WdkModelException, DataValidationException { - User user = getPrivateRegisteredUser(); - try { - JSONObject json = new JSONObject(body); - PasswordChangeRequest request = PasswordChangeRequest.createFromJson(json); - UserFactory userMgr = getWdkModel().getUserFactory(); - if (userMgr.isCorrectPassword(user.getEmail(), request.getOldPassword())) { - userMgr.changePassword(user.getUserId(), request.getNewPassword()); - return Response.noContent().build(); - } - else { - // user submitted wrong old password, permission denied - throw new ForbiddenException("Old password specified is not correct."); - } - } - catch (JSONException | RequestMisformatException e) { - throw new BadRequestException(e); - } - } - /** * Ensures that a new email address, if provided, does not duplicate that of another account. If * the new email passes validation, a new cookie is returned to be used for authentication following the diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserService.java index 400e3fa959..005df36dc5 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserService.java @@ -6,8 +6,8 @@ import javax.ws.rs.PathParam; import org.gusdb.fgputil.FormatUtil; -import org.gusdb.fgputil.validation.ValidationLevel; import org.gusdb.fgputil.validation.ValidObjectFactory.RunnableObj; +import org.gusdb.fgputil.validation.ValidationLevel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.user.Step; import org.gusdb.wdk.model.user.User; diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java index 1d643076e8..706b98a3c4 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java @@ -1,5 +1,6 @@ package org.gusdb.wdk.service.service.user; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -18,10 +19,10 @@ import org.gusdb.fgputil.json.JsonIterators; import org.gusdb.fgputil.json.JsonType; import org.gusdb.fgputil.json.JsonType.ValueType; +import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkUserException; -import org.gusdb.wdk.model.config.ModelConfigAccountDB; import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; @@ -58,7 +59,7 @@ public class UserUtilityServices extends AbstractWdkService { public Response createNewUser(String body) throws RequestMisformatException, DataValidationException, WdkModelException { try { JSONObject requestJson = new JSONObject(body); - List configuredUserProps = getWdkModel().getModelConfig().getAccountDB().getUserPropertyNames(); + Collection configuredUserProps = User.getPropertyDefs(); UserCreationRequest request = UserCreationRequest.createFromJson(requestJson, configuredUserProps); // create the user, saving to DB From 95bc8968420baff3ad9525aab4d7b7b6bc866f40 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 21 Feb 2024 00:24:23 -0500 Subject: [PATCH 11/25] Fix up user-related services and clean out code a bit --- .../conifer/templates/WDK/model-config.xml.j2 | 3 + .../org/gusdb/wdk/events/NewUserEvent.java | 9 +- .../wdk/events/UserProfileUpdateEvent.java | 20 +- .../gusdb/wdk/model/config/ModelConfig.java | 42 +++++ .../org/gusdb/wdk/model/user/BasicUser.java | 10 + .../wdk/model/user/StrategyAnalysis.java | 4 +- .../java/org/gusdb/wdk/model/user/User.java | 7 - .../org/gusdb/wdk/model/user/UserFactory.java | 178 +++--------------- .../wdk/session/WdkOAuthClientWrapper.java | 96 +++++++--- .../wdk/service/filter/CheckLoginFilter.java | 7 +- .../service/formatter/ProjectFormatter.java | 3 +- .../wdk/service/formatter/UserFormatter.java | 2 +- .../request/user/UserCreationRequest.java | 4 +- .../request/user/UserDatasetShareRequest.java | 169 ++++++++--------- .../service/service/AbstractWdkService.java | 5 + .../wdk/service/service/SessionService.java | 128 +++---------- .../service/service/user/ProfileService.java | 94 ++------- .../service/user/UserUtilityServices.java | 46 +++-- 18 files changed, 320 insertions(+), 507 deletions(-) diff --git a/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 b/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 index ebb2626861..41fc61bba5 100644 --- a/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 +++ b/Model/lib/conifer/roles/conifer/templates/WDK/model-config.xml.j2 @@ -29,6 +29,9 @@ site_vars file: {{ site_vars }} {% if modelconfig_useWeights is defined -%} assetsUrl="{{ modelconfig_useWeights }}" {% endif -%} + {% if modelconfig_secretKeyFile is defined -%} + secretKeyFile="{{ modelconfig_secretKeyFile }}" + {% endif -%} {% if modelconfig_blockedThreshold is defined -%} monitorBlockedThreads="{{ modelconfig_monitorBlockedThreads }}" {% endif -%} diff --git a/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java b/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java index c572428ed4..943f99cfab 100644 --- a/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java +++ b/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java @@ -1,19 +1,16 @@ package org.gusdb.wdk.events; import org.gusdb.fgputil.events.Event; -import org.gusdb.fgputil.web.SessionProxy; import org.gusdb.wdk.model.user.User; public class NewUserEvent extends Event { private final User _newUser; private final User _oldUser; - private final SessionProxy _session; - public NewUserEvent(User newUser, User oldUser, SessionProxy session) { + public NewUserEvent(User newUser, User oldUser) { _newUser = newUser; _oldUser = oldUser; - _session = session; } public User getNewUser() { @@ -23,8 +20,4 @@ public User getNewUser() { public User getOldUser() { return _oldUser; } - - public SessionProxy getSession() { - return _session; - } } diff --git a/Model/src/main/java/org/gusdb/wdk/events/UserProfileUpdateEvent.java b/Model/src/main/java/org/gusdb/wdk/events/UserProfileUpdateEvent.java index cd9229fb95..2116e36384 100644 --- a/Model/src/main/java/org/gusdb/wdk/events/UserProfileUpdateEvent.java +++ b/Model/src/main/java/org/gusdb/wdk/events/UserProfileUpdateEvent.java @@ -1,27 +1,27 @@ package org.gusdb.wdk.events; -import org.gusdb.fgputil.accountdb.UserProfile; import org.gusdb.fgputil.events.Event; import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.user.User; public class UserProfileUpdateEvent extends Event { - private final UserProfile _oldProfile; - private final UserProfile _newProfile; + private final User _oldUser; + private final User _newUser; private final WdkModel _wdkModel; - public UserProfileUpdateEvent(UserProfile oldProfile, UserProfile newProfile, WdkModel wdkModel) { - _oldProfile = oldProfile; - _newProfile = newProfile; + public UserProfileUpdateEvent(User oldUser, User newUser, WdkModel wdkModel) { + _oldUser = oldUser; + _newUser = newUser; _wdkModel = wdkModel; } - public UserProfile getOldProfile() { - return _oldProfile; + public User getOldUser() { + return _oldUser; } - public UserProfile getNewProfile() { - return _newProfile; + public User getNewUser() { + return _newUser; } public WdkModel getWdkModel() { diff --git a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java index 6bb84b1236..af2b74436f 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java +++ b/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfig.java @@ -1,10 +1,15 @@ package org.gusdb.wdk.model.config; +import java.io.FileReader; +import java.io.IOException; import java.nio.file.Path; import java.util.List; import java.util.Optional; +import org.apache.log4j.Logger; +import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.FormatUtil; +import org.gusdb.fgputil.IoUtil; import org.gusdb.fgputil.Named.NamedObject; import org.gusdb.oauth2.client.KeyStoreTrustManager.KeyStoreConfig; import org.gusdb.oauth2.client.OAuthConfig; @@ -17,6 +22,8 @@ */ public class ModelConfig implements OAuthConfig, KeyStoreConfig { + private static final Logger LOG = Logger.getLogger(ModelConfig.class); + public static final String WSF_LOCAL = "local"; public enum AuthenticationMethod implements NamedObject { @@ -80,6 +87,17 @@ public String getName() { */ private final boolean _useWeights; + /** + * location of secret key file + */ + private final Optional _secretKeyFile; + + /** + * cached secret key; value is assigned at construction or as soon as the + * secretKeyFile is present and readable when the secret key is requested + */ + private String _secretKey; + /** * default regex used by all the stringParams */ @@ -147,6 +165,7 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac // file locations _wdkTempDir = wdkTempDir; + _secretKeyFile = secretKeyFile; // network locations _webServiceUrl = webServiceUrl; @@ -183,6 +202,9 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac // SSL key store information _keyStoreFile = keyStoreFile; _keyStorePassPhrase = keyStorePassPhrase; + + // get secret key at object creation time if available + getSecretKey(); } /** @@ -271,6 +293,26 @@ public ModelConfigAppDB getAppDB() { return _appDB; } + /** + * Returns a cached secret key, generated by encrypting the value in the + * configured secret key file. If the configured filename is null or the contents + * of the file cannot be read for any reason, null is returned. + * + * @return secret key + */ + public String getSecretKey() { + if (_secretKey == null && _secretKeyFile.isPresent()) { + try (FileReader in = new FileReader(_secretKeyFile.get().toFile())) { + _secretKey = EncryptionUtil.md5(IoUtil.readAllChars(in).strip()); + } + catch (IOException e) { + // log error but otherwise ignore so null is returned; problem may be remedied in the future + LOG.warn("Unable to read secret key value from file: " + _secretKeyFile, e); + } + } + return _secretKey; + } + /** * @return configured authentication method */ diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java index a7b877d7ba..7287c039c2 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java @@ -1,5 +1,6 @@ package org.gusdb.wdk.model.user; +import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.model.WdkModel; import org.json.JSONObject; @@ -22,6 +23,15 @@ public BasicUser(WdkModel wdkModel, JSONObject json) { _wdkModel = wdkModel; } + public BasicUser(User user) { + super(user.getUserId(), user.isGuest(), user.getSignature(), user.getStableId()); + _wdkModel = user.getWdkModel(); + setEmail(user.getEmail()); + for (UserProperty prop : USER_PROPERTIES.values()) { + prop.setValue(this, prop.getValue(user)); + } + } + @Override public WdkModel getWdkModel() { return _wdkModel; diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/StrategyAnalysis.java b/Model/src/main/java/org/gusdb/wdk/model/user/StrategyAnalysis.java index 5434c241f3..06c38cbc6f 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/StrategyAnalysis.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/StrategyAnalysis.java @@ -37,8 +37,8 @@ public static void main(String[] args) throws WdkModelException { !userEmail.isPresent() ? model.getStepFactory().getAllStrategies(validationLevel, malformedStrats, stratsWithBuildErrors).values() : model.getStepFactory().getStrategies( - Optional.ofNullable(model.getUserFactory() - .getUserByEmail(userEmail.get())) + model.getUserFactory() + .getUserByEmail(userEmail.get()) .orElseThrow(() -> new WdkModelException( "Could not find user with email: " + userEmail )) diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/User.java b/Model/src/main/java/org/gusdb/wdk/model/user/User.java index 266107cfdd..168e7de1c4 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/User.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/User.java @@ -1,16 +1,9 @@ package org.gusdb.wdk.model.user; -import java.util.Collection; - -import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.model.WdkModel; public interface User extends org.gusdb.oauth2.client.veupathdb.User { - static Collection getPropertyDefs() { - return BasicUser.USER_PROPERTIES.values(); - } - WdkModel getWdkModel(); } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index c815767749..80d5b384fe 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -2,35 +2,29 @@ import java.sql.Timestamp; import java.sql.Types; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Random; +import java.util.Set; import java.util.regex.Matcher; +import java.util.stream.Collectors; import org.apache.log4j.Logger; -import org.gusdb.fgputil.MapBuilder; import org.gusdb.fgputil.Tuples.ThreeTuple; import org.gusdb.fgputil.Tuples.TwoTuple; -import org.gusdb.fgputil.accountdb.AccountManager; -import org.gusdb.fgputil.accountdb.UserProfile; -import org.gusdb.fgputil.db.pool.DatabaseInstance; import org.gusdb.fgputil.db.runner.SQLRunner; import org.gusdb.fgputil.db.runner.SQLRunnerException; -import org.gusdb.fgputil.db.runner.SingleLongResultSetHandler; -import org.gusdb.fgputil.db.runner.SingleLongResultSetHandler.Status; import org.gusdb.fgputil.events.Events; import org.gusdb.oauth2.client.ValidatedToken; -import org.gusdb.oauth2.shared.IdTokenFields; +import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.wdk.events.UserProfileUpdateEvent; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.WdkRuntimeException; -import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.config.ModelConfig; import org.gusdb.wdk.session.WdkOAuthClientWrapper; -import org.json.JSONObject; import io.prometheus.client.Counter; @@ -107,7 +101,6 @@ public UserReference(Long userId, Boolean isGuest, Date firstAccess) { // ------------------------------------------------------------------------- private final WdkModel _wdkModel; - private final DatabaseInstance _userDb; private final String _userSchema; private final WdkOAuthClientWrapper _client; @@ -118,7 +111,6 @@ public UserReference(Long userId, Boolean isGuest, Date firstAccess) { public UserFactory(WdkModel wdkModel) { // save model for populating new users _wdkModel = wdkModel; - _userDb = wdkModel.getUserDb(); _userSchema = wdkModel.getModelConfig().getUserDB().getUserSchema(); _client = new WdkOAuthClientWrapper(wdkModel); } @@ -128,10 +120,10 @@ public UserFactory(WdkModel wdkModel) { // ------------------------------------------------------------------------- public User createUser(String email, Map profileProperties) - throws WdkModelException, WdkUserException { + throws WdkModelException, InvalidPropertiesException, InvalidUsernameOrEmailException { // contact OAuth server to create a new user with the passed props - TwoTuple userTuple = parseExpandedUserJson(_client.createUser(email, profileProperties)); + TwoTuple userTuple = _client.createUser(email, profileProperties); User user = userTuple.getFirst(); // add user to this user DB (will be added to other user DBs as needed during login) @@ -143,19 +135,6 @@ public User createUser(String email, Map profileProperties) } return user; - - } - - private TwoTuple parseExpandedUserJson(JSONObject userJson) { - User user = new BasicUser(_wdkModel, - Long.valueOf(userJson.getString(IdTokenFields.sub.name())), - userJson.getBoolean(IdTokenFields.is_guest.name()), - userJson.getString(IdTokenFields.signature.name()), - userJson.getString(IdTokenFields.preferred_username.name()) - ); - user.setPropertyValues(userJson); - String password = userJson.getString(IdTokenFields.password.name()); - return new TwoTuple<>(user, password); } /** @@ -200,8 +179,8 @@ public int addUserReference(User user) throws WdkModelException { Timestamp insertedOn = new Timestamp(new Date().getTime()); String sql = INSERT_USER_REF_SQL .replace(USER_SCHEMA_MACRO, _userSchema) - .replace(IS_GUEST_VALUE_MACRO, _userDb.getPlatform().convertBoolean(isGuest).toString()); - return new SQLRunner(_userDb.getDataSource(), sql, "insert-user-ref") + .replace(IS_GUEST_VALUE_MACRO, _wdkModel.getUserDb().getPlatform().convertBoolean(isGuest).toString()); + return new SQLRunner(_wdkModel.getUserDb().getDataSource(), sql, "insert-user-ref") .executeUpdate(new Object[]{ userId, userId, insertedOn }, INSERT_USER_REF_PARAM_TYPES); } catch (SQLRunnerException e) { @@ -209,10 +188,11 @@ public int addUserReference(User user) throws WdkModelException { } } + // FIXME: see if this is actually needed anywhere? E.g. do we ever need to look up user refs by user ID to find last login? private Optional getUserReference(long userId) throws WdkModelException { try { String sql = SELECT_USER_REF_BY_ID_SQL.replace(USER_SCHEMA_MACRO, _userSchema); - return new SQLRunner(_userDb.getDataSource(), sql, "get-user-ref").executeQuery( + return new SQLRunner(_wdkModel.getUserDb().getDataSource(), sql, "get-user-ref").executeQuery( new Object[]{ userId }, SELECT_USER_REF_BY_ID_PARAM_TYPES, rs -> @@ -235,40 +215,9 @@ public TwoTuple createUnregisteredUser() { return new TwoTuple<>(token, user); } - - public User login(User guest, String email, String password) - throws WdkUserException { - // make sure the guest is really a guest - if (!guest.isGuest()) - throw new WdkUserException("User has been logged in."); - - // authenticate the user; if fails, a WdkUserException will be thrown out - User user = authenticate(email, password); - if (user == null) { - throw new WdkUserException("Invalid email or password."); - } - return completeLogin(user); - } - - public User login(long userId) throws WdkModelException { - return completeLogin(getUserById(userId) - .orElseThrow(() -> new WdkModelException("User with ID " + userId + " could not be found."))); - } - - /** - * Returns whether email and password are a correct credentials combination. - * - * @param usernameOrEmail user email - * @param password user password - * @return true email corresponds to user and password is correct, else false - * @throws WdkModelException if error occurs while determining result - */ - public boolean isCorrectPassword(String usernameOrEmail, String password) throws WdkModelException { - return authenticate(usernameOrEmail, password) != null; - } - - private User authenticate(String usernameOrEmail, String password) { - return getUserFromProfile(_accountManager.getUserProfile(usernameOrEmail, password)); + public Map verifyUserids(Set userIds) { + Map userMap = _client.getUsersById(new ArrayList<>(userIds)); + return userIds.stream().collect(Collectors.toMap(id -> id, id -> userMap.get(id) != null)); } /** @@ -279,104 +228,31 @@ private User authenticate(String usernameOrEmail, String password) { * @throws WdkModelException if an error occurs in the attempt */ public Optional getUserById(long userId) throws WdkModelException { - UserProfile profile = _accountManager.getUserProfile(userId); - if (profile != null) { - // found registered user in account DB; create RegisteredUser and populate - return Optional.of(getUserFromProfile(profile)); - } - else { - // cannot find user in account DB; however, the passed ID may represent a guest local to this userDb - Date accessDate = getGuestUserRefFirstAccess(userId); - if (accessDate != null) { - // guest user was found in local user Db; create UnregisteredUser and populate - profile = AccountManager.createGuestProfile("GUEST_", userId, accessDate); - return Optional.of(new User(_wdkModel, profile.getUserId(), true, - profile.getSignature(), profile.getStableId()).setEmail(profile.getEmail())); - - } - else { - // user does not exist in account or user DBs - return Optional.empty(); - } - } - } - - public User getUserByEmail(String email) { - return getUserFromProfile(_accountManager.getUserProfileByEmail(email)); + return Optional.ofNullable(_client.getUsersById(List.of(userId)).get(userId)); } - private User getUserProfileByUsernameOrEmail(String usernameOrEmail) { - return getUserFromProfile(_accountManager.getUserProfileByUsernameOrEmail(usernameOrEmail)); + public Optional getUserByEmail(String email) { + return Optional.ofNullable(_client.getUsersByEmail(List.of(email)).get(email)); } /** * Save the basic information of a user * * @param user + * @param newUser + * @throws InvalidPropertiesException */ - public void saveUser(User user) throws WdkModelException, InvalidUsernameOrEmailException { - try { - - // Three integrity checks: - - // 1. Check if user exists in the database. if not, fail and ask to create the user first - UserProfile oldProfile = _accountManager.getUserProfile(user.getUserId()); - if (oldProfile == null) { - throw new WdkModelException("Cannot update user; no user exists with ID " + user.getUserId()); - } - - // 2. Check if another user exists with this email (PK will protect us but want better message) - UserProfile emailUser = _accountManager.getUserProfileByEmail(user.getEmail()); - if (emailUser != null && emailUser.getUserId() != user.getUserId()) { - throw new InvalidUsernameOrEmailException("This email is already in use by another account. Please choose another."); - } - - // 3. Check if another user exists with this username (if supplied) - if (user.getProfileProperties().containsKey(AccountManager.USERNAME_PROPERTY_KEY)) { - String username = user.getProfileProperties().get(AccountManager.USERNAME_PROPERTY_KEY); - - UserProfile usernameUser = _accountManager.getUserProfileByUsername(username); - if (usernameUser != null && user.getUserId() != usernameUser.getUserId()) { - throw new InvalidUsernameOrEmailException("The username '" + username + "' is already in use. " + "Please choose another one."); - } - } - - // save off other data to user profile - _accountManager.saveUserProfile(user.getUserId(), user.getEmail(), new MapBuilder() - .put("firstName", user.getFirstName()) - .put("middleName", user.getMiddleName()) - .put("lastName", user.getLastName()) - .put("organization", user.getOrganization()) - .toMap() - ); - - // get updated profile and trigger profile update event - UserProfile newProfile = _accountManager.getUserProfile(user.getUserId()); - Events.trigger(new UserProfileUpdateEvent(oldProfile, newProfile, _wdkModel)); - - } - catch (InvalidUsernameOrEmailException e) { - throw e; - } - // wrap any other exception in WdkModelException - catch (Exception e) { - throw new WdkModelException("Unable to update user profile for ID " + user.getUserId(), e); - } + public User saveUser(User oldUser, User newUser, ValidatedToken authorizationToken) throws InvalidUsernameOrEmailException, InvalidPropertiesException { + User savedUser = new BasicUser(_wdkModel, _client.updateUser(newUser, authorizationToken)); + Events.trigger(new UserProfileUpdateEvent(oldUser, newUser, _wdkModel)); + return savedUser; } - public void resetPassword(String emailOrLoginName) throws WdkUserException, WdkModelException { - User user = getUserProfileByUsernameOrEmail(emailOrLoginName); - if (user == null) { - throw new WdkUserException("Cannot find user with email or login name: " + emailOrLoginName); - } - // create new temporary password - String newPassword = generateTemporaryPassword(); - // set new password on user - _accountManager.updatePassword(user.getUserId(), newPassword); + public void resetPassword(String loginName) throws InvalidUsernameOrEmailException, WdkModelException { + TwoTuple user = _client.resetPassword(loginName); + // email user new password - emailTemporaryPassword(user, newPassword, _wdkModel.getModelConfig()); + emailTemporaryPassword(user.getFirst(), user.getSecond(), _wdkModel.getModelConfig()); } - - } diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index 709b71f046..000fb2a3e7 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -1,77 +1,119 @@ package org.gusdb.wdk.session; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -import org.gusdb.oauth2.client.InvalidPropertiesException; +import org.gusdb.fgputil.Tuples.TwoTuple; import org.gusdb.oauth2.client.OAuthClient; import org.gusdb.oauth2.client.OAuthConfig; import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.client.veupathdb.OAuthQuerier; +import org.gusdb.oauth2.client.veupathdb.UserProperty; +import org.gusdb.oauth2.exception.ConflictException; +import org.gusdb.oauth2.exception.InvalidPropertiesException; +import org.gusdb.oauth2.exception.InvalidTokenException; import org.gusdb.oauth2.shared.IdTokenFields; import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.WdkUserException; -import org.json.JSONArray; +import org.gusdb.wdk.model.user.BasicUser; +import org.gusdb.wdk.model.user.BearerTokenUser; +import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; +import org.gusdb.wdk.model.user.User; import org.json.JSONObject; public class WdkOAuthClientWrapper { + private final WdkModel _wdkModel; private final OAuthConfig _config; private final OAuthClient _client; public WdkOAuthClientWrapper(WdkModel wdkModel) { + _wdkModel = wdkModel; _config = wdkModel.getModelConfig(); _client = new OAuthClient(OAuthClient.getTrustManager(wdkModel.getModelConfig())); } - public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) { + public OAuthClient getOAuthClient() { + return _client; + } + + public OAuthConfig getOAuthConfig() { + return _config; + } + + public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) throws InvalidTokenException { return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); } - public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) { + public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) throws InvalidTokenException { return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); } - public ValidatedToken validateBearerToken(String rawToken) { + public ValidatedToken validateBearerToken(String rawToken) throws InvalidTokenException { return _client.getValidatedEcdsaSignedToken(_config.getOauthUrl(), rawToken); } - public JSONObject getUserData(ValidatedToken token) { - return _client.getUserData(_config.getOauthUrl(), token); + public User getUserData(ValidatedToken token) { + return new BearerTokenUser(_wdkModel, this, token); } - public List getUserData(List userIds) { - List stringIds = userIds.stream().map(String::valueOf).collect(Collectors.toList()); - JSONArray usersJson = _client.getUserData(_config, stringIds); - List users = new ArrayList<>(); - for (int i = 0; i < usersJson.length(); i++) { - users.add(usersJson.getJSONObject(i)); - } - return users; + public Map getUsersById(List userIds) { + return OAuthQuerier.getUsersById(_client, _config, userIds, json -> new BasicUser(_wdkModel, json)); + } + + public Map getUsersByEmail(List emails) { + return OAuthQuerier.getUsersByEmail(_client, _config, emails, json -> new BasicUser(_wdkModel, json)); } public ValidatedToken getNewGuestToken() { - return _client.getNewGuestToken(_config); + try { + return _client.getNewGuestToken(_config); + } + catch (InvalidTokenException e) { + throw new IllegalStateException(e); // this will go away with the next OAuth release + } + } + + private TwoTuple parseExpandedUserJson(JSONObject userJson) { + User user = new BasicUser(_wdkModel, userJson); + String password = userJson.getString(IdTokenFields.password.name()); + return new TwoTuple<>(user, password); } - public JSONObject createUser(String email, Map profileProperties) throws WdkUserException { + public TwoTuple createUser(String email, Map profileProperties) throws InvalidPropertiesException, InvalidUsernameOrEmailException { try { Map allProps = new HashMap<>(profileProperties); allProps.put(IdTokenFields.email.name(), email); - return _client.createNewUser(_config, allProps); + return parseExpandedUserJson(_client.createNewUser(_config, allProps)); } - catch (InvalidPropertiesException e) { - throw new WdkUserException(e.getMessage()); + catch (ConflictException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); } } - public OAuthClient getOAuthClient() { - return _client; + public JSONObject updateUser(User user, ValidatedToken token) throws InvalidUsernameOrEmailException, InvalidPropertiesException { + try { + Map props = new HashMap<>(); + props.put(IdTokenFields.email.name(), user.getEmail()); + for (UserProperty prop : User.USER_PROPERTIES.values()) { + props.put(prop.getName(), prop.getValue(user)); + } + return _client.modifyUser(_config, token, props); + } + catch (ConflictException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); + } } - public OAuthConfig getOAuthConfig() { - return _config; + public TwoTuple resetPassword(String loginName) throws InvalidUsernameOrEmailException { + try { + return parseExpandedUserJson(_client.resetPassword(_config, loginName)); + } + catch (InvalidPropertiesException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); + } } + + + } diff --git a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java index 98ea9a7d9c..198af43e9c 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java @@ -15,6 +15,7 @@ import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.NewCookie; import org.apache.log4j.Logger; import org.glassfish.grizzly.http.server.Request; @@ -128,10 +129,8 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont String tokenValue = (String)requestContext.getProperty(TOKEN_COOKIE_VALUE_TO_SET); // set cookie value for both Authorization cookie - CookieBuilder cookie = new CookieBuilder(HttpHeaders.AUTHORIZATION, tokenValue); - cookie.setMaxAge(SessionService.EXPIRATION_3_YEARS_SECS); - cookie.setPath("/"); - headers.add(HttpHeaders.SET_COOKIE, cookie.toJaxRsCookie().toString()); + NewCookie authCookie = SessionService.getAuthCookie(tokenValue); + headers.add(HttpHeaders.SET_COOKIE, authCookie.toString()); } // unset legacy WDK check auth cookie in case it is present diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java index 01bc586a59..d0abea4be9 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java @@ -2,7 +2,6 @@ import java.util.Optional; -import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModel; @@ -51,7 +50,7 @@ public static JSONObject getWdkProjectInfo(WdkModel wdkModel, String serviceEndp // create profile property config sub-array JSONArray userProfileProps = new JSONArray(); - for (UserProperty prop : User.getPropertyDefs()) { + for (UserProperty prop : User.USER_PROPERTIES.values()) { userProfileProps.put(new JSONObject() .put(JsonKeys.NAME, prop.getName()) .put(JsonKeys.DISPLAY_NAME, prop.getDisplayName()) diff --git a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java index f4b89f0cf8..476f17d2dd 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/formatter/UserFormatter.java @@ -44,7 +44,7 @@ public static JSONObject getUserJson(User user, boolean isOwner, private static JSONObject getPropertiesJson(User user, boolean isOwner) { JSONObject propsJson = new JSONObject(); - for (UserProperty definedProperty : User.getPropertyDefs()) { + for (UserProperty definedProperty : User.USER_PROPERTIES.values()) { if (isOwner || definedProperty.isPublic()) { String key = definedProperty.getName(); String value = Optional.ofNullable(definedProperty.getValue(user)).orElse(""); diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java index 2d3e5f55f3..aea61375f7 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java @@ -1,9 +1,7 @@ package org.gusdb.wdk.service.request.user; import java.util.Collection; -import java.util.List; -import org.gusdb.fgputil.accountdb.UserPropertyName; import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.service.request.exception.DataValidationException; @@ -36,7 +34,6 @@ public UserCreationRequest(UserProfileRequest profileRequest, UserPreferencesReq _profileRequest = profileRequest; _globalPreferencesRequest = globalPreferencesRequest; _projectPreferencesRequest = projectPreferencesRequest; - } public UserProfileRequest getProfileRequest() { @@ -46,6 +43,7 @@ public UserProfileRequest getProfileRequest() { public UserPreferencesRequest getGlobalPreferencesRequest() { return _globalPreferencesRequest; } + public UserPreferencesRequest getProjectPreferencesRequest() { return _projectPreferencesRequest; } diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java index fafa4c742a..33cc824c48 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java @@ -12,9 +12,8 @@ import org.apache.log4j.Logger; import org.gusdb.fgputil.FormatUtil; -import org.gusdb.fgputil.accountdb.AccountManager; import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.config.ModelConfig; +import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.json.JSONArray; @@ -25,18 +24,18 @@ import com.fasterxml.jackson.databind.type.CollectionType; /** - * Parses the JSON object returned by either a PATCH REST request for - * managing user dataset sharing + * Parses the JSON object returned by either a PATCH REST request for managing user dataset sharing + * * @author crisl-adm * */ public class UserDatasetShareRequest { - + private static Logger LOG = Logger.getLogger(UserDatasetShareRequest.class); - - public static final List SHARE_TYPES = new ArrayList<>(Arrays.asList("add","delete")); - - private AccountManager _accountManager; + + public static final List SHARE_TYPES = new ArrayList<>(Arrays.asList("add", "delete")); + + private final UserFactory _userFactory; private Map>> _userDatasetShareMap; private List _invalidActions; private List _malformedDatasetIds; @@ -45,26 +44,24 @@ public class UserDatasetShareRequest { private List _invalidDatasetIds; public UserDatasetShareRequest(WdkModel wdkModel) { - ModelConfig modelConfig = wdkModel.getModelConfig(); - _accountManager = new AccountManager(wdkModel.getAccountDb(), - accountDbConfig.getAccountSchema(), accountDbConfig.getUserPropertyNames()); + _userFactory = wdkModel.getUserFactory(); } - + public Map>> getUserDatasetShareMap() { - return _userDatasetShareMap; + return _userDatasetShareMap; } public void setUserDatasetShareMap(Map>> userDatasetShareMap) { _userDatasetShareMap = userDatasetShareMap; } - + public void verifyUserIds(JSONObject userDatasetShare) { - Set userIds = new HashSet<>(); - _invalidUserIds = new ArrayList<>(); - for(Object shareType : userDatasetShare.keySet()) { - JSONObject userDatasets = userDatasetShare.getJSONObject((String)shareType); - for(Object userDataset : userDatasets.keySet()) { - JSONArray userIdsJsonArray = userDatasets.getJSONArray(userDataset.toString()); + Set userIds = new HashSet<>(); + _invalidUserIds = new ArrayList<>(); + for (Object shareType : userDatasetShare.keySet()) { + JSONObject userDatasets = userDatasetShare.getJSONObject((String) shareType); + for (Object userDataset : userDatasets.keySet()) { + JSONArray userIdsJsonArray = userDatasets.getJSONArray(userDataset.toString()); ObjectMapper mapper = new ObjectMapper(); String userIdsJson = null; try { @@ -72,121 +69,121 @@ public void verifyUserIds(JSONObject userDatasetShare) { CollectionType setType = mapper.getTypeFactory().constructCollectionType(Set.class, Long.class); userIds.addAll(mapper.readValue(userIdsJson, setType)); } - catch(IOException ioe) { - // Will catch these later in second pass + catch (IOException ioe) { + // Will catch these later in second pass continue; } } } - Map _userIdMap = _accountManager.verifyUserids(userIds); - _invalidUserIds = _userIdMap.keySet().stream().filter(userId -> !_userIdMap.get(userId)).collect(Collectors.toList()); + Map _userIdMap = _userFactory.verifyUserids(userIds); + _invalidUserIds = _userIdMap.keySet().stream() + .filter(userId -> !_userIdMap.get(userId)) + .collect(Collectors.toList()); } - + public JSONObject getErrors() { - JSONObject jsonErrors = new JSONObject(); - if(!_invalidActions.isEmpty()) { + JSONObject jsonErrors = new JSONObject(); + if (!_invalidActions.isEmpty()) { jsonErrors.put("invalid actions", FormatUtil.join(_invalidActions.toArray(), ",")); - } - if(!_malformedDatasetIds.isEmpty()) { + } + if (!_malformedDatasetIds.isEmpty()) { jsonErrors.put("malformed dataset ids", FormatUtil.join(_malformedDatasetIds.toArray(), ",")); - } - if(!_malformedUserIds.isEmpty()) { - JSONArray jsonArray = new JSONArray(); - for(Object dataset : _malformedUserIds.keySet()) { - jsonArray.put(new JSONObject().put("dataset", dataset).put("user id list", _malformedUserIds.get(dataset))); - } - jsonErrors.put("malformed user id lists", jsonArray); - } - if(!_invalidUserIds.isEmpty()) { + } + if (!_malformedUserIds.isEmpty()) { + JSONArray jsonArray = new JSONArray(); + for (Object dataset : _malformedUserIds.keySet()) { + jsonArray.put( + new JSONObject().put("dataset", dataset).put("user id list", _malformedUserIds.get(dataset))); + } + jsonErrors.put("malformed user id lists", jsonArray); + } + if (!_invalidUserIds.isEmpty()) { jsonErrors.put("invalid user ids", FormatUtil.join(_invalidUserIds.toArray(), ",")); - } - if(!_invalidDatasetIds.isEmpty()) { + } + if (!_invalidDatasetIds.isEmpty()) { jsonErrors.put("invalid dataset ids", FormatUtil.join(_invalidDatasetIds.toArray(), ",")); - } - LOG.error(jsonErrors.toString()); - return jsonErrors; + } + LOG.error(jsonErrors.toString()); + return jsonErrors; } - + /** * Input Format: * - * { - * "add": { - * "dataset_id1": [ "userEmail1", "userEmail2" ] - * "dataset_id2": [ "userEmail1" ] - * }, - * "delete" { - * "dataset_id3": [ "userEmail1", "userEmail3" ] - * } - * } + * { "add": { "dataset_id1": [ "userEmail1", "userEmail2" ] "dataset_id2": [ "userEmail1" ] }, "delete" { + * "dataset_id3": [ "userEmail1", "userEmail3" ] } } * * @param json * @return * @throws RequestMisformatException */ - public static UserDatasetShareRequest createFromJson(JSONObject json, WdkModel wdkModel, Set ownedDatasetIds) throws RequestMisformatException, DataValidationException { + public static UserDatasetShareRequest createFromJson(JSONObject json, WdkModel wdkModel, + Set ownedDatasetIds) throws RequestMisformatException, DataValidationException { try { - UserDatasetShareRequest request = new UserDatasetShareRequest(wdkModel); - request.verifyUserIds(json); - request.setUserDatasetShareMap(request.parseUserDatasetShare(json, ownedDatasetIds)); - JSONObject errors = request.getErrors(); - if(errors.length() != 0) { + UserDatasetShareRequest request = new UserDatasetShareRequest(wdkModel); + request.verifyUserIds(json); + request.setUserDatasetShareMap(request.parseUserDatasetShare(json, ownedDatasetIds)); + JSONObject errors = request.getErrors(); + if (errors.length() != 0) { throw new DataValidationException(errors.toString(2)); - } - return request; - } - catch (JSONException e) { - String detailMessage = e.getMessage() != null ? e.getMessage() : "No additional information."; - throw new RequestMisformatException(detailMessage, e); - } + } + return request; + } + catch (JSONException e) { + String detailMessage = e.getMessage() != null ? e.getMessage() : "No additional information."; + throw new RequestMisformatException(detailMessage, e); + } } /** - * Parses the share types, the participating datasets and the users on the receiving end of the share into - * a Java structure. Bad shareTypes, datasets and users are ignored (warnings only in the log). - * @param userDatasetShare - the input json object - see above for structure + * Parses the share types, the participating datasets and the users on the receiving end of the share into a + * Java structure. Bad shareTypes, datasets and users are ignored (warnings only in the log). + * + * @param userDatasetShare + * - the input json object - see above for structure * @return - the Java map representing this JSON object * @throws JSONException */ - protected Map>> parseUserDatasetShare(JSONObject userDatasetShare, Set ownedDatasetIds) throws JSONException { + protected Map>> parseUserDatasetShare(JSONObject userDatasetShare, + Set ownedDatasetIds) throws JSONException { List shareTypes = SHARE_TYPES; _invalidActions = new ArrayList<>(); _malformedDatasetIds = new ArrayList<>(); _invalidDatasetIds = new ArrayList<>(); - _malformedUserIds = new HashMap(); + _malformedUserIds = new HashMap(); Map>> map = new HashMap<>(); - for(Object shareType : userDatasetShare.keySet()) { - if(shareTypes.contains(((String)shareType).trim())) { - JSONObject userDatasets = userDatasetShare.getJSONObject((String)shareType); + for (Object shareType : userDatasetShare.keySet()) { + if (shareTypes.contains(((String) shareType).trim())) { + JSONObject userDatasets = userDatasetShare.getJSONObject((String) shareType); Map> innerMap = new HashMap<>(); - for(Object userDataset : userDatasets.keySet()) { + for (Object userDataset : userDatasets.keySet()) { Long datasetId = 0L; try { - datasetId = Long.valueOf(((String)userDataset).trim()); + datasetId = Long.valueOf(((String) userDataset).trim()); } - catch(NumberFormatException nfe) { + catch (NumberFormatException nfe) { _malformedDatasetIds.add(userDataset); continue; } - if(!ownedDatasetIds.contains(datasetId)) { - _invalidDatasetIds.add(datasetId); - continue; + if (!ownedDatasetIds.contains(datasetId)) { + _invalidDatasetIds.add(datasetId); + continue; } - JSONArray userIdsJsonArray = userDatasets.getJSONArray(datasetId.toString()); + JSONArray userIdsJsonArray = userDatasets.getJSONArray(datasetId.toString()); ObjectMapper mapper = new ObjectMapper(); String userIdsJson = null; try { userIdsJson = userIdsJsonArray.toString(); CollectionType setType = mapper.getTypeFactory().constructCollectionType(Set.class, Long.class); Set userIds = mapper.readValue(userIdsJson, setType); - innerMap.put(datasetId, userIds); + innerMap.put(datasetId, userIds); } - catch(IOException ioe) { - _malformedUserIds.put(datasetId, userIdsJson); + catch (IOException ioe) { + _malformedUserIds.put(datasetId, userIdsJson); continue; } } - map.put(((String)shareType).trim(), innerMap); + map.put(((String) shareType).trim(), innerMap); } else { _invalidActions.add(shareType); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java index 473840151c..bd8ee85458 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java @@ -26,6 +26,7 @@ import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.RequestData; import org.gusdb.fgputil.web.SessionProxy; +import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.wdk.controller.ContextLookup; import org.gusdb.wdk.errors.ErrorContext; import org.gusdb.wdk.errors.ErrorContext.ErrorLocation; @@ -149,6 +150,10 @@ protected User getRequestingUser() { throw new IllegalStateException("No user present on request."); } + protected ValidatedToken getAuthorizationToken() { + return (ValidatedToken)getRequest().getAttributeMap().get(Utilities.BEARER_TOKEN_KEY); + } + protected boolean isSessionUserAdmin() { List adminEmails = getWdkModel().getModelConfig().getAdminEmails(); return adminEmails.contains(getRequestingUser().getEmail()); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 5cd709df20..b3b0042aa2 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -15,17 +15,20 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import org.apache.log4j.Logger; import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.FormatUtil; +import org.gusdb.fgputil.Tuples.TwoTuple; import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.fgputil.web.SessionProxy; import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.exception.InvalidTokenException; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.events.NewUserEvent; import org.gusdb.wdk.model.Utilities; @@ -60,16 +63,6 @@ public class SessionService extends AbstractWdkService { private static final String OAUTH_CODE_KEY = "code"; private static final String OAUTH_REDIRECT_URL_KEY = "redirectUrl"; - // input param constants - private static final String COOKIE_KEY = "wdkLoginCookieValue"; - - // json output constants for cookie verification endpoint - private static final String IS_VALID_KEY = "isValid"; - private static final String USER_DATA_KEY = "userData"; - private static final String USER_ID_KEY = "id"; - private static final String DISPLAY_NAME_KEY = "displayName"; - private static final String EMAIL_KEY = "email"; - // redirect URLs private static final String OAUTH_ERROR_URL = "/app/user/message/login-error?requestUrl="; @@ -185,12 +178,13 @@ public Response processOauthLogin( @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer, String body) - throws RequestMisformatException { + throws RequestMisformatException, WdkModelException { try { WdkModel wdkModel = getWdkModel(); // RRD 11/17: Allow sites configured to use OAuth (e.g. live sites) to log in users with this endpoint; - // Reason: logging in this way will be easier for programmatic service access that requires login + // Reason: logging in this way will be easier for programmatic service access that requires login, + // including using local web client implementation for testing //if (!AuthenticationMethod.USER_DB.equals(modelConfig.getAuthenticationMethodEnum())) { // return Response.status(new MethodNotAllowedStatusType()).build(); //} @@ -221,7 +215,7 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer catch (JSONException e) { throw new RequestMisformatException(e.getMessage()); } - catch (Exception ex) { + catch (InvalidTokenException ex) { LOG.error("Could not authenticate user's identity. Exception thrown: ", ex); return createJsonResponse(false, "Invalid username or password", null).build(); } @@ -255,7 +249,7 @@ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, Us session.setAttribute(Utilities.WDK_USER_KEY, newUser); - Events.triggerAndWait(new NewUserEvent(newUser, oldUser, session), + Events.triggerAndWait(new NewUserEvent(newUser, oldUser), new WdkRuntimeException("Unable to complete WDK user assignement.")); // 3-year expiration (should change secret key before then) @@ -293,15 +287,14 @@ public Response processLogout() throws WdkModelException { // get the current session's user, then invalidate the session User oldUser = getRequestingUser(); - getSession().invalidate(); + getSession().invalidate(); // legacy // get a new session and add new guest user to it - SessionProxy session = getSession(); - User newUser = getWdkModel().getUserFactory().createUnregisteredUser(); - session.setAttribute(Utilities.WDK_USER_KEY, newUser); + TwoTuple newUser = getWdkModel().getUserFactory().createUnregisteredUser(); + getSession().setAttribute(Utilities.WDK_USER_KEY, newUser.getSecond()); // throw new user event - Events.triggerAndWait(new NewUserEvent(newUser, oldUser, session), + Events.triggerAndWait(new NewUserEvent(newUser.getSecond(), oldUser), new WdkRuntimeException("Unable to complete WDK user assignement.")); // create and append logout cookies to response @@ -316,9 +309,20 @@ public Response processLogout() throws WdkModelException { for (CookieBuilder logoutCookie : logoutCookies) { builder.cookie(logoutCookie.toJaxRsCookie()); } + + // add new guest auth cookie to response + builder.cookie(getAuthCookie(newUser.getFirst().getTokenValue())); + return builder.build(); } + public static NewCookie getAuthCookie(String tokenValue) { + CookieBuilder cookie = new CookieBuilder(HttpHeaders.AUTHORIZATION, tokenValue); + cookie.setMaxAge(SessionService.EXPIRATION_3_YEARS_SECS); + cookie.setPath("/"); + return cookie.toJaxRsCookie(); + } + /** * Convenience method to set up a response builder that returns JSON containing request result * @@ -361,90 +365,4 @@ private static boolean isEmpty(String str) { return str == null || str.trim().isEmpty(); } - /** - * Web service action that takes parts of a WDK login cookie and verifies that they indeed represent a valid - * cookie for an existing WDK user. Returns user's display name and email address for use by caller if - * valid. - * - * A JSON object like the following is returned with the information. If the cookie is invalid, the isValid - * property is set to false and the userData property is undefined. - * - * { - * "isValid": true, - * "userData": { - * "id": 7145453, - * "displayName": "Ryan Doherty", - * "email": "rdoherty@pcbi.upenn.edu" - * } - * } - */ - @GET - @Path("login/verification") - @Produces(MediaType.APPLICATION_JSON) - public Response processLoginVerification(@QueryParam(COOKIE_KEY) String cookieValue) throws WdkModelException { - WdkModel wdkModel = getWdkModel(); - return Response.ok( - getVerificationJsonResult( - getVerifiedUsername(cookieValue, wdkModel.getModelConfig().getSecretKey()), wdkModel) - ).build(); - } - - /** - * Fetches the cookie value parameter, verifies its validity, and returns the username (email) contained - * within the cookie value. If the cookie is invalid, null is returned - * - * @param cookieValue value of the cookie - * @param secretKey key used to create user hash - * @return username, or null if not valid - * @throws WdkModelException if a system problem occurs - */ - private static String getVerifiedUsername(String cookieValue, String secretKey) throws WdkModelException { - try { - String recreatedCookie = cookieValue == null ? "" : cookieValue; - LoginCookieParts cookieParts = LoginCookieFactory.parseCookieValue(recreatedCookie); - LoginCookieFactory auth = new LoginCookieFactory(secretKey); - return (auth.isValidCookie(cookieParts) ? cookieParts.getUsername() : null); - } - catch (IllegalArgumentException e) { - LOG.warn("Unable to parse cookie value param.", e); - return null; - } - } - - /** - * Generates the appropriate JSON object given the username parsed from the cookie value (if any). Even if a - * non-null username was retrieved, the cookie value may still be deemed invalid if the username does not - * correspond to an existing user. - * - * @param username username to generate JSON for (or null if no username was able to be parsed) - * @param wdkModel WDK model - * @return response JSON - * @throws WdkModelException if a system problem occurs - */ - private static String getVerificationJsonResult(String username, WdkModel wdkModel) throws WdkModelException { - try { - JSONObject result = new JSONObject(); - boolean isValid = (username != null); - if (isValid) { - // cookie seems valid; try to get user's name and email - User user = wdkModel.getUserFactory().getUserByEmail(username); - if (user == null) { - // user does not exist; cookie is invalid - isValid = false; - } - else { - result - .put(USER_DATA_KEY, new JSONObject() - .put(USER_ID_KEY, user.getUserId()) - .put(DISPLAY_NAME_KEY, user.getDisplayName()) - .put(EMAIL_KEY, user.getEmail())); - isValid = true; - } - } - return result.put(IS_VALID_KEY, isValid).toString(); - } - catch (JSONException e) { - throw new WdkModelException("Unable to generate JSON object from data.", e); - } - } } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index f7c8c2a2ca..1a16353d4a 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -15,8 +15,10 @@ import javax.ws.rs.core.Response; import org.gusdb.fgputil.web.LoginCookieFactory; +import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.user.BasicUser; import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserPreferenceFactory; @@ -33,8 +35,6 @@ public class ProfileService extends UserService { - private static final String DUPLICATE_EMAIL = "This email is already in use by another account. Please choose another."; - public ProfileService(@PathParam(USER_ID_PATH_PARAM) String uid) { super(uid); } @@ -78,91 +78,31 @@ protected JSONObject formatUser(User user, boolean isSessionUser, boolean includ public Response setUserProfile(String body) throws ConflictException, DataValidationException, WdkModelException { try { - User user = getPrivateRegisteredUser(); + User oldUser = getPrivateRegisteredUser(); UserProfileRequest request = UserProfileRequest.createFromJson( - new JSONObject(body), getPropertiesConfig(), true); - NewCookie loginCookie = processEmail(user, request.getEmail()); + new JSONObject(body), User.USER_PROPERTIES.values(), true); + // overwrite user's old email and profile - user.setEmail(request.getEmail()); - user.setProfileProperties(request.getProfileMap()); - // save user to DB - getWdkModel().getUserFactory().saveUser(user); + User newUser = new BasicUser(oldUser); + newUser.setEmail(request.getEmail()); + newUser.setProfileProperties(request.getProfileMap()); + + // save user to OAuth + newUser = getWdkModel().getUserFactory().saveUser(oldUser, newUser, getAuthorizationToken()); + // save user to session - getSession().setAttribute(Utilities.WDK_USER_KEY, user); - return getProfileUpdateResponse(loginCookie); + getSession().setAttribute(Utilities.WDK_USER_KEY, newUser); + + return Response.noContent().build(); } catch(JSONException | RequestMisformatException e) { throw new BadRequestException(e); } - catch (InvalidUsernameOrEmailException e) { + catch (InvalidPropertiesException e) { throw new DataValidationException(e.getMessage()); } - } - - /** - * Web service to replace profile and profile properties of existing user with those provided in the - * request. If the properties object is present but not populated, the profile properties will be removed. - * @param body - * @return - 204 - Success without content - * @throws DataValidationException - in the event of a - * @throws WdkModelException - in the event of a server error - */ - @PATCH - @Consumes(MediaType.APPLICATION_JSON) - public Response updateUserProfile(String body) - throws ConflictException, DataValidationException, WdkModelException { - try { - User user = getPrivateRegisteredUser(); - UserProfileRequest request = UserProfileRequest.createFromJson( - new JSONObject(body), User.USER_PROPERTIES, false); - NewCookie loginCookie = processEmail(user, request.getEmail()); - // overwrite any provided properties - if (request.getEmail() != null) { - user.setEmail(request.getEmail()); - } - for (Entry newProp : request.getProfileMap().entrySet()) { - user.setProfileProperty(newProp.getKey(), newProp.getValue()); - } - // save user to DB - getWdkModel().getUserFactory().saveUser(user); - // save user to session - getSession().setAttribute(Utilities.WDK_USER_KEY, user); - return getProfileUpdateResponse(loginCookie); - } - catch(JSONException | RequestMisformatException e) { - throw new BadRequestException(e); - } catch (InvalidUsernameOrEmailException e) { - throw new DataValidationException(e.getMessage(), e); - } - } - - private Response getProfileUpdateResponse(NewCookie loginCookie) { - return loginCookie == null ? - Response.noContent().build() : - Response.noContent().cookie(loginCookie).build(); - } - - /** - * Ensures that a new email address, if provided, does not duplicate that of another account. If - * the new email passes validation, a new cookie is returned to be used for authentication following the - * email address update. - * @param user - the subject of a put or patch - * @param email - the provided email - * @throws WdkModelException - * @throws ConflictException - thrown if the provided email address duplicates that of another account. - */ - protected NewCookie processEmail(User user, String email) throws ConflictException, WdkModelException { - // Check to see if this email address matches that of the given user. If so, no need to check further. - if (email != null && !email.equalsIgnoreCase(user.getEmail())) { - User emailUser = getWdkModel().getUserFactory().getUserByEmail(email); - // Check if the new email address is on record with a different user - if (emailUser != null && emailUser.getUserId() != user.getUserId()) { - throw new ConflictException(DUPLICATE_EMAIL); - } - LoginCookieFactory cookieFactory = new LoginCookieFactory(getWdkModel().getModelConfig().getSecretKey()); - return cookieFactory.createLoginCookie(email, LoginCookieFactory.getDefaultMaxAge()).toJaxRsCookie(); + throw new ConflictException(e.getMessage()); } - return null; } } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java index 706b98a3c4..11e35109f9 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java @@ -1,10 +1,10 @@ package org.gusdb.wdk.service.service.user; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import javax.ws.rs.Consumes; @@ -14,24 +14,25 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.gusdb.fgputil.accountdb.AccountManager; -import org.gusdb.fgputil.accountdb.UserPropertyName; +import org.gusdb.fgputil.functional.Functions; import org.gusdb.fgputil.json.JsonIterators; import org.gusdb.fgputil.json.JsonType; import org.gusdb.fgputil.json.JsonType.ValueType; import org.gusdb.oauth2.client.veupathdb.UserProperty; +import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; import org.gusdb.wdk.model.user.User; -import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.model.user.UserPreferenceFactory; import org.gusdb.wdk.model.user.UserPreferences; +import org.gusdb.wdk.service.request.exception.ConflictException; import org.gusdb.wdk.service.request.exception.DataValidationException; import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.gusdb.wdk.service.request.user.UserCreationRequest; import org.gusdb.wdk.service.service.AbstractWdkService; +import org.gusdb.wdk.session.WdkOAuthClientWrapper; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -51,15 +52,16 @@ public class UserUtilityServices extends AbstractWdkService { * @throws RequestMisformatException if JSON is misformatted * @throws DataValidationException if request contains invalid information * @throws WdkModelException if error occurs creating user + * @throws ConflictException if input conflicts with existing data (i.e. duplicate email or username) */ @POST @Path(USERS_PATH) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public Response createNewUser(String body) throws RequestMisformatException, DataValidationException, WdkModelException { + public Response createNewUser(String body) throws RequestMisformatException, DataValidationException, WdkModelException, ConflictException { try { JSONObject requestJson = new JSONObject(body); - Collection configuredUserProps = User.getPropertyDefs(); + Collection configuredUserProps = User.USER_PROPERTIES.values(); UserCreationRequest request = UserCreationRequest.createFromJson(requestJson, configuredUserProps); // create the user, saving to DB @@ -77,6 +79,9 @@ public Response createNewUser(String body) throws RequestMisformatException, Dat .build(); } catch (InvalidUsernameOrEmailException e) { + throw new ConflictException(e.getMessage()); + } + catch (InvalidPropertiesException e) { throw new DataValidationException(e.getMessage()); } catch (JSONException e) { @@ -101,19 +106,16 @@ public Response resetUserPassword(String body) throws WdkModelException, DataValidationException, RequestMisformatException { try { JSONObject json = new JSONObject(body); - String emailOrLoginName = json.getString(JsonKeys.EMAIL); - UserFactory userMgr = getWdkModel().getUserFactory(); - try { - userMgr.resetPassword(emailOrLoginName); - } - catch (WdkUserException e) { - throw new DataValidationException(NO_USER_BY_THAT_LOGIN); - } + String loginName = json.getString(JsonKeys.EMAIL); // email, but also now supports username + getWdkModel().getUserFactory().resetPassword(loginName); return Response.noContent().build(); } catch (JSONException e) { throw new RequestMisformatException(e.toString()); } + catch (WdkUserException e) { + throw new DataValidationException(NO_USER_BY_THAT_LOGIN); + } } /** @@ -140,15 +142,11 @@ public Response lookupUserId(String body) throws RequestMisformatException { throw new RequestMisformatException("The user email list provided must be an array of strings."); } } - ModelConfigAccountDB accountDbConfig = getWdkModel().getModelConfig().getAccountDB(); - AccountManager accountManager = new AccountManager(getWdkModel().getAccountDb(), - accountDbConfig.getAccountSchema(), accountDbConfig.getUserPropertyNames()); - Map userEmailIdMap = accountManager.lookUpUserIdsByEmail(userEmails); - JSONArray jsonUserList = new JSONArray(); - for (Entry mapping : userEmailIdMap.entrySet()) { - jsonUserList.put(new JSONObject().put(mapping.getKey(), mapping.getValue())); - } - return Response.ok(new JSONObject().put("results", jsonUserList).toString()).build(); + WdkOAuthClientWrapper oauth = new WdkOAuthClientWrapper(getWdkModel()); + Map userMap = oauth.getUsersByEmail(new ArrayList<>(userEmails)); + Map idMap = Functions.getMapFromKeys(userMap.keySet(), + email -> Optional.ofNullable(userMap.get(email)).map(User::getUserId).orElse(null)); + return Response.ok(new JSONObject().put("results", idMap).toString()).build(); } catch (JSONException e) { throw new RequestMisformatException(e.getMessage()); From fa8c77847845f09abc436fbc8087ba42fd734373 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 21 Feb 2024 00:41:05 -0500 Subject: [PATCH 12/25] Comply with OAuth upgrade in install --- .../java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java | 7 +------ .../org/gusdb/wdk/service/service/user/ProfileService.java | 6 +----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java index 000fb2a3e7..a399e3a3cb 100644 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java @@ -66,12 +66,7 @@ public Map getUsersByEmail(List emails) { } public ValidatedToken getNewGuestToken() { - try { - return _client.getNewGuestToken(_config); - } - catch (InvalidTokenException e) { - throw new IllegalStateException(e); // this will go away with the next OAuth release - } + return _client.getNewGuestToken(_config); } private TwoTuple parseExpandedUserJson(JSONObject userJson) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index 1a16353d4a..595b07c924 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -1,6 +1,5 @@ package org.gusdb.wdk.service.service.user; -import java.util.Map.Entry; import java.util.Optional; import javax.ws.rs.BadRequestException; @@ -11,10 +10,8 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; -import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModelException; @@ -24,7 +21,6 @@ import org.gusdb.wdk.model.user.UserPreferenceFactory; import org.gusdb.wdk.model.user.UserPreferences; import org.gusdb.wdk.service.UserBundle; -import org.gusdb.wdk.service.annotation.PATCH; import org.gusdb.wdk.service.formatter.UserFormatter; import org.gusdb.wdk.service.request.exception.ConflictException; import org.gusdb.wdk.service.request.exception.DataValidationException; @@ -85,7 +81,7 @@ public Response setUserProfile(String body) // overwrite user's old email and profile User newUser = new BasicUser(oldUser); newUser.setEmail(request.getEmail()); - newUser.setProfileProperties(request.getProfileMap()); + newUser.setPropertyValues(request.getProfileMap()); // save user to OAuth newUser = getWdkModel().getUserFactory().saveUser(oldUser, newUser, getAuthorizationToken()); From 54d7c3cdcddfc13dcb00681adfc835e7a2acf772 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Wed, 21 Feb 2024 21:32:26 -0500 Subject: [PATCH 13/25] Merge WdkOAuthClientWrapper into UserFactory, but split off management of the DB users table and password emails into their own classes --- .../gusdb/wdk/model/user/BearerTokenUser.java | 7 +- .../gusdb/wdk/model/user/FavoriteFactory.java | 2 +- .../org/gusdb/wdk/model/user/UserFactory.java | 267 +++++++----------- .../wdk/model/user/UserPasswordEmailer.java | 53 ++++ .../wdk/model/user/UserReferenceFactory.java | 112 ++++++++ .../wdk/session/WdkOAuthClientWrapper.java | 114 -------- .../wdk/service/filter/CheckLoginFilter.java | 12 +- .../service/service/AbstractWdkService.java | 6 + .../wdk/service/service/SessionService.java | 17 +- .../service/user/UserUtilityServices.java | 4 +- 10 files changed, 294 insertions(+), 300 deletions(-) create mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java create mode 100644 Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java delete mode 100644 Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java index 040c3dc941..23e9348f35 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java @@ -1,15 +1,16 @@ package org.gusdb.wdk.model.user; +import org.gusdb.oauth2.client.OAuthClient; +import org.gusdb.oauth2.client.OAuthConfig; import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.session.WdkOAuthClientWrapper; public class BearerTokenUser extends org.gusdb.oauth2.client.veupathdb.BearerTokenUser implements User { private final WdkModel _wdkModel; - public BearerTokenUser(WdkModel wdkModel, WdkOAuthClientWrapper client, ValidatedToken token) { - super(client.getOAuthClient(), client.getOAuthConfig().getOauthUrl(), token); + public BearerTokenUser(WdkModel wdkModel, OAuthClient client, OAuthConfig config, ValidatedToken token) { + super(client, config.getOauthUrl(), token); _wdkModel = wdkModel; } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/FavoriteFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/FavoriteFactory.java index f4ceb2d6f2..01fdc3c71a 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/FavoriteFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/FavoriteFactory.java @@ -7,7 +7,7 @@ import static org.gusdb.fgputil.functional.Functions.mapToList; import static org.gusdb.fgputil.functional.Functions.mapToListWithIndex; import static org.gusdb.wdk.model.Utilities.COLUMN_PK_PREFIX; -import static org.gusdb.wdk.model.user.UserFactory.USER_SCHEMA_MACRO; +import static org.gusdb.wdk.model.user.UserReferenceFactory.USER_SCHEMA_MACRO; import java.sql.ResultSet; import java.sql.SQLException; diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index 80d5b384fe..bfed400183 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -1,30 +1,29 @@ package org.gusdb.wdk.model.user; -import java.sql.Timestamp; -import java.sql.Types; import java.util.ArrayList; -import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; import java.util.stream.Collectors; import org.apache.log4j.Logger; -import org.gusdb.fgputil.Tuples.ThreeTuple; import org.gusdb.fgputil.Tuples.TwoTuple; -import org.gusdb.fgputil.db.runner.SQLRunner; -import org.gusdb.fgputil.db.runner.SQLRunnerException; import org.gusdb.fgputil.events.Events; +import org.gusdb.oauth2.client.OAuthClient; +import org.gusdb.oauth2.client.OAuthConfig; import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.client.veupathdb.OAuthQuerier; +import org.gusdb.oauth2.client.veupathdb.UserProperty; +import org.gusdb.oauth2.exception.ConflictException; import org.gusdb.oauth2.exception.InvalidPropertiesException; +import org.gusdb.oauth2.exception.InvalidTokenException; +import org.gusdb.oauth2.shared.IdTokenFields; import org.gusdb.wdk.events.UserProfileUpdateEvent; -import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.config.ModelConfig; -import org.gusdb.wdk.session.WdkOAuthClientWrapper; +import org.json.JSONObject; import io.prometheus.client.Counter; @@ -46,63 +45,14 @@ public class UserFactory { .register(); // ------------------------------------------------------------------------- - // database table and column definitions - // ------------------------------------------------------------------------- - - public static final String TABLE_USERS = "users"; - public static final String COL_USER_ID = "user_id"; - public static final String COL_IS_GUEST = "is_guest"; - public static final String COL_FIRST_ACCESS = "first_access"; - - // ------------------------------------------------------------------------- - // sql and sql macro definitions - // ------------------------------------------------------------------------- - - static final String USER_SCHEMA_MACRO = "$$USER_SCHEMA$$"; - private static final String IS_GUEST_VALUE_MACRO = "$$IS_GUEST$$"; - - // SQL and types to insert previously unknown user refs into the users table - private static final String INSERT_USER_REF_SQL = - "insert" + - " when not exists (select 1 from " + USER_SCHEMA_MACRO + TABLE_USERS + " where " + COL_USER_ID + " = ?)" + - " then" + - " into " + USER_SCHEMA_MACRO + TABLE_USERS + " (" + COL_USER_ID + "," + COL_IS_GUEST + "," + COL_FIRST_ACCESS +")" + - " select ?, " + IS_GUEST_VALUE_MACRO + ", ? from dual"; - - private static final Integer[] INSERT_USER_REF_PARAM_TYPES = { Types.BIGINT, Types.BIGINT, Types.TIMESTAMP }; - - // SQL and types to select user ref by ID - private static final String SELECT_USER_REF_BY_ID_SQL = - "select " + COL_USER_ID + ", " + COL_IS_GUEST + ", " + COL_FIRST_ACCESS + - " from " + USER_SCHEMA_MACRO + TABLE_USERS + - " where " + COL_USER_ID + " = ?"; - - private static final Integer[] SELECT_USER_REF_BY_ID_PARAM_TYPES = { Types.BIGINT }; - - private static class UserReference extends ThreeTuple { - public UserReference(Long userId, Boolean isGuest, Date firstAccess) { - super(userId, isGuest, firstAccess); - } - public Long getUserId() { return getFirst(); } - public Boolean isGuest() { return getSecond(); } - public Date getFirstAccess() { return getThird(); } - } - - // ------------------------------------------------------------------------- - // the macros used by the registration email - // ------------------------------------------------------------------------- - - private static final String EMAIL_MACRO_USER_NAME = "USER_NAME"; - private static final String EMAIL_MACRO_EMAIL = "EMAIL"; - private static final String EMAIL_MACRO_PASSWORD = "PASSWORD"; - - // ------------------------------------------------------------------------- - // member variables + // member fields // ------------------------------------------------------------------------- private final WdkModel _wdkModel; - private final String _userSchema; - private final WdkOAuthClientWrapper _client; + private final UserReferenceFactory _userRefFactory; + private final UserPasswordEmailer _emailer; + private final OAuthConfig _config; + private final OAuthClient _client; // ------------------------------------------------------------------------- // constructor @@ -111,113 +61,126 @@ public UserReference(Long userId, Boolean isGuest, Date firstAccess) { public UserFactory(WdkModel wdkModel) { // save model for populating new users _wdkModel = wdkModel; - _userSchema = wdkModel.getModelConfig().getUserDB().getUserSchema(); - _client = new WdkOAuthClientWrapper(wdkModel); + _userRefFactory = new UserReferenceFactory(wdkModel); + _emailer = new UserPasswordEmailer(wdkModel); + _config = wdkModel.getModelConfig(); + _client = new OAuthClient(OAuthClient.getTrustManager(wdkModel.getModelConfig())); } // ------------------------------------------------------------------------- - // methods + // methods to manage tokens // ------------------------------------------------------------------------- - public User createUser(String email, Map profileProperties) - throws WdkModelException, InvalidPropertiesException, InvalidUsernameOrEmailException { + public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) throws InvalidTokenException { + return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); + } - // contact OAuth server to create a new user with the passed props - TwoTuple userTuple = _client.createUser(email, profileProperties); - User user = userTuple.getFirst(); + public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) throws InvalidTokenException { + return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); + } - // add user to this user DB (will be added to other user DBs as needed during login) - addUserReference(user); + public ValidatedToken validateBearerToken(String rawToken) throws InvalidTokenException { + return _client.getValidatedEcdsaSignedToken(_config.getOauthUrl(), rawToken); + } - // if needed, send user temporary password via email - if (isSendWelcomeEmail()) { - emailTemporaryPassword(user, userTuple.getSecond(), _wdkModel.getModelConfig()); - } + // ------------------------------------------------------------------------- + // methods to manage users + // ------------------------------------------------------------------------- + public User convertToUser(ValidatedToken token) throws WdkModelException { + User user = new BearerTokenUser(_wdkModel, _client, _config, token); + _userRefFactory.addUserReference(user); return user; } - /** - * @return whether or not WDK is configured to send a welcome email to new registered users (defaults to true) - */ - private boolean isSendWelcomeEmail() { - String dontEmailProp = _wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); - return dontEmailProp == null || !dontEmailProp.equals("true"); + public TwoTuple createUnregisteredUser() throws WdkModelException { + ValidatedToken token = _client.getNewGuestToken(_config); + GUEST_CREATION_COUNTER.inc(); + return new TwoTuple<>(token, convertToUser(token)); } - private static void emailTemporaryPassword(User user, String password, - ModelConfig modelConfig) throws WdkModelException { - - String smtpServer = modelConfig.getSmtpServer(); - String supportEmail = modelConfig.getSupportEmail(); - String emailSubject = modelConfig.getEmailSubject(); + public User createUser(String email, Map profileProperties) + throws WdkModelException, InvalidPropertiesException, InvalidUsernameOrEmailException { + try { - // populate email content macros with user data - String emailContent = modelConfig.getEmailContent() - .replaceAll("\\$\\$" + EMAIL_MACRO_USER_NAME + "\\$\\$", - Matcher.quoteReplacement(user.getDisplayName())) - .replaceAll("\\$\\$" + EMAIL_MACRO_EMAIL + "\\$\\$", - Matcher.quoteReplacement(user.getEmail())) - .replaceAll("\\$\\$" + EMAIL_MACRO_PASSWORD + "\\$\\$", - Matcher.quoteReplacement(password)); + // contact OAuth server to create a new user with the passed props + Map allProps = new HashMap<>(profileProperties); + allProps.put(IdTokenFields.email.name(), email); + TwoTuple userTuple = parseExpandedUserJson(_client.createNewUser(_config, allProps)); + + User user = userTuple.getFirst(); + String password = userTuple.getSecond(); + + // add user to this user DB (will be added to other user DBs as needed during login) + _userRefFactory.addUserReference(user); + + // if needed, send user temporary password via email + if (_emailer.isSendWelcomeEmail()) { + _emailer.emailTemporaryPassword(user, password); + } + + return user; + } + catch (ConflictException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); + } + } - Utilities.sendEmail(smtpServer, user.getEmail(), supportEmail, emailSubject, emailContent); + private TwoTuple parseExpandedUserJson(JSONObject userJson) { + User user = new BasicUser(_wdkModel, userJson); + String password = userJson.getString(IdTokenFields.password.name()); + return new TwoTuple<>(user, password); } /** - * Adds a user reference row to the UserDB users table if one does not exist. - * Note is_guest and first_access are immutable fields and once set will not be - * changed by this code. - * - * @param user user to add - * @throws WdkModelException + * Save the basic information of a user + * + * @param user + * @param newUser + * @throws InvalidPropertiesException */ - public int addUserReference(User user) throws WdkModelException { + public User saveUser(User oldUser, User newUser, ValidatedToken authorizationToken) throws InvalidUsernameOrEmailException, InvalidPropertiesException { try { - long userId = user.getUserId(); - boolean isGuest = user.isGuest(); - Timestamp insertedOn = new Timestamp(new Date().getTime()); - String sql = INSERT_USER_REF_SQL - .replace(USER_SCHEMA_MACRO, _userSchema) - .replace(IS_GUEST_VALUE_MACRO, _wdkModel.getUserDb().getPlatform().convertBoolean(isGuest).toString()); - return new SQLRunner(_wdkModel.getUserDb().getDataSource(), sql, "insert-user-ref") - .executeUpdate(new Object[]{ userId, userId, insertedOn }, INSERT_USER_REF_PARAM_TYPES); + // build map of user props + email to send to OAuth + Map props = new HashMap<>(); + props.put(IdTokenFields.email.name(), newUser.getEmail()); + for (UserProperty prop : User.USER_PROPERTIES.values()) { + props.put(prop.getName(), prop.getValue(newUser)); + } + + // build a user from the response + User savedUser = new BasicUser(_wdkModel, _client.modifyUser(_config, authorizationToken, props)); + + Events.trigger(new UserProfileUpdateEvent(oldUser, savedUser, _wdkModel)); + return savedUser; } - catch (SQLRunnerException e) { - throw WdkModelException.translateFrom(e); + catch (ConflictException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); } } - // FIXME: see if this is actually needed anywhere? E.g. do we ever need to look up user refs by user ID to find last login? - private Optional getUserReference(long userId) throws WdkModelException { + public void resetPassword(String loginName) throws InvalidUsernameOrEmailException, WdkModelException { try { - String sql = SELECT_USER_REF_BY_ID_SQL.replace(USER_SCHEMA_MACRO, _userSchema); - return new SQLRunner(_wdkModel.getUserDb().getDataSource(), sql, "get-user-ref").executeQuery( - new Object[]{ userId }, - SELECT_USER_REF_BY_ID_PARAM_TYPES, - rs -> - !rs.next() - ? Optional.empty() - : Optional.of(new UserReference( - rs.getLong(COL_USER_ID), - rs.getBoolean(COL_IS_GUEST), - new Date(rs.getTimestamp(COL_FIRST_ACCESS).getTime())))); + TwoTuple user = parseExpandedUserJson(_client.resetPassword(_config, loginName)); + + // email user new password + _emailer.emailTemporaryPassword(user.getFirst(), user.getSecond()); } - catch (SQLRunnerException e) { - throw WdkModelException.translateFrom(e); + catch (InvalidPropertiesException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); } } - public TwoTuple createUnregisteredUser() { - ValidatedToken token = _client.getNewGuestToken(); - User user = new BearerTokenUser(_wdkModel, _client, token); - GUEST_CREATION_COUNTER.inc(); - return new TwoTuple<>(token, user); + // ------------------------------------------------------------------------- + // methods to query users + // ------------------------------------------------------------------------- + + public Map getUsersById(List userIds) { + return OAuthQuerier.getUsersById(_client, _config, userIds, json -> new BasicUser(_wdkModel, json)); } - public Map verifyUserids(Set userIds) { - Map userMap = _client.getUsersById(new ArrayList<>(userIds)); - return userIds.stream().collect(Collectors.toMap(id -> id, id -> userMap.get(id) != null)); + public Map getUsersByEmail(List emails) { + return OAuthQuerier.getUsersByEmail(_client, _config, emails, json -> new BasicUser(_wdkModel, json)); } /** @@ -228,31 +191,15 @@ public Map verifyUserids(Set userIds) { * @throws WdkModelException if an error occurs in the attempt */ public Optional getUserById(long userId) throws WdkModelException { - return Optional.ofNullable(_client.getUsersById(List.of(userId)).get(userId)); + return Optional.ofNullable(getUsersById(List.of(userId)).get(userId)); } public Optional getUserByEmail(String email) { - return Optional.ofNullable(_client.getUsersByEmail(List.of(email)).get(email)); - } - - /** - * Save the basic information of a user - * - * @param user - * @param newUser - * @throws InvalidPropertiesException - */ - public User saveUser(User oldUser, User newUser, ValidatedToken authorizationToken) throws InvalidUsernameOrEmailException, InvalidPropertiesException { - User savedUser = new BasicUser(_wdkModel, _client.updateUser(newUser, authorizationToken)); - Events.trigger(new UserProfileUpdateEvent(oldUser, newUser, _wdkModel)); - return savedUser; + return Optional.ofNullable(getUsersByEmail(List.of(email)).get(email)); } - public void resetPassword(String loginName) throws InvalidUsernameOrEmailException, WdkModelException { - TwoTuple user = _client.resetPassword(loginName); - - // email user new password - emailTemporaryPassword(user.getFirst(), user.getSecond(), _wdkModel.getModelConfig()); + public Map verifyUserids(Set userIds) { + Map userMap = getUsersById(new ArrayList<>(userIds)); + return userIds.stream().collect(Collectors.toMap(id -> id, id -> userMap.get(id) != null)); } - } diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java new file mode 100644 index 0000000000..bf7c0f38bd --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java @@ -0,0 +1,53 @@ +package org.gusdb.wdk.model.user; + +import java.util.regex.Matcher; + +import org.gusdb.wdk.model.Utilities; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkModelException; +import org.gusdb.wdk.model.config.ModelConfig; + +public class UserPasswordEmailer { + + // ------------------------------------------------------------------------- + // the macros used by the registration email + // ------------------------------------------------------------------------- + + private static final String EMAIL_MACRO_USER_NAME = "USER_NAME"; + private static final String EMAIL_MACRO_EMAIL = "EMAIL"; + private static final String EMAIL_MACRO_PASSWORD = "PASSWORD"; + + private final boolean _sendWelcomeEmail; + private final ModelConfig _wdkModelConfig; + + public UserPasswordEmailer(WdkModel wdkModel) { + _wdkModelConfig = wdkModel.getModelConfig(); + + // whether or not WDK is configured to send a welcome email to new registered users (defaults to true) + String dontEmailProp = wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); + _sendWelcomeEmail = dontEmailProp == null || !dontEmailProp.equals("true"); + } + + public boolean isSendWelcomeEmail() { + return _sendWelcomeEmail; + } + + public void emailTemporaryPassword(User user, String password) throws WdkModelException { + if (!_sendWelcomeEmail) return; + + String smtpServer = _wdkModelConfig.getSmtpServer(); + String supportEmail = _wdkModelConfig.getSupportEmail(); + String emailSubject = _wdkModelConfig.getEmailSubject(); + + // populate email content macros with user data + String emailContent = _wdkModelConfig.getEmailContent() + .replaceAll("\\$\\$" + EMAIL_MACRO_USER_NAME + "\\$\\$", + Matcher.quoteReplacement(user.getDisplayName())) + .replaceAll("\\$\\$" + EMAIL_MACRO_EMAIL + "\\$\\$", + Matcher.quoteReplacement(user.getEmail())) + .replaceAll("\\$\\$" + EMAIL_MACRO_PASSWORD + "\\$\\$", + Matcher.quoteReplacement(password)); + + Utilities.sendEmail(smtpServer, user.getEmail(), supportEmail, emailSubject, emailContent); + } +} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java new file mode 100644 index 0000000000..00ea2cf98b --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java @@ -0,0 +1,112 @@ +package org.gusdb.wdk.model.user; + +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Date; +import java.util.Optional; + +import org.gusdb.fgputil.Tuples.ThreeTuple; +import org.gusdb.fgputil.db.pool.DatabaseInstance; +import org.gusdb.fgputil.db.runner.SQLRunner; +import org.gusdb.fgputil.db.runner.SQLRunnerException; +import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkModelException; + +class UserReferenceFactory { + + // ------------------------------------------------------------------------- + // database table and column definitions + // ------------------------------------------------------------------------- + + public static final String TABLE_USERS = "users"; + public static final String COL_USER_ID = "user_id"; + public static final String COL_IS_GUEST = "is_guest"; + public static final String COL_FIRST_ACCESS = "first_access"; + + // ------------------------------------------------------------------------- + // sql and sql macro definitions + // ------------------------------------------------------------------------- + + public static final String USER_SCHEMA_MACRO = "$$USER_SCHEMA$$"; + private static final String IS_GUEST_VALUE_MACRO = "$$IS_GUEST$$"; + + // SQL and types to insert previously unknown user refs into the users table + private static final String INSERT_USER_REF_SQL = + "insert" + + " when not exists (select 1 from " + USER_SCHEMA_MACRO + TABLE_USERS + " where " + COL_USER_ID + " = ?)" + + " then" + + " into " + USER_SCHEMA_MACRO + TABLE_USERS + " (" + COL_USER_ID + "," + COL_IS_GUEST + "," + COL_FIRST_ACCESS +")" + + " select ?, " + IS_GUEST_VALUE_MACRO + ", ? from dual"; + + private static final Integer[] INSERT_USER_REF_PARAM_TYPES = { Types.BIGINT, Types.BIGINT, Types.TIMESTAMP }; + + // SQL and types to select user ref by ID + private static final String SELECT_USER_REF_BY_ID_SQL = + "select " + COL_USER_ID + ", " + COL_IS_GUEST + ", " + COL_FIRST_ACCESS + + " from " + USER_SCHEMA_MACRO + TABLE_USERS + + " where " + COL_USER_ID + " = ?"; + + private static final Integer[] SELECT_USER_REF_BY_ID_PARAM_TYPES = { Types.BIGINT }; + + private static class UserReference extends ThreeTuple { + public UserReference(Long userId, Boolean isGuest, Date firstAccess) { + super(userId, isGuest, firstAccess); + } + public Long getUserId() { return getFirst(); } + public Boolean isGuest() { return getSecond(); } + public Date getFirstAccess() { return getThird(); } + } + + private final DatabaseInstance _userDb; + private final String _userSchema; + + public UserReferenceFactory(WdkModel wdkModel) { + _userDb = wdkModel.getUserDb(); + _userSchema = wdkModel.getModelConfig().getUserDB().getUserSchema(); + } + + /** + * Adds a user reference row to the UserDB users table if one does not exist. + * This is for tracking and for foreign keys on other user DB tables. + * Note is_guest and first_access are immutable fields and once set will not be + * changed by this code. + * + * @param user user to add + * @throws WdkModelException + */ + public int addUserReference(User user) throws WdkModelException { + try { + long userId = user.getUserId(); + boolean isGuest = user.isGuest(); + Timestamp insertedOn = new Timestamp(new Date().getTime()); + String sql = INSERT_USER_REF_SQL + .replace(USER_SCHEMA_MACRO, _userSchema) + .replace(IS_GUEST_VALUE_MACRO, _userDb.getPlatform().convertBoolean(isGuest).toString()); + return new SQLRunner(_userDb.getDataSource(), sql, "insert-user-ref") + .executeUpdate(new Object[]{ userId, userId, insertedOn }, INSERT_USER_REF_PARAM_TYPES); + } + catch (SQLRunnerException e) { + throw WdkModelException.translateFrom(e); + } + } + + // FIXME: see if this is actually needed anywhere? E.g. do we ever need to look up user refs by user ID to find last login? + private Optional getUserReference(long userId) throws WdkModelException { + try { + String sql = SELECT_USER_REF_BY_ID_SQL.replace(USER_SCHEMA_MACRO, _userSchema); + return new SQLRunner(_userDb.getDataSource(), sql, "get-user-ref").executeQuery( + new Object[]{ userId }, + SELECT_USER_REF_BY_ID_PARAM_TYPES, + rs -> + !rs.next() + ? Optional.empty() + : Optional.of(new UserReference( + rs.getLong(COL_USER_ID), + rs.getBoolean(COL_IS_GUEST), + new Date(rs.getTimestamp(COL_FIRST_ACCESS).getTime())))); + } + catch (SQLRunnerException e) { + throw WdkModelException.translateFrom(e); + } + } +} diff --git a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java b/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java deleted file mode 100644 index a399e3a3cb..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/session/WdkOAuthClientWrapper.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.gusdb.wdk.session; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.gusdb.fgputil.Tuples.TwoTuple; -import org.gusdb.oauth2.client.OAuthClient; -import org.gusdb.oauth2.client.OAuthConfig; -import org.gusdb.oauth2.client.ValidatedToken; -import org.gusdb.oauth2.client.veupathdb.OAuthQuerier; -import org.gusdb.oauth2.client.veupathdb.UserProperty; -import org.gusdb.oauth2.exception.ConflictException; -import org.gusdb.oauth2.exception.InvalidPropertiesException; -import org.gusdb.oauth2.exception.InvalidTokenException; -import org.gusdb.oauth2.shared.IdTokenFields; -import org.gusdb.wdk.model.WdkModel; -import org.gusdb.wdk.model.user.BasicUser; -import org.gusdb.wdk.model.user.BearerTokenUser; -import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; -import org.gusdb.wdk.model.user.User; -import org.json.JSONObject; - -public class WdkOAuthClientWrapper { - - private final WdkModel _wdkModel; - private final OAuthConfig _config; - private final OAuthClient _client; - - public WdkOAuthClientWrapper(WdkModel wdkModel) { - _wdkModel = wdkModel; - _config = wdkModel.getModelConfig(); - _client = new OAuthClient(OAuthClient.getTrustManager(wdkModel.getModelConfig())); - } - - public OAuthClient getOAuthClient() { - return _client; - } - - public OAuthConfig getOAuthConfig() { - return _config; - } - - public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) throws InvalidTokenException { - return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); - } - - public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) throws InvalidTokenException { - return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); - } - - public ValidatedToken validateBearerToken(String rawToken) throws InvalidTokenException { - return _client.getValidatedEcdsaSignedToken(_config.getOauthUrl(), rawToken); - } - - public User getUserData(ValidatedToken token) { - return new BearerTokenUser(_wdkModel, this, token); - } - - public Map getUsersById(List userIds) { - return OAuthQuerier.getUsersById(_client, _config, userIds, json -> new BasicUser(_wdkModel, json)); - } - - public Map getUsersByEmail(List emails) { - return OAuthQuerier.getUsersByEmail(_client, _config, emails, json -> new BasicUser(_wdkModel, json)); - } - - public ValidatedToken getNewGuestToken() { - return _client.getNewGuestToken(_config); - } - - private TwoTuple parseExpandedUserJson(JSONObject userJson) { - User user = new BasicUser(_wdkModel, userJson); - String password = userJson.getString(IdTokenFields.password.name()); - return new TwoTuple<>(user, password); - } - - public TwoTuple createUser(String email, Map profileProperties) throws InvalidPropertiesException, InvalidUsernameOrEmailException { - try { - Map allProps = new HashMap<>(profileProperties); - allProps.put(IdTokenFields.email.name(), email); - return parseExpandedUserJson(_client.createNewUser(_config, allProps)); - } - catch (ConflictException e) { - throw new InvalidUsernameOrEmailException(e.getMessage()); - } - } - - public JSONObject updateUser(User user, ValidatedToken token) throws InvalidUsernameOrEmailException, InvalidPropertiesException { - try { - Map props = new HashMap<>(); - props.put(IdTokenFields.email.name(), user.getEmail()); - for (UserProperty prop : User.USER_PROPERTIES.values()) { - props.put(prop.getName(), prop.getValue(user)); - } - return _client.modifyUser(_config, token, props); - } - catch (ConflictException e) { - throw new InvalidUsernameOrEmailException(e.getMessage()); - } - } - - public TwoTuple resetPassword(String loginName) throws InvalidUsernameOrEmailException { - try { - return parseExpandedUserJson(_client.resetPassword(_config, loginName)); - } - catch (InvalidPropertiesException e) { - throw new InvalidUsernameOrEmailException(e.getMessage()); - } - } - - - -} diff --git a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java index 198af43e9c..c6b0b684d4 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java @@ -29,12 +29,10 @@ import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkRuntimeException; -import org.gusdb.wdk.model.user.BearerTokenUser; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.service.service.SessionService; import org.gusdb.wdk.service.service.SystemService; -import org.gusdb.wdk.session.WdkOAuthClientWrapper; @Priority(30) public class CheckLoginFilter implements ContainerRequestFilter, ContainerResponseFilter { @@ -63,24 +61,23 @@ public void filter(ContainerRequestContext requestContext) throws IOException { ApplicationContext context = ContextLookup.getApplicationContext(_servletContext); RequestData request = ContextLookup.getRequest(_servletRequest.get(), _grizzlyRequest.get()); WdkModel wdkModel = ContextLookup.getWdkModel(context); - WdkOAuthClientWrapper oauth = new WdkOAuthClientWrapper(wdkModel); // try to find submitted bearer token String rawToken = findRawBearerToken(request, requestContext); try { + UserFactory factory = wdkModel.getUserFactory(); ValidatedToken token; User user; if (rawToken != null) { // validate submitted token - token = oauth.validateBearerToken(rawToken); - user = new BearerTokenUser(wdkModel, oauth, token); + token = factory.validateBearerToken(rawToken); + user = factory.convertToUser(token); LOG.info("Validated successfully. Request will be processed for user " + user.getUserId() + " / " + user.getEmail()); } else { // no credentials submitted; automatically create a guest to use on this request - UserFactory factory = wdkModel.getUserFactory(); TwoTuple guestPair = factory.createUnregisteredUser(); token = guestPair.getFirst(); user = guestPair.getSecond(); @@ -91,9 +88,6 @@ public void filter(ContainerRequestContext requestContext) throws IOException { requestContext.setProperty(TOKEN_COOKIE_VALUE_TO_SET, token.getTokenValue()); } - // insert reference to this user into user DB for tracking and for foreign keys on other user DB tables - wdkModel.getUserFactory().addUserReference(user); - // set creds and user on the request object for use by this request's processing request.setAttribute(Utilities.BEARER_TOKEN_KEY, token); request.setAttribute(Utilities.WDK_USER_KEY, user); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java index bd8ee85458..22674a7510 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java @@ -137,6 +137,12 @@ protected Map> getHeaders() { return _headers.getRequestHeaders(); } + /** + * Note! Use of session should be extremely limited. WDK is intended to be a + * stateless service. + * + * @return genericized session object (compatible with both Servlet and Grizzly sessions) + */ protected SessionProxy getSession() { return getRequest().getSession(); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index b3b0042aa2..821dcb2c68 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -37,12 +37,11 @@ import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.config.ModelConfig; import org.gusdb.wdk.model.config.ModelConfig.AuthenticationMethod; -import org.gusdb.wdk.model.user.BearerTokenUser; import org.gusdb.wdk.model.user.User; +import org.gusdb.wdk.model.user.UserFactory; import org.gusdb.wdk.service.request.LoginRequest; import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.gusdb.wdk.service.statustype.MethodNotAllowedStatusType; -import org.gusdb.wdk.session.WdkOAuthClientWrapper; import org.json.JSONException; import org.json.JSONObject; @@ -155,10 +154,9 @@ public Response processOauthLogin( } // Use auth code to get the bearer token, then convert to User - WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); - ValidatedToken bearerToken = client.getBearerTokenFromAuthCode(authCode, appUrl); - User newUser = new BearerTokenUser(wdkModel, client, bearerToken); - wdkModel.getUserFactory().addUserReference(newUser); + UserFactory userFactory = wdkModel.getUserFactory(); + ValidatedToken bearerToken = userFactory.getBearerTokenFromAuthCode(authCode, appUrl); + User newUser = userFactory.convertToUser(bearerToken); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); @@ -201,10 +199,9 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer } // Use passed credentials to get the bearer token, then convert to User - WdkOAuthClientWrapper client = new WdkOAuthClientWrapper(wdkModel); - ValidatedToken bearerToken = client.getBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); - User newUser = new BearerTokenUser(wdkModel, client, bearerToken); - wdkModel.getUserFactory().addUserReference(newUser); + UserFactory userFactory = wdkModel.getUserFactory(); + ValidatedToken bearerToken = userFactory.getBearerTokenFromCredentials(request.getEmail(), request.getPassword(), appUrl); + User newUser = userFactory.convertToUser(bearerToken); // transfer ownership from guest to logged-in user transferOwnership(oldUser, newUser, wdkModel); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java index 11e35109f9..c6282e63fc 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/UserUtilityServices.java @@ -32,7 +32,6 @@ import org.gusdb.wdk.service.request.exception.RequestMisformatException; import org.gusdb.wdk.service.request.user.UserCreationRequest; import org.gusdb.wdk.service.service.AbstractWdkService; -import org.gusdb.wdk.session.WdkOAuthClientWrapper; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -142,8 +141,7 @@ public Response lookupUserId(String body) throws RequestMisformatException { throw new RequestMisformatException("The user email list provided must be an array of strings."); } } - WdkOAuthClientWrapper oauth = new WdkOAuthClientWrapper(getWdkModel()); - Map userMap = oauth.getUsersByEmail(new ArrayList<>(userEmails)); + Map userMap = getWdkModel().getUserFactory().getUsersByEmail(new ArrayList<>(userEmails)); Map idMap = Functions.getMapFromKeys(userMap.keySet(), email -> Optional.ofNullable(userMap.get(email)).map(User::getUserId).orElse(null)); return Response.ok(new JSONObject().put("results", idMap).toString()).build(); From 1ad644962d7f96328c5e1584b130cd6615ac78bc Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Thu, 22 Feb 2024 13:00:57 -0500 Subject: [PATCH 14/25] Convert session to temporary user data store --- Model/pom.xml | 5 + .../wdk/cache/TemporaryUserDataStore.java | 124 ++++++++++++++++++ .../gusdb/wdk/controller/WdkInitializer.java | 4 + .../service/service/AbstractWdkService.java | 7 +- .../wdk/service/service/SessionService.java | 12 +- .../service/service/TemporaryFileService.java | 6 +- .../service/service/user/DatasetService.java | 2 +- .../service/service/user/ProfileService.java | 2 +- 8 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java diff --git a/Model/pom.xml b/Model/pom.xml index 17ad15f118..14c53f6244 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -90,6 +90,11 @@ oauth2-client + + com.github.ben-manes.caffeine + caffeine + + com.itextpdf itextpdf diff --git a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java new file mode 100644 index 0000000000..72065aaa6a --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java @@ -0,0 +1,124 @@ +package org.gusdb.wdk.cache; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.log4j.Logger; +import org.gusdb.fgputil.collection.ReadOnlyHashMap; +import org.gusdb.fgputil.collection.ReadOnlyMap; +import org.gusdb.fgputil.web.SessionProxy; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalListener; +import com.github.benmanes.caffeine.cache.stats.CacheStats; + +/** + * Manages a map of user-scoped short-term information. Traditionally, + * this data was stored in the user's session object; instead, we store + * it now in a userId-keyed map, whose values time out some duration + * after last access (currently 60 minutes). If instance() is called + * within the application, shutDown() should also be called to clean + * up the expiration thread threadpool. + */ +public class TemporaryUserDataStore { + + private static final Logger LOG = Logger.getLogger(TemporaryUserDataStore.class); + + public static class TemporaryUserData extends HashMap implements SessionProxy { + + private final TemporaryUserDataStore _parent; + private final Long _owner; + private final String _id; + + private TemporaryUserData(TemporaryUserDataStore parent, Long owner) { + _parent = parent; + _owner = owner; + _id = UUID.randomUUID().toString(); + } + + @Override + public Object getAttribute(String key) { + return get(key); + } + + @Override + public ReadOnlyMap getAttributeMap() { + ReadOnlyHashMap.Builder builder = ReadOnlyHashMap.builder(); + builder.putAll(this); + return builder.build(); + } + + @Override + public void setAttribute(String key, Object value) { + put(key, value); + } + + @Override + public void removeAttribute(String key) { + remove(key); + } + + @Override + public Object getUnderlyingSession() { + return this; + } + + @Override + public void invalidate() { + clear(); + _parent.remove(_owner); + } + + @Override + public String getId() { + return _id; + } + } + + // singleton pattern + private static TemporaryUserDataStore _instance; + + public static synchronized TemporaryUserDataStore instance() { + return _instance == null ? (_instance = new TemporaryUserDataStore()) : _instance; + } + + public static void shutDown() { + if (_instance != null) + _instance._threadPool.shutdown(); + _instance = null; + } + + private static final RemovalListener> LISTENER = + (k,v,cause) -> LOG.info("User " + k + "'s temporary user data store has expired with " + v.size() + " entries; Reason: " + cause); + + private final ExecutorService _threadPool; + private final Cache _data; + + private TemporaryUserDataStore() { + _threadPool = Executors.newCachedThreadPool(); + _data = Caffeine.newBuilder() + .executor(_threadPool) + .recordStats() + .removalListener(LISTENER) + .expireAfterAccess(60, TimeUnit.MINUTES) + .build(); + } + + public TemporaryUserData get(Long userId) { + return _data.get(userId, id -> new TemporaryUserData(this, id)); + } + + public void remove(Long userId) { + _data.invalidate(userId); + } + + public CacheStats getStats() { + return _data.stats(); + } + +} diff --git a/Model/src/main/java/org/gusdb/wdk/controller/WdkInitializer.java b/Model/src/main/java/org/gusdb/wdk/controller/WdkInitializer.java index 245a95ea82..69b5550736 100644 --- a/Model/src/main/java/org/gusdb/wdk/controller/WdkInitializer.java +++ b/Model/src/main/java/org/gusdb/wdk/controller/WdkInitializer.java @@ -6,6 +6,7 @@ import org.gusdb.fgputil.logging.ThreadLocalLoggingVars; import org.gusdb.fgputil.runtime.GusHome; import org.gusdb.fgputil.web.ApplicationContext; +import org.gusdb.wdk.cache.TemporaryUserDataStore; import org.gusdb.wdk.model.ThreadMonitor; import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; @@ -77,6 +78,9 @@ public static void terminateWdk(ApplicationContext applicationScope) { // shut down thread monitor ThreadMonitor.shutDown(); + // shut down TemporaryUserData threadpool + TemporaryUserDataStore.shutDown(); + WdkModel wdkModel = getWdkModel(applicationScope); if (wdkModel != null) { // insulate in case model never properly loaded diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java index 22674a7510..69efe7c593 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java @@ -25,8 +25,9 @@ import org.gusdb.fgputil.IoUtil; import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.RequestData; -import org.gusdb.fgputil.web.SessionProxy; import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.wdk.cache.TemporaryUserDataStore; +import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData; import org.gusdb.wdk.controller.ContextLookup; import org.gusdb.wdk.errors.ErrorContext; import org.gusdb.wdk.errors.ErrorContext.ErrorLocation; @@ -143,8 +144,8 @@ protected Map> getHeaders() { * * @return genericized session object (compatible with both Servlet and Grizzly sessions) */ - protected SessionProxy getSession() { - return getRequest().getSession(); + protected TemporaryUserData getTemporaryUserData() { + return TemporaryUserDataStore.instance().get(getRequestingUser().getUserId()); } protected User getRequestingUser() { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 821dcb2c68..987ec465b6 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -85,7 +85,7 @@ public class SessionService extends AbstractWdkService { @Produces(MediaType.APPLICATION_JSON) public Response getOauthStateToken() throws WdkModelException { String newToken = generateStateToken(getWdkModel()); - getSession().setAttribute(STATE_TOKEN_KEY, newToken); + getTemporaryUserData().setAttribute(STATE_TOKEN_KEY, newToken); JSONObject json = new JSONObject(); json.put(JsonKeys.OAUTH_STATE_TOKEN, newToken); return Response.ok(json.toString()).build(); @@ -145,8 +145,8 @@ public Response processOauthLogin( } // get state token off session and remove; only needed for this request - String storedStateToken = (String) getSession().getAttribute(STATE_TOKEN_KEY); - getSession().removeAttribute(STATE_TOKEN_KEY); + String storedStateToken = (String) getTemporaryUserData().getAttribute(STATE_TOKEN_KEY); + getTemporaryUserData().removeAttribute(STATE_TOKEN_KEY); // Is the state token present and does it match the session state token? if (stateToken == null || !stateToken.equals(storedStateToken)) { @@ -239,7 +239,7 @@ protected void transferOwnership(User oldUser, User newUser, WdkModel wdkModel) */ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, User oldUser, String redirectUrl, boolean isRedirectResponse) throws WdkModelException { - SessionProxy session = getSession(); + SessionProxy session = getTemporaryUserData(); // synchronize on the underlying session object (SessionProxy is request-local) synchronized(session.getUnderlyingSession()) { @@ -284,11 +284,11 @@ public Response processLogout() throws WdkModelException { // get the current session's user, then invalidate the session User oldUser = getRequestingUser(); - getSession().invalidate(); // legacy + getTemporaryUserData().invalidate(); // legacy // get a new session and add new guest user to it TwoTuple newUser = getWdkModel().getUserFactory().createUnregisteredUser(); - getSession().setAttribute(Utilities.WDK_USER_KEY, newUser.getSecond()); + getTemporaryUserData().setAttribute(Utilities.WDK_USER_KEY, newUser.getSecond()); // throw new user event Events.triggerAndWait(new NewUserEvent(newUser.getSecond(), oldUser), diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java index eb100dd847..fa8b9aede2 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java @@ -109,7 +109,7 @@ public static java.nio.file.Path writeTemporaryFile(WdkModel wdkModel, InputStre } private void addTempFileToSession(TemporaryFileMetadata tempFileMetadata) { - SessionProxy session = getSession(); + SessionProxy session = getTemporaryUserData(); @SuppressWarnings("unchecked") Map tempFilesInSession = (Map)(session.getAttribute(TEMP_FILE_METADATA)); if (tempFilesInSession == null) { @@ -133,7 +133,7 @@ public Response deleteTempFile(@PathParam("id") String tempFileName) throws WdkM java.nio.file.Path path = optPath.orElseThrow(() -> new NotFoundException( "Temporary file with ID " + tempFileName + " is not found in this user's session")); - SessionProxy session = getSession(); + SessionProxy session = getTemporaryUserData(); @SuppressWarnings("unchecked") Map tempFilesInSession = (Map)(session.getAttribute(TEMP_FILE_METADATA)); if (tempFilesInSession != null) { @@ -187,7 +187,7 @@ public static Function> getTempFileFactory( * session, or does not exist, return empty optional. */ private Optional getTempFileFromSession(String tempFileName) { - return getTempFileFactory(getWdkModel(), getSession()).apply(tempFileName); + return getTempFileFactory(getWdkModel(), getTemporaryUserData()).apply(tempFileName); } /** diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/DatasetService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/DatasetService.java index ca4f7b1346..f6007d0d94 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/DatasetService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/DatasetService.java @@ -74,7 +74,7 @@ public Response addDatasetFromJson(JSONObject input) var user = getUserBundle(Access.PRIVATE).getSessionUser(); var factory = getWdkModel().getDatasetFactory(); var request = new DatasetRequest(input); - var dataset = DatasetRequestProcessor.createFromRequest(request, user, factory, getSession()); + var dataset = DatasetRequestProcessor.createFromRequest(request, user, factory, getTemporaryUserData()); if (request.getDisplayName().isPresent()) { dataset.setName(request.getDisplayName().get()); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index 595b07c924..c90f34ca8e 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -87,7 +87,7 @@ public Response setUserProfile(String body) newUser = getWdkModel().getUserFactory().saveUser(oldUser, newUser, getAuthorizationToken()); // save user to session - getSession().setAttribute(Utilities.WDK_USER_KEY, newUser); + getTemporaryUserData().setAttribute(Utilities.WDK_USER_KEY, newUser); return Response.noContent().build(); } From cc2fe2cf10be052cfda850d3632d081c157c853c Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Thu, 22 Feb 2024 21:09:43 -0500 Subject: [PATCH 15/25] Stop using SessionProxy interface --- .../wdk/cache/TemporaryUserDataStore.java | 40 +------------------ .../org/gusdb/wdk/errors/ErrorContext.java | 4 -- .../service/filter/LoggingContextFilter.java | 1 - .../user/dataset/DatasetRequestProcessor.java | 10 ++--- .../service/service/AbstractWdkService.java | 1 - .../wdk/service/service/SessionService.java | 19 ++++----- .../service/service/TemporaryFileService.java | 26 ++++++------ .../service/service/user/ProfileService.java | 4 -- 8 files changed, 27 insertions(+), 78 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java index 72065aaa6a..385e5df50b 100644 --- a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java +++ b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java @@ -2,15 +2,11 @@ import java.util.HashMap; import java.util.Map; -import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.apache.log4j.Logger; -import org.gusdb.fgputil.collection.ReadOnlyHashMap; -import org.gusdb.fgputil.collection.ReadOnlyMap; -import org.gusdb.fgputil.web.SessionProxy; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; @@ -29,55 +25,21 @@ public class TemporaryUserDataStore { private static final Logger LOG = Logger.getLogger(TemporaryUserDataStore.class); - public static class TemporaryUserData extends HashMap implements SessionProxy { + public static class TemporaryUserData extends HashMap { private final TemporaryUserDataStore _parent; private final Long _owner; - private final String _id; private TemporaryUserData(TemporaryUserDataStore parent, Long owner) { _parent = parent; _owner = owner; - _id = UUID.randomUUID().toString(); } - @Override - public Object getAttribute(String key) { - return get(key); - } - - @Override - public ReadOnlyMap getAttributeMap() { - ReadOnlyHashMap.Builder builder = ReadOnlyHashMap.builder(); - builder.putAll(this); - return builder.build(); - } - - @Override - public void setAttribute(String key, Object value) { - put(key, value); - } - - @Override - public void removeAttribute(String key) { - remove(key); - } - - @Override - public Object getUnderlyingSession() { - return this; - } - - @Override public void invalidate() { clear(); _parent.remove(_owner); } - @Override - public String getId() { - return _id; - } } // singleton pattern diff --git a/Model/src/main/java/org/gusdb/wdk/errors/ErrorContext.java b/Model/src/main/java/org/gusdb/wdk/errors/ErrorContext.java index fbed76f373..58bc2ee09e 100644 --- a/Model/src/main/java/org/gusdb/wdk/errors/ErrorContext.java +++ b/Model/src/main/java/org/gusdb/wdk/errors/ErrorContext.java @@ -24,19 +24,16 @@ public static enum ErrorLocation { private final WdkModel _wdkModel; private final RequestSnapshot _requestData; private final ReadOnlyMap _requestAttributeMap; - private final ReadOnlyMap _sessionAttributeMap; private final ErrorLocation _errorLocation; private final ThreadContextBundle _mdcBundle; private final String _logMarker; private final Date _date; public ErrorContext(WdkModel wdkModel, RequestSnapshot requestData, - ReadOnlyMap sessionAttributeMap, ErrorLocation errorLocation) { _wdkModel = wdkModel; _requestData = requestData; _requestAttributeMap = requestData.getAttributes(); - _sessionAttributeMap = sessionAttributeMap; _errorLocation = errorLocation; _mdcBundle = ThreadLocalLoggingVars.getThreadContextBundle(); _logMarker = UUID.randomUUID().toString(); @@ -46,7 +43,6 @@ public ErrorContext(WdkModel wdkModel, RequestSnapshot requestData, public WdkModel getWdkModel() { return _wdkModel; } public RequestSnapshot getRequestData() { return _requestData; } public ReadOnlyMap getRequestAttributeMap() { return _requestAttributeMap; } - public ReadOnlyMap getSessionAttributeMap() { return _sessionAttributeMap; } /** * A site is considered monitored if the administrator email from adminEmail in the model-config.xml has content. diff --git a/Service/src/main/java/org/gusdb/wdk/service/filter/LoggingContextFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/LoggingContextFilter.java index 866b8ff6ca..71fe96a65e 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/filter/LoggingContextFilter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/filter/LoggingContextFilter.java @@ -40,7 +40,6 @@ public void filter(ContainerRequestContext requestContext) throws IOException { ThreadLocalLoggingVars.setIpAddress(request.getRemoteIpAddress()); ThreadLocalLoggingVars.setRequestedDomain(request.getServerName()); ThreadLocalLoggingVars.setRequestId(String.valueOf(requestId.getAndIncrement())); - ThreadLocalLoggingVars.setSessionId(request.getSession().getId()); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/dataset/DatasetRequestProcessor.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/dataset/DatasetRequestProcessor.java index 4f6e04c803..dd41417108 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/dataset/DatasetRequestProcessor.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/dataset/DatasetRequestProcessor.java @@ -24,13 +24,13 @@ import org.gusdb.fgputil.json.JsonType.ValueType; import org.gusdb.fgputil.validation.ValidObjectFactory.RunnableObj; import org.gusdb.fgputil.validation.ValidationLevel; -import org.gusdb.fgputil.web.SessionProxy; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkUserException; import org.gusdb.wdk.model.answer.AnswerValue; import org.gusdb.wdk.model.answer.factory.AnswerValueFactory; +import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData; import org.gusdb.wdk.model.dataset.Dataset; import org.gusdb.wdk.model.dataset.DatasetContents; import org.gusdb.wdk.model.dataset.DatasetFactory; @@ -63,14 +63,14 @@ public static Dataset createFromRequest( DatasetRequest request, User user, DatasetFactory factory, - SessionProxy session + TemporaryUserData tmpUserData ) throws WdkModelException, DataValidationException, RequestMisformatException { JsonType value = request.getConfigValue(); switch(request.getSourceType()) { case ID_LIST: return createFromIdList(value.getJSONArray(), user, factory); case BASKET: return createFromBasket(value.getString(), user, factory); case STRATEGY: return createFromStrategy(getStrategyId(value), user, factory); - case FILE: return createFromTemporaryFile(value.getString(), user, factory, request.getAdditionalConfig(), session); + case FILE: return createFromTemporaryFile(value.getString(), user, factory, request.getAdditionalConfig(), tmpUserData); case URL: return createFromUrl(value.getString(), user, factory, request.getAdditionalConfig()); default: throw new DataValidationException("Unrecognized " + JsonKeys.SOURCE_TYPE + ": " + request.getSourceType()); @@ -235,13 +235,13 @@ private static Dataset createFromTemporaryFile( final User user, final DatasetFactory factory, final Map additionalConfig, - final SessionProxy session + final TemporaryUserData tmpUserData ) throws DataValidationException, WdkModelException, RequestMisformatException { var model = factory.getWdkModel(); var parser = getDatasetParser(model, additionalConfig); - var tempFilePath = TemporaryFileService.getTempFileFactory(factory.getWdkModel(), session) + var tempFilePath = TemporaryFileService.getTempFileFactory(factory.getWdkModel(), tmpUserData) .apply(tempFileId) .orElseThrow(() -> new DataValidationException( "Temporary file with the ID '" + tempFileId + "' could not be found in this session.")); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java index 69efe7c593..e27d4760b0 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/AbstractWdkService.java @@ -217,7 +217,6 @@ public static ErrorContext getErrorContext(RequestData request, WdkModel wdkMode return new ErrorContext( wdkModel, request.getSnapshot(), - request.getSession().getAttributeMap(), errorLocation); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index 987ec465b6..df09f97513 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -26,12 +26,11 @@ import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; -import org.gusdb.fgputil.web.SessionProxy; import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.oauth2.exception.InvalidTokenException; +import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.events.NewUserEvent; -import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkRuntimeException; @@ -85,7 +84,7 @@ public class SessionService extends AbstractWdkService { @Produces(MediaType.APPLICATION_JSON) public Response getOauthStateToken() throws WdkModelException { String newToken = generateStateToken(getWdkModel()); - getTemporaryUserData().setAttribute(STATE_TOKEN_KEY, newToken); + getTemporaryUserData().put(STATE_TOKEN_KEY, newToken); JSONObject json = new JSONObject(); json.put(JsonKeys.OAUTH_STATE_TOKEN, newToken); return Response.ok(json.toString()).build(); @@ -145,8 +144,8 @@ public Response processOauthLogin( } // get state token off session and remove; only needed for this request - String storedStateToken = (String) getTemporaryUserData().getAttribute(STATE_TOKEN_KEY); - getTemporaryUserData().removeAttribute(STATE_TOKEN_KEY); + String storedStateToken = (String) getTemporaryUserData().get(STATE_TOKEN_KEY); + getTemporaryUserData().remove(STATE_TOKEN_KEY); // Is the state token present and does it match the session state token? if (stateToken == null || !stateToken.equals(storedStateToken)) { @@ -239,12 +238,10 @@ protected void transferOwnership(User oldUser, User newUser, WdkModel wdkModel) */ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, User oldUser, String redirectUrl, boolean isRedirectResponse) throws WdkModelException { - SessionProxy session = getTemporaryUserData(); - // synchronize on the underlying session object (SessionProxy is request-local) - synchronized(session.getUnderlyingSession()) { + TemporaryUserData tmpData = getTemporaryUserData(); - session.setAttribute(Utilities.WDK_USER_KEY, newUser); + synchronized(tmpData) { Events.triggerAndWait(new NewUserEvent(newUser, oldUser), new WdkRuntimeException("Unable to complete WDK user assignement.")); @@ -285,10 +282,9 @@ public Response processLogout() throws WdkModelException { // get the current session's user, then invalidate the session User oldUser = getRequestingUser(); getTemporaryUserData().invalidate(); // legacy - + // get a new session and add new guest user to it TwoTuple newUser = getWdkModel().getUserFactory().createUnregisteredUser(); - getTemporaryUserData().setAttribute(Utilities.WDK_USER_KEY, newUser.getSecond()); // throw new user event Events.triggerAndWait(new NewUserEvent(newUser.getSecond(), oldUser), @@ -302,6 +298,7 @@ public Response processLogout() throws WdkModelException { extraCookie.setMaxAge(-1); logoutCookies.add(extraCookie); } + ResponseBuilder builder = createRedirectResponse(getContextUri()); for (CookieBuilder logoutCookie : logoutCookies) { builder.cookie(logoutCookie.toJaxRsCookie()); diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java index fa8b9aede2..3fe7836a6a 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java @@ -22,7 +22,7 @@ import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; import org.gusdb.fgputil.IoUtil; -import org.gusdb.fgputil.web.SessionProxy; +import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkRuntimeException; @@ -109,14 +109,14 @@ public static java.nio.file.Path writeTemporaryFile(WdkModel wdkModel, InputStre } private void addTempFileToSession(TemporaryFileMetadata tempFileMetadata) { - SessionProxy session = getTemporaryUserData(); + TemporaryUserData tmpData = getTemporaryUserData(); @SuppressWarnings("unchecked") - Map tempFilesInSession = (Map)(session.getAttribute(TEMP_FILE_METADATA)); + Map tempFilesInSession = (Map)(tmpData.get(TEMP_FILE_METADATA)); if (tempFilesInSession == null) { tempFilesInSession = Collections.synchronizedMap(new HashMap<>()); } tempFilesInSession.put(tempFileMetadata.getTempName(), tempFileMetadata); - session.setAttribute(TEMP_FILE_METADATA, tempFilesInSession); + tmpData.put(TEMP_FILE_METADATA, tempFilesInSession); } /** @@ -133,12 +133,12 @@ public Response deleteTempFile(@PathParam("id") String tempFileName) throws WdkM java.nio.file.Path path = optPath.orElseThrow(() -> new NotFoundException( "Temporary file with ID " + tempFileName + " is not found in this user's session")); - SessionProxy session = getTemporaryUserData(); + TemporaryUserData tmpData = getTemporaryUserData(); @SuppressWarnings("unchecked") - Map tempFilesInSession = (Map)(session.getAttribute(TEMP_FILE_METADATA)); + Map tempFilesInSession = (Map)(tmpData.get(TEMP_FILE_METADATA)); if (tempFilesInSession != null) { tempFilesInSession.remove(tempFileName); - session.setAttribute(TEMP_FILE_METADATA, tempFilesInSession); + tmpData.put(TEMP_FILE_METADATA, tempFilesInSession); } try { @@ -157,12 +157,12 @@ public Response deleteTempFile(@PathParam("id") String tempFileName) throws WdkM * * @param wdkModel * the WDK model - * @param session - * the current user session + * @param userTmpData + * user's temporary data * * @return a factory function for looking up temporary file paths by name */ - public static Function> getTempFileFactory(WdkModel wdkModel, SessionProxy session) { + public static Function> getTempFileFactory(WdkModel wdkModel, TemporaryUserData userTmpData) { java.nio.file.Path tempDirPath = wdkModel.getModelConfig().getWdkTempDir(); return tempFileName -> { @@ -172,7 +172,7 @@ public static Function> getTempFileFactory( throw new WdkRuntimeException("WDK Temp file " + tempFilePath + " exists but is not readable"); } @SuppressWarnings("unchecked") - Map tempFilesInSession = (Map)(session.getAttribute(TEMP_FILE_METADATA)); + Map tempFilesInSession = (Map)(userTmpData.get(TEMP_FILE_METADATA)); if (tempFilesInSession != null && tempFilesInSession.containsKey(tempFileName)) { return Optional.of(tempFilePath); } @@ -195,9 +195,9 @@ private Optional getTempFileFromSession(String tempFileName) * (having been put there by the temp file service). If the metadata is not * found in the session, return empty optional. */ - public static Optional getTempFileMetadata(String tempFileName, SessionProxy session) { + public static Optional getTempFileMetadata(String tempFileName, TemporaryUserData userTmpData) { @SuppressWarnings("unchecked") - Map tempFilesInSession = (Map)(session.getAttribute(TEMP_FILE_METADATA)); + Map tempFilesInSession = (Map)(userTmpData.get(TEMP_FILE_METADATA)); return Optional.ofNullable( tempFilesInSession.get(tempFileName) diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index c90f34ca8e..f2bdd3c467 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java @@ -13,7 +13,6 @@ import javax.ws.rs.core.Response; import org.gusdb.oauth2.exception.InvalidPropertiesException; -import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.user.BasicUser; import org.gusdb.wdk.model.user.InvalidUsernameOrEmailException; @@ -86,9 +85,6 @@ public Response setUserProfile(String body) // save user to OAuth newUser = getWdkModel().getUserFactory().saveUser(oldUser, newUser, getAuthorizationToken()); - // save user to session - getTemporaryUserData().setAttribute(Utilities.WDK_USER_KEY, newUser); - return Response.noContent().build(); } catch(JSONException | RequestMisformatException e) { From 6c7777ac778d8f602f9973b140dfa4867b72cdf0 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Fri, 23 Feb 2024 21:15:36 -0500 Subject: [PATCH 16/25] Switch to ConcurrentHashMap in case user has multiple requests going at the same time --- .../main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java index 385e5df50b..04a9ec3947 100644 --- a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java +++ b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java @@ -1,7 +1,7 @@ package org.gusdb.wdk.cache; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -25,7 +25,7 @@ public class TemporaryUserDataStore { private static final Logger LOG = Logger.getLogger(TemporaryUserDataStore.class); - public static class TemporaryUserData extends HashMap { + public static class TemporaryUserData extends ConcurrentHashMap { private final TemporaryUserDataStore _parent; private final Long _owner; From 5b5cd9072a61ccbedb7138b38093ef2d68e21e62 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Tue, 27 Feb 2024 12:50:34 -0500 Subject: [PATCH 17/25] ditch accountdb --- Service/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Service/pom.xml b/Service/pom.xml index 5e896d9cb0..85c1b2a16f 100644 --- a/Service/pom.xml +++ b/Service/pom.xml @@ -36,11 +36,6 @@ - - org.gusdb - fgputil-accountdb - - org.gusdb fgputil-server From 3105dce19c2c7462c58df1bda5432581c80e3ed9 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Tue, 27 Feb 2024 14:50:46 -0500 Subject: [PATCH 18/25] Remove accountDb from RNG --- Model/lib/rng/wdkModel-config.rng | 33 ------------------------------- 1 file changed, 33 deletions(-) diff --git a/Model/lib/rng/wdkModel-config.rng b/Model/lib/rng/wdkModel-config.rng index e3fde44be3..e9d5e48b59 100644 --- a/Model/lib/rng/wdkModel-config.rng +++ b/Model/lib/rng/wdkModel-config.rng @@ -94,39 +94,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 16bca8d801f057ab490a475b3bba17476f965d92 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Tue, 27 Feb 2024 15:09:31 -0500 Subject: [PATCH 19/25] Produce user factories on demand vs keeping one around --- Model/src/main/java/org/gusdb/wdk/model/WdkModel.java | 6 ++---- .../wdk/model/record/attribute/DerivedAttributeField.java | 1 - 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java index a87dbcfb2f..a4e5737d8a 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java +++ b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java @@ -176,7 +176,6 @@ public static WdkModel construct(String projectId, String gusHome) throws WdkMod private File xmlDataDir; - private UserFactory userFactory; private StepFactory stepFactory; private DatasetFactory datasetFactory; private BasketFactory basketFactory; @@ -563,7 +562,6 @@ public void configure(ModelConfig modelConfig) throws WdkModelException { _userDatasetStore = Optional.of(udsConfig.getUserDatasetStore(modelConfig.getWdkTempDir())); } - userFactory = new UserFactory(this); stepFactory = new StepFactory(this); datasetFactory = new DatasetFactory(this); basketFactory = new BasketFactory(this); @@ -594,7 +592,7 @@ public void configure(ModelConfig modelConfig) throws WdkModelException { new UnconfiguredStepAnalysisFactory(this) : new StepAnalysisFactoryImpl(this)); - systemUser = userFactory.createUnregisteredUser().getSecond(); + systemUser = new UserFactory(this).createUnregisteredUser().getSecond(); LOG.info("WDK Model configured."); } @@ -703,7 +701,7 @@ public DatabaseInstance getAccountDb() { } public UserFactory getUserFactory() { - return userFactory; + return new UserFactory(this); } public StepFactory getStepFactory() { diff --git a/Model/src/main/java/org/gusdb/wdk/model/record/attribute/DerivedAttributeField.java b/Model/src/main/java/org/gusdb/wdk/model/record/attribute/DerivedAttributeField.java index a460f51864..da41e3e641 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/record/attribute/DerivedAttributeField.java +++ b/Model/src/main/java/org/gusdb/wdk/model/record/attribute/DerivedAttributeField.java @@ -19,7 +19,6 @@ import org.gusdb.wdk.model.Utilities; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.WdkModelText; import org.gusdb.wdk.model.WdkUserException; /** From 6882f156b4253a2c19edcb5082e231059a2e412a Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Tue, 27 Feb 2024 22:30:57 -0500 Subject: [PATCH 20/25] Remove acctdb references --- Model/src/main/java/org/gusdb/wdk/model/WdkModel.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java index a4e5737d8a..5bed180490 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java +++ b/Model/src/main/java/org/gusdb/wdk/model/WdkModel.java @@ -111,10 +111,8 @@ public static WdkModel construct(String projectId, String gusHome) throws WdkMod private String _projectId; private long _startupTime; - private DatabaseInstance appDb; private DatabaseInstance userDb; - private DatabaseInstance accountDb; private Optional _userDatasetStore; @@ -656,7 +654,6 @@ public void close() { stepAnalysisFactory.shutDown(); releaseDb(appDb); releaseDb(userDb); - releaseDb(accountDb); Events.shutDown(); managedCloseables.close(); LOG.info("WDK Model resources released."); @@ -696,10 +693,6 @@ public DatabaseInstance getUserDb() { return userDb; } - public DatabaseInstance getAccountDb() { - return accountDb; - } - public UserFactory getUserFactory() { return new UserFactory(this); } From af09d25dc2e46a3ea77b5dbc84d2e0096e968aa1 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Tue, 27 Feb 2024 22:35:24 -0500 Subject: [PATCH 21/25] why is there an NPE? --- .../java/org/gusdb/wdk/model/user/UserPasswordEmailer.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java index bf7c0f38bd..3390ebb828 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java @@ -1,5 +1,6 @@ package org.gusdb.wdk.model.user; +import java.util.Objects; import java.util.regex.Matcher; import org.gusdb.wdk.model.Utilities; @@ -24,6 +25,8 @@ public UserPasswordEmailer(WdkModel wdkModel) { _wdkModelConfig = wdkModel.getModelConfig(); // whether or not WDK is configured to send a welcome email to new registered users (defaults to true) + Objects.requireNonNull(wdkModel); + Objects.requireNonNull(wdkModel.getProperties()); String dontEmailProp = wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); _sendWelcomeEmail = dontEmailProp == null || !dontEmailProp.equals("true"); } From 5afa814dde442a7b075bff652efea90eef93be9b Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Tue, 27 Feb 2024 22:58:34 -0500 Subject: [PATCH 22/25] Fix NPE --- .../wdk/model/user/UserPasswordEmailer.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java index 3390ebb828..7a083f7060 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java @@ -1,6 +1,5 @@ package org.gusdb.wdk.model.user; -import java.util.Objects; import java.util.regex.Matcher; import org.gusdb.wdk.model.Utilities; @@ -18,32 +17,28 @@ public class UserPasswordEmailer { private static final String EMAIL_MACRO_EMAIL = "EMAIL"; private static final String EMAIL_MACRO_PASSWORD = "PASSWORD"; - private final boolean _sendWelcomeEmail; - private final ModelConfig _wdkModelConfig; + private final WdkModel _wdkModel; public UserPasswordEmailer(WdkModel wdkModel) { - _wdkModelConfig = wdkModel.getModelConfig(); - - // whether or not WDK is configured to send a welcome email to new registered users (defaults to true) - Objects.requireNonNull(wdkModel); - Objects.requireNonNull(wdkModel.getProperties()); - String dontEmailProp = wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); - _sendWelcomeEmail = dontEmailProp == null || !dontEmailProp.equals("true"); + _wdkModel = wdkModel; } public boolean isSendWelcomeEmail() { - return _sendWelcomeEmail; + // whether or not WDK is configured to send a welcome email to new registered users (defaults to true) + String dontEmailProp = _wdkModel.getProperties().get("DONT_EMAIL_NEW_USER"); + return dontEmailProp == null || !dontEmailProp.equals("true"); } public void emailTemporaryPassword(User user, String password) throws WdkModelException { - if (!_sendWelcomeEmail) return; + if (!isSendWelcomeEmail()) return; - String smtpServer = _wdkModelConfig.getSmtpServer(); - String supportEmail = _wdkModelConfig.getSupportEmail(); - String emailSubject = _wdkModelConfig.getEmailSubject(); + ModelConfig wdkModelConfig = _wdkModel.getModelConfig(); + String smtpServer = wdkModelConfig.getSmtpServer(); + String supportEmail = wdkModelConfig.getSupportEmail(); + String emailSubject = wdkModelConfig.getEmailSubject(); // populate email content macros with user data - String emailContent = _wdkModelConfig.getEmailContent() + String emailContent = wdkModelConfig.getEmailContent() .replaceAll("\\$\\$" + EMAIL_MACRO_USER_NAME + "\\$\\$", Matcher.quoteReplacement(user.getDisplayName())) .replaceAll("\\$\\$" + EMAIL_MACRO_EMAIL + "\\$\\$", From facb734890fdccd1f98e05011744a11e885465ae Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Fri, 1 Mar 2024 06:53:08 -0500 Subject: [PATCH 23/25] Comply with new oauth API and try to return better errors --- .../main/java/org/gusdb/wdk/model/user/UserFactory.java | 7 ++++--- .../org/gusdb/wdk/service/service/SessionService.java | 8 ++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java index bfed400183..f885ebc554 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserFactory.java @@ -17,6 +17,7 @@ import org.gusdb.oauth2.client.veupathdb.OAuthQuerier; import org.gusdb.oauth2.client.veupathdb.UserProperty; import org.gusdb.oauth2.exception.ConflictException; +import org.gusdb.oauth2.exception.ExpiredTokenException; import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.oauth2.exception.InvalidTokenException; import org.gusdb.oauth2.shared.IdTokenFields; @@ -71,15 +72,15 @@ public UserFactory(WdkModel wdkModel) { // methods to manage tokens // ------------------------------------------------------------------------- - public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) throws InvalidTokenException { + public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) throws InvalidPropertiesException { return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); } - public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) throws InvalidTokenException { + public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) throws InvalidPropertiesException { return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); } - public ValidatedToken validateBearerToken(String rawToken) throws InvalidTokenException { + public ValidatedToken validateBearerToken(String rawToken) throws InvalidTokenException, ExpiredTokenException { return _client.getValidatedEcdsaSignedToken(_config.getOauthUrl(), rawToken); } diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index df09f97513..cbf8207e4f 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -27,7 +27,7 @@ import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.oauth2.client.ValidatedToken; -import org.gusdb.oauth2.exception.InvalidTokenException; +import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData; import org.gusdb.wdk.core.api.JsonKeys; import org.gusdb.wdk.events.NewUserEvent; @@ -163,6 +163,10 @@ public Response processOauthLogin( // login successful; create redirect response return getSuccessResponse(bearerToken, newUser, oldUser, redirectUrl, true); } + catch (InvalidPropertiesException ex) { + LOG.error("Could not authenticate user's identity. Exception thrown: ", ex); + return createJsonResponse(false, "Invalid auth token or redirect URI", null).build(); + } catch (Exception ex) { LOG.error("Unsuccessful login attempt: " + ex.getMessage(), ex); String oauthFailureUrl = appUrl + OAUTH_ERROR_URL + FormatUtil.urlEncodeUtf8(originalUrl); @@ -211,7 +215,7 @@ public Response processDbLogin(@HeaderParam(REFERRER_HEADER_KEY) String referrer catch (JSONException e) { throw new RequestMisformatException(e.getMessage()); } - catch (InvalidTokenException ex) { + catch (InvalidPropertiesException ex) { LOG.error("Could not authenticate user's identity. Exception thrown: ", ex); return createJsonResponse(false, "Invalid username or password", null).build(); } From 0baf925772b42b80640d602ae5dbc492f3a6f62d Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Fri, 1 Mar 2024 09:40:11 -0500 Subject: [PATCH 24/25] Handle expired tokens better + some cleanup --- .../org/gusdb/wdk/events/NewUserEvent.java | 23 ------- .../wdk/model/user/UserReferenceFactory.java | 5 +- .../wdk/service/filter/CheckLoginFilter.java | 69 ++++++++++++------- .../wdk/service/service/SessionService.java | 16 ++--- 4 files changed, 52 insertions(+), 61 deletions(-) delete mode 100644 Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java diff --git a/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java b/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java deleted file mode 100644 index 943f99cfab..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.gusdb.wdk.events; - -import org.gusdb.fgputil.events.Event; -import org.gusdb.wdk.model.user.User; - -public class NewUserEvent extends Event { - - private final User _newUser; - private final User _oldUser; - - public NewUserEvent(User newUser, User oldUser) { - _newUser = newUser; - _oldUser = oldUser; - } - - public User getNewUser() { - return _newUser; - } - - public User getOldUser() { - return _oldUser; - } -} diff --git a/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java index 00ea2cf98b..2673c9cc65 100644 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java @@ -48,7 +48,8 @@ class UserReferenceFactory { private static final Integer[] SELECT_USER_REF_BY_ID_PARAM_TYPES = { Types.BIGINT }; - private static class UserReference extends ThreeTuple { + // TODO: decide if this is actually needed/desired anywhere. UserRef lookups are not currently used. + public static class UserReference extends ThreeTuple { public UserReference(Long userId, Boolean isGuest, Date firstAccess) { super(userId, isGuest, firstAccess); } @@ -91,7 +92,7 @@ public int addUserReference(User user) throws WdkModelException { } // FIXME: see if this is actually needed anywhere? E.g. do we ever need to look up user refs by user ID to find last login? - private Optional getUserReference(long userId) throws WdkModelException { + public Optional getUserReference(long userId) throws WdkModelException { try { String sql = SELECT_USER_REF_BY_ID_SQL.replace(USER_SCHEMA_MACRO, _userSchema); return new SQLRunner(_userDb.getDataSource(), sql, "get-user-ref").executeQuery( diff --git a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java index c6b0b684d4..10ba972bcb 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java +++ b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java @@ -7,6 +7,7 @@ import javax.inject.Provider; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.container.ContainerResponseContext; @@ -16,6 +17,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; import org.apache.log4j.Logger; import org.glassfish.grizzly.http.server.Request; @@ -25,9 +27,11 @@ import org.gusdb.fgputil.web.RequestData; import org.gusdb.oauth2.client.OAuthClient; import org.gusdb.oauth2.client.ValidatedToken; +import org.gusdb.oauth2.exception.ExpiredTokenException; +import org.gusdb.oauth2.exception.InvalidTokenException; import org.gusdb.wdk.controller.ContextLookup; import org.gusdb.wdk.model.Utilities; -import org.gusdb.wdk.model.WdkModel; +import org.gusdb.wdk.model.WdkModelException; import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.user.User; import org.gusdb.wdk.model.user.UserFactory; @@ -60,45 +64,60 @@ public void filter(ContainerRequestContext requestContext) throws IOException { ApplicationContext context = ContextLookup.getApplicationContext(_servletContext); RequestData request = ContextLookup.getRequest(_servletRequest.get(), _grizzlyRequest.get()); - WdkModel wdkModel = ContextLookup.getWdkModel(context); + UserFactory factory = ContextLookup.getWdkModel(context).getUserFactory(); // try to find submitted bearer token String rawToken = findRawBearerToken(request, requestContext); try { - UserFactory factory = wdkModel.getUserFactory(); - ValidatedToken token; - User user; - if (rawToken != null) { - // validate submitted token - token = factory.validateBearerToken(rawToken); - user = factory.convertToUser(token); - - LOG.info("Validated successfully. Request will be processed for user " + user.getUserId() + " / " + user.getEmail()); + if (rawToken == null) { + // no credentials submitted; automatically create a guest to use on this request + useNewGuest(factory, request, requestContext, requestPath); } else { - // no credentials submitted; automatically create a guest to use on this request - TwoTuple guestPair = factory.createUnregisteredUser(); - token = guestPair.getFirst(); - user = guestPair.getSecond(); - - LOG.info("Created new guest user [" + user.getUserId() + "] for request to path: /" + requestPath); - - // set flag indicating that cookies should be added to response containing the new token - requestContext.setProperty(TOKEN_COOKIE_VALUE_TO_SET, token.getTokenValue()); + try { + // validate submitted token + ValidatedToken token = factory.validateBearerToken(rawToken); + User user = factory.convertToUser(token); + setRequestAttributes(request, token, user); + LOG.info("Validated successfully. Request will be processed for user " + user.getUserId() + " / " + user.getEmail()); + } + catch (ExpiredTokenException e) { + // token is expired; use guest token for now which should inspire them to log back in + useNewGuest(factory, request, requestContext, requestPath); + } + catch (InvalidTokenException e) { + // passed token is invalid; throw 401 + LOG.warn("Received invalid bearer token for auth: " + rawToken); + throw new NotAuthorizedException(Response.noContent().build()); + } } - - // set creds and user on the request object for use by this request's processing - request.setAttribute(Utilities.BEARER_TOKEN_KEY, token); - request.setAttribute(Utilities.WDK_USER_KEY, user); } catch (Exception e) { - // for now, log and let this go, deferring to legacy authentication + // any other exception is fatal, but log first LOG.error("Unable to authenticate with Authorization header " + rawToken, e); throw e instanceof RuntimeException ? (RuntimeException)e : new WdkRuntimeException(e); } } + private void useNewGuest(UserFactory factory, RequestData request, ContainerRequestContext requestContext, String requestPath) throws WdkModelException { + TwoTuple guestPair = factory.createUnregisteredUser(); + ValidatedToken token = guestPair.getFirst(); + User user = guestPair.getSecond(); + setRequestAttributes(request, token, user); + + LOG.info("Created new guest user [" + user.getUserId() + "] for request to path: /" + requestPath); + + // set flag indicating that cookies should be added to response containing the new token + requestContext.setProperty(TOKEN_COOKIE_VALUE_TO_SET, token.getTokenValue()); + } + + private void setRequestAttributes(RequestData request, ValidatedToken token, User user) { + // set creds and user on the request object for use by this request's processing + request.setAttribute(Utilities.BEARER_TOKEN_KEY, token); + request.setAttribute(Utilities.WDK_USER_KEY, user); + } + private String findRawBearerToken(RequestData request, ContainerRequestContext requestContext) { String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); if (authHeader != null) { diff --git a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java index cbf8207e4f..8f52c70a19 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java +++ b/Service/src/main/java/org/gusdb/wdk/service/service/SessionService.java @@ -23,17 +23,14 @@ import org.gusdb.fgputil.EncryptionUtil; import org.gusdb.fgputil.FormatUtil; import org.gusdb.fgputil.Tuples.TwoTuple; -import org.gusdb.fgputil.events.Events; import org.gusdb.fgputil.web.CookieBuilder; import org.gusdb.fgputil.web.LoginCookieFactory; import org.gusdb.oauth2.client.ValidatedToken; import org.gusdb.oauth2.exception.InvalidPropertiesException; import org.gusdb.wdk.cache.TemporaryUserDataStore.TemporaryUserData; import org.gusdb.wdk.core.api.JsonKeys; -import org.gusdb.wdk.events.NewUserEvent; import org.gusdb.wdk.model.WdkModel; import org.gusdb.wdk.model.WdkModelException; -import org.gusdb.wdk.model.WdkRuntimeException; import org.gusdb.wdk.model.config.ModelConfig; import org.gusdb.wdk.model.config.ModelConfig.AuthenticationMethod; import org.gusdb.wdk.model.user.User; @@ -247,9 +244,6 @@ private Response getSuccessResponse(ValidatedToken bearerToken, User newUser, Us synchronized(tmpData) { - Events.triggerAndWait(new NewUserEvent(newUser, oldUser), - new WdkRuntimeException("Unable to complete WDK user assignement.")); - // 3-year expiration (should change secret key before then) CookieBuilder bearerTokenCookie = new CookieBuilder( HttpHeaders.AUTHORIZATION, @@ -285,15 +279,15 @@ public Response processLogout() throws WdkModelException { // get the current session's user, then invalidate the session User oldUser = getRequestingUser(); - getTemporaryUserData().invalidate(); // legacy + getTemporaryUserData().invalidate(); + + // if user is already a guest, no need to log out + if (oldUser.isGuest()) + return createRedirectResponse(getContextUri()).build(); // get a new session and add new guest user to it TwoTuple newUser = getWdkModel().getUserFactory().createUnregisteredUser(); - // throw new user event - Events.triggerAndWait(new NewUserEvent(newUser.getSecond(), oldUser), - new WdkRuntimeException("Unable to complete WDK user assignement.")); - // create and append logout cookies to response Set logoutCookies = new HashSet<>(); logoutCookies.add(LoginCookieFactory.createLogoutCookie()); From efb5c299c74753f98926a87d16b538173bd5d112 Mon Sep 17 00:00:00 2001 From: Ryan Doherty Date: Sun, 17 Mar 2024 07:40:51 -0400 Subject: [PATCH 25/25] Just a little cleanup --- .../org/gusdb/wdk/cache/TemporaryUserDataStore.java | 3 +-- .../request/user/UserDatasetShareRequest.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java index 04a9ec3947..5ca37b68e3 100644 --- a/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java +++ b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java @@ -36,7 +36,6 @@ private TemporaryUserData(TemporaryUserDataStore parent, Long owner) { } public void invalidate() { - clear(); _parent.remove(_owner); } @@ -49,7 +48,7 @@ public static synchronized TemporaryUserDataStore instance() { return _instance == null ? (_instance = new TemporaryUserDataStore()) : _instance; } - public static void shutDown() { + public static synchronized void shutDown() { if (_instance != null) _instance._threadPool.shutdown(); _instance = null; diff --git a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java index 33cc824c48..4bd5208b76 100644 --- a/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java +++ b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserDatasetShareRequest.java @@ -109,9 +109,15 @@ public JSONObject getErrors() { /** * Input Format: - * - * { "add": { "dataset_id1": [ "userEmail1", "userEmail2" ] "dataset_id2": [ "userEmail1" ] }, "delete" { - * "dataset_id3": [ "userEmail1", "userEmail3" ] } } + * { + * "add": { + * "dataset_id1": [ "userEmail1", "userEmail2" ] + * "dataset_id2": [ "userEmail1" ] + * }, + * "delete" { + * "dataset_id3": [ "userEmail1", "userEmail3" ] + * } + * } * * @param json * @return