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..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 @@ -145,49 +145,6 @@ site_vars file: {{ site_vars }} {% endif -%} /> - - - - - - - - - - - {% if modelconfig_userDatasetStoreConfig is defined -%} {{ modelconfig_userDatasetStoreConfig|indent }} {% endif -%} 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Model/pom.xml b/Model/pom.xml index af5da321e9..14c53f6244 100644 --- a/Model/pom.xml +++ b/Model/pom.xml @@ -77,17 +77,22 @@ org.gusdb - fgputil-accountdb + fgputil-server org.gusdb - fgputil-server + fgputil-db org.gusdb - fgputil-db + oauth2-client + + + + com.github.ben-manes.caffeine + caffeine @@ -115,17 +120,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/cache/TemporaryUserDataStore.java b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java new file mode 100644 index 0000000000..5ca37b68e3 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/cache/TemporaryUserDataStore.java @@ -0,0 +1,85 @@ +package org.gusdb.wdk.cache; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.log4j.Logger; + +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 ConcurrentHashMap { + + private final TemporaryUserDataStore _parent; + private final Long _owner; + + private TemporaryUserData(TemporaryUserDataStore parent, Long owner) { + _parent = parent; + _owner = owner; + } + + public void invalidate() { + _parent.remove(_owner); + } + + } + + // singleton pattern + private static TemporaryUserDataStore _instance; + + public static synchronized TemporaryUserDataStore instance() { + return _instance == null ? (_instance = new TemporaryUserDataStore()) : _instance; + } + + public static synchronized 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/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/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/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 c572428ed4..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/events/NewUserEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -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) { - _newUser = newUser; - _oldUser = oldUser; - _session = session; - } - - public User getNewUser() { - return _newUser; - } - - 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/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 34c02fc6ad..cdfff9bf1f 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 444a423e57..5bed180490 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,8 +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.UnregisteredUser.UnregisteredUserType; -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; @@ -114,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; @@ -179,7 +174,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; @@ -204,7 +198,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; @@ -552,14 +545,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 ) { @@ -569,7 +560,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); @@ -600,6 +590,8 @@ public void configure(ModelConfig modelConfig) throws WdkModelException { new UnconfiguredStepAnalysisFactory(this) : new StepAnalysisFactoryImpl(this)); + systemUser = new UserFactory(this).createUnregisteredUser().getSecond(); + LOG.info("WDK Model configured."); } @@ -662,7 +654,6 @@ public void close() { stepAnalysisFactory.shutDown(); releaseDb(appDb); releaseDb(userDb); - releaseDb(accountDb); Events.shutDown(); managedCloseables.close(); LOG.info("WDK Model resources released."); @@ -702,12 +693,8 @@ public DatabaseInstance getUserDb() { return userDb; } - public DatabaseInstance getAccountDb() { - return accountDb; - } - public UserFactory getUserFactory() { - return userFactory; + return new UserFactory(this); } public StepFactory getStepFactory() { @@ -1361,17 +1348,6 @@ public String queryParamDisplayName(String paramName) { } public User getSystemUser() { - if (systemUser == null) { - try { - systemUserLock.lock(); - if (systemUser == null) { - systemUser = userFactory.createUnregistedUser(UnregisteredUserType.SYSTEM); - } - } - 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 7d70842d8b..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 @@ -11,6 +11,8 @@ 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; /** * An object representation of the {@code model-config.xml} file. It holds all the configuration information @@ -18,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); @@ -69,7 +71,6 @@ public String getName() { private final ModelConfigUserDB _userDB; private final ModelConfigAppDB _appDB; - private final ModelConfigAccountDB _accountDB; private final ModelConfigUserDatasetStore _userDatasetStoreConfig; @@ -81,6 +82,11 @@ public String getName() { private final String _projectId; private final Path _gusHome; + /** + * enable/disable weight feature in the steps. default enabled. + */ + private final boolean _useWeights; + /** * location of secret key file */ @@ -92,11 +98,6 @@ public String getName() { */ private String _secretKey; - /** - * enable/disable weight feature in the steps. default enabled. - */ - private final boolean _useWeights; - /** * default regex used by all the stringParams */ @@ -146,7 +147,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, @@ -163,8 +164,8 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac _paramRegex = paramRegex; // file locations - _secretKeyFile = secretKeyFile; _wdkTempDir = wdkTempDir; + _secretKeyFile = secretKeyFile; // network locations _webServiceUrl = webServiceUrl; @@ -180,7 +181,6 @@ public ModelConfig(String modelName, String projectId, Path gusHome, boolean cac // databases _userDB = userDB; _appDB = appDB; - _accountDB = accountDB; // user dataset config _userDatasetStoreConfig = userDatasetStoreConfig; @@ -293,13 +293,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 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 0307ff8e61..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/config/ModelConfigAccountDB.java +++ /dev/null @@ -1,29 +0,0 @@ -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); - } - - 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/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 874f876ca1..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 @@ -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; @@ -63,10 +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"); - configureNode(digester, "modelConfig/accountDb/userProperty", UserPropertyName.class, "addUserPropertyName"); - // 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/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/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/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/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; /** 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..7287c039c2 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BasicUser.java @@ -0,0 +1,41 @@ +package org.gusdb.wdk.model.user; + +import org.gusdb.oauth2.client.veupathdb.UserProperty; +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; + } + + 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/BearerTokenUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java new file mode 100644 index 0000000000..23e9348f35 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/BearerTokenUser.java @@ -0,0 +1,21 @@ +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; + +public class BearerTokenUser extends org.gusdb.oauth2.client.veupathdb.BearerTokenUser implements User { + + private final WdkModel _wdkModel; + + public BearerTokenUser(WdkModel wdkModel, OAuthClient client, OAuthConfig config, ValidatedToken token) { + super(client, config.getOauthUrl(), token); + _wdkModel = wdkModel; + } + + @Override + public WdkModel getWdkModel() { + return _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/RegisteredUser.java b/Model/src/main/java/org/gusdb/wdk/model/user/RegisteredUser.java deleted file mode 100644 index f0f3b606b2..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 { - - 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/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/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 a0f89c326f..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,251 +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. - * - * @see UnregisteredUser for subclass representing guest user - * @see RegisteredUser for subclass representing registered user - * - * @author rdoherty - */ -public abstract class User { - - private static final Logger LOG = Logger.getLogger(User.class); - - protected WdkModel _wdkModel; - - protected long _userId; - protected String _email; - protected String _signature; - protected String _stableId; - - // 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 - * - * @return display name for this user - */ - public abstract String getDisplayName(); - - /** - * Tells whether this user is a guest - * - * @return true if guest, else false - */ - public abstract boolean isGuest(); - - protected User(WdkModel wdkModel, long userId, String email, String signature, String stableId) { - _wdkModel = wdkModel; - _userId = userId; - _email = email; - _signature = signature; - _stableId = stableId; - _preferences = new UserPreferences(); - } - - public long getUserId() { - return _userId; - } - - public String getEmail() { - return _email; - } - - public String getSignature() { - return _signature; - } - - public String getStableId() { - return _stableId; - } - - public void setEmail(String email) { - _email = email; - } - - /** - * 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 void setProfileProperties(Map properties) { - _properties = properties; - } - - /** - * Return the entire user profile property map - * @return - */ - public Map getProfileProperties() { - return _properties; - } - - /** - * Removes all existing user profile properties - */ - public void clearProfileProperties() { - _properties.clear(); - } - - public void setPreferences(UserPreferences preferences) { - _preferences = preferences; - } - - public UserPreferences getPreferences() { - return _preferences; - } - - public WdkModel getWdkModel() { - return _wdkModel; - } - - @Override - public String toString() { - return "User #" + getUserId() + " - " + getEmail(); - } - - @Override - public int hashCode() { - return (int)getUserId(); - } - - @Override - 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()) - ); - } - -/** - * 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; - } -} +package org.gusdb.wdk.model.user; + +import org.gusdb.wdk.model.WdkModel; + +public interface User extends org.gusdb.oauth2.client.veupathdb.User { + + WdkModel getWdkModel(); + +} 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 87093118a6..0000000000 --- a/Model/src/main/java/org/gusdb/wdk/model/user/UserCreationScript.java +++ /dev/null @@ -1,162 +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(), - parsedLine.getGlobalUserPrefs(), Collections.emptyMap(), true, false); - 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 - UserPreferences userPrefs = user.getPreferences(); - for (Entry newPref : parsedLine.getGlobalUserPrefs().entrySet()) { - userPrefs.setGlobalPreference(newPref.getKey(), newPref.getValue()); - } - user.setPreferences(userPrefs); - model.getUserFactory().savePreferences(user); - 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 caa5b3e004..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 @@ -1,30 +1,32 @@ package org.gusdb.wdk.model.user; -import java.sql.Timestamp; -import java.sql.Types; -import java.util.Date; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Random; -import java.util.regex.Matcher; +import java.util.Set; +import java.util.stream.Collectors; import org.apache.log4j.Logger; -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.SingleLongResultSetHandler; -import org.gusdb.fgputil.db.runner.SingleLongResultSetHandler.Status; +import org.gusdb.fgputil.Tuples.TwoTuple; 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.ExpiredTokenException; +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.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.model.user.UnregisteredUser.UnregisteredUserType; +import org.json.JSONObject; + +import io.prometheus.client.Counter; /** * Manages persistence of user profile and preferences and creation and @@ -38,302 +40,148 @@ public class UserFactory { @SuppressWarnings("unused") private static Logger LOG = Logger.getLogger(UserFactory.class); - // ------------------------------------------------------------------------- - // 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$$"; - - private static final String COUNT_USER_REF_BY_ID_SQL = - "select count(*)" + - " 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 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 final Counter GUEST_CREATION_COUNTER = Counter.build() + .name("wdk_guest_creation_count") + .help("Number of guest users created by WDK services") + .register(); // ------------------------------------------------------------------------- - // 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 DatabaseInstance _userDb; - private final String _userSchema; - private final AccountManager _accountManager; - private final UserPreferenceFactory _preferenceFactory; + private final UserReferenceFactory _userRefFactory; + private final UserPasswordEmailer _emailer; + private final OAuthConfig _config; + private final OAuthClient _client; // ------------------------------------------------------------------------- // constructor // ------------------------------------------------------------------------- public UserFactory(WdkModel wdkModel) { + // save model for populating new users _wdkModel = wdkModel; - _preferenceFactory = new UserPreferenceFactory(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()); + _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, - Map globalPreferences, - Map projectPreferences) - 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); + public ValidatedToken getBearerTokenFromAuthCode(String authCode, String redirectUri) throws InvalidPropertiesException { + return _client.getBearerTokenFromAuthCode(_config, authCode, redirectUri); } - public User createUser(String email, - Map profileProperties, - Map globalPreferences, - Map projectPreferences, - 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(); - - // add user to account DB - UserProfile profile = _accountManager.createAccount(email, password, profileProperties); - - // 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 - RegisteredUser user = new RegisteredUser(_wdkModel, profile.getUserId(), - 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()); - } - - return user; - } - catch (WdkModelException | InvalidUsernameOrEmailException e) { - // do not wrap known exceptions - throw e; - } - catch (Exception e) { - // wrap unknown exceptions with WdkModelException - throw new WdkModelException("Could not completely create new user", e); - } + public ValidatedToken getBearerTokenFromCredentials(String email, String password, String redirectUrl) throws InvalidPropertiesException { + return _client.getBearerTokenFromUsernamePassword(_config, email, password, redirectUrl); } - public void savePreferences(User user) throws WdkModelException { - _preferenceFactory.savePreferences(user); + public ValidatedToken validateBearerToken(String rawToken) throws InvalidTokenException, ExpiredTokenException { + return _client.getValidatedEcdsaSignedToken(_config.getOauthUrl(), rawToken); } - 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); - } + // ------------------------------------------------------------------------- + // methods to manage users + // ------------------------------------------------------------------------- - 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); + public User convertToUser(ValidatedToken token) throws WdkModelException { + User user = new BearerTokenUser(_wdkModel, _client, _config, token); + _userRefFactory.addUserReference(user); + return user; } - 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())); + 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(); - - // 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)); - - Utilities.sendEmail(smtpServer, user.getEmail(), supportEmail, emailSubject, emailContent); - } + public User createUser(String email, Map profileProperties) + throws WdkModelException, InvalidPropertiesException, InvalidUsernameOrEmailException { + try { - 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 one."); - return email; - } + // 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)); - 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)); + 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; } - return buffer.toString(); + catch (ConflictException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); + } + } + + private TwoTuple parseExpandedUserJson(JSONObject userJson) { + User user = new BasicUser(_wdkModel, userJson); + String password = userJson.getString(IdTokenFields.password.name()); + return new TwoTuple<>(user, password); } /** - * Create an unregistered user of the specified type and persist in the database + * Save the basic information of a user * - * @param userType unregistered user type to persist - * @return new unregistered user with remaining fields populated - * @throws WdkRuntimeException if unable to persist temporary user + * @param user + * @param newUser + * @throws InvalidPropertiesException */ - public UnregisteredUser createUnregistedUser(UnregisteredUserType userType) throws WdkRuntimeException { + public User saveUser(User oldUser, User newUser, ValidatedToken authorizationToken) throws InvalidUsernameOrEmailException, InvalidPropertiesException { try { - UserProfile profile = _accountManager.createGuestAccount(userType.getStableIdPrefix()); - addUserReference(profile.getUserId(), true); - return new UnregisteredUser(_wdkModel, profile.getUserId(), profile.getEmail(), profile.getSignature(), profile.getStableId()); - } - catch (Exception e) { - throw new WdkRuntimeException("Unable to save temporary user", e); - } - } + // 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)); + } - private User completeLogin(User user) { - if (user == null) - return user; + // build a user from the response + User savedUser = new BasicUser(_wdkModel, _client.modifyUser(_config, authorizationToken, props)); - // make sure user has reference in this user DB (needs to happen before merging) - if (!hasUserReference(user.getUserId())) { - addUserReference(user.getUserId(), false); + Events.trigger(new UserProfileUpdateEvent(oldUser, savedUser, _wdkModel)); + return savedUser; + } + catch (ConflictException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); } - - // update user active timestamp - _accountManager.updateLastLogin(user.getUserId()); - - return user; } - public User login(User guest, String email, String password) - throws WdkUserException, WdkModelException { - // make sure the guest is really a guest - if (!guest.isGuest()) - throw new WdkUserException("User has been logged in."); + public void resetPassword(String loginName) throws InvalidUsernameOrEmailException, WdkModelException { + try { + TwoTuple user = parseExpandedUserJson(_client.resetPassword(_config, loginName)); - // 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."); + // email user new password + _emailer.emailTemporaryPassword(user.getFirst(), user.getSecond()); + } + catch (InvalidPropertiesException e) { + throw new InvalidUsernameOrEmailException(e.getMessage()); } - 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."))); - } + // ------------------------------------------------------------------------- + // methods to query users + // ------------------------------------------------------------------------- - /** - * 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; + public Map getUsersById(List userIds) { + return OAuthQuerier.getUsersById(_client, _config, userIds, json -> new BasicUser(_wdkModel, json)); } - private User authenticate(String usernameOrEmail, String password) throws WdkModelException { - return populateRegisteredUser(_accountManager.getUserProfile(usernameOrEmail, password)); + public Map getUsersByEmail(List emails) { + return OAuthQuerier.getUsersByEmail(_client, _config, emails, json -> new BasicUser(_wdkModel, json)); } /** @@ -344,119 +192,15 @@ private User authenticate(String usernameOrEmail, String password) throws WdkMod * @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(populateRegisteredUser(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())); - - } - else { - // user does not exist in account or user DBs - return Optional.empty(); - } - } + return Optional.ofNullable(getUsersById(List.of(userId)).get(userId)); } - public User getUserByEmail(String email) throws WdkModelException { - return populateRegisteredUser(_accountManager.getUserProfileByEmail(email)); - } - - private User getUserProfileByUsernameOrEmail(String usernameOrEmail) throws WdkModelException { - return populateRegisteredUser(_accountManager.getUserProfileByUsernameOrEmail(usernameOrEmail)); - } - - public User getUserBySignature(String signature) throws WdkModelException, 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) throws WdkModelException { - 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; - } - - /** - * Save the basic information of a user - * - * @param user - */ - 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(), user.getProfileProperties()); - - // get updated profile and trigger profile update event - UserProfile newProfile = _accountManager.getUserProfile(user.getUserId()); - Events.trigger(new UserProfileUpdateEvent(oldProfile, newProfile, _wdkModel)); - - // save preferences - _preferenceFactory.savePreferences(user); - } - 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 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); - // email user new password - emailTemporaryPassword(user, newPassword, _wdkModel.getModelConfig()); + public Optional getUserByEmail(String email) { + return Optional.ofNullable(getUsersByEmail(List.of(email)).get(email)); } - public void changePassword(long userId, String newPassword) { - _accountManager.updatePassword(userId, newPassword); + 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..7a083f7060 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserPasswordEmailer.java @@ -0,0 +1,51 @@ +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 WdkModel _wdkModel; + + public UserPasswordEmailer(WdkModel wdkModel) { + _wdkModel = wdkModel; + } + + public boolean isSendWelcomeEmail() { + // 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 (!isSendWelcomeEmail()) return; + + 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() + .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/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/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/model/user/UserReferenceFactory.java b/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java new file mode 100644 index 0000000000..2673c9cc65 --- /dev/null +++ b/Model/src/main/java/org/gusdb/wdk/model/user/UserReferenceFactory.java @@ -0,0 +1,113 @@ +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 }; + + // 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); + } + 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? + 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( + 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/OAuthClient.java b/Model/src/main/java/org/gusdb/wdk/session/OAuthClient.java deleted file mode 100644 index dd56f0f0c7..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.wdk.model.WdkModelException; -import org.gusdb.wdk.model.config.OAuthConfig; -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/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/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/pom.xml b/Service/pom.xml index 4b89378b8f..85c1b2a16f 100644 --- a/Service/pom.xml +++ b/Service/pom.xml @@ -38,17 +38,17 @@ org.gusdb - fgputil-accountdb + fgputil-server org.gusdb - fgputil-server + fgputil-client org.gusdb - fgputil-client + oauth2-client 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/filter/CheckLoginFilter.java b/Service/src/main/java/org/gusdb/wdk/service/filter/CheckLoginFilter.java index d4ea31b860..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 @@ -1,14 +1,13 @@ 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; 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,24 +15,40 @@ 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 javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +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.LoginCookieFactory; 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.controller.filter.CheckLoginFilterShared; +import org.gusdb.wdk.model.Utilities; +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; +import org.gusdb.wdk.service.service.SessionService; import org.gusdb.wdk.service.service.SystemService; @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 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 +58,75 @@ 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()); + UserFactory factory = ContextLookup.getWdkModel(context).getUserFactory(); + + // try to find submitted bearer token + String rawToken = findRawBearerToken(request, requestContext); + + try { + if (rawToken == null) { + // no credentials submitted; automatically create a guest to use on this request + useNewGuest(factory, request, requestContext, requestPath); + } + else { + 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()); + } + } + } + catch (Exception e) { + // 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); + } + } - Optional newCookie = - CheckLoginFilterShared.calculateUserActions( - findLoginCookie(requestContext.getCookies()), - ContextLookup.getWdkModel(context), - request.getSession(), requestPath); + 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); - if (newCookie.isPresent()) { - requestContext.setProperty(SESSION_COOKIE_TO_SET, newCookie.get().toJaxRsCookie().toString()); + 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) { + 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 +134,22 @@ 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 + NewCookie authCookie = SessionService.getAuthCookie(tokenValue); + headers.add(HttpHeaders.SET_COOKIE, authCookie.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/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/formatter/ProjectFormatter.java b/Service/src/main/java/org/gusdb/wdk/service/formatter/ProjectFormatter.java index 466fcae614..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 @@ -1,9 +1,12 @@ package org.gusdb.wdk.service.formatter; -import org.gusdb.fgputil.accountdb.UserPropertyName; +import java.util.Optional; + +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; +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 (UserProperty 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 7967c2978d..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 @@ -1,9 +1,8 @@ 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.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; @@ -29,27 +28,26 @@ public class UserFormatter { public static JSONObject getUserJson(User user, boolean isOwner, - boolean includePreferences, 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)); - if (includePreferences) { - json.put(JsonKeys.PREFERENCES, getPreferencesJson(user.getPreferences())); - } + 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 (UserProperty 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/request/user/UserCreationRequest.java b/Service/src/main/java/org/gusdb/wdk/service/request/user/UserCreationRequest.java index 96d5a6a96f..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,8 +1,8 @@ package org.gusdb.wdk.service.request.user; -import java.util.List; +import java.util.Collection; -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 +15,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); @@ -34,7 +34,6 @@ public UserCreationRequest(UserProfileRequest profileRequest, UserPreferencesReq _profileRequest = profileRequest; _globalPreferencesRequest = globalPreferencesRequest; _projectPreferencesRequest = projectPreferencesRequest; - } public UserProfileRequest getProfileRequest() { @@ -44,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 e3a1968567..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 @@ -12,10 +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.config.ModelConfigAccountDB; +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; @@ -26,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; @@ -46,27 +44,24 @@ public class UserDatasetShareRequest { private List _invalidDatasetIds; public UserDatasetShareRequest(WdkModel wdkModel) { - ModelConfig modelConfig = wdkModel.getModelConfig(); - ModelConfigAccountDB accountDbConfig = modelConfig.getAccountDB(); - _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 { @@ -74,121 +69,127 @@ 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/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/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 392b132dd9..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 @@ -25,7 +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; @@ -38,7 +40,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 +100,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; @@ -141,27 +138,32 @@ protected Map> getHeaders() { return _headers.getRequestHeaders(); } - protected SessionProxy getSession() { - return getRequest().getSession(); + /** + * 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 TemporaryUserData getTemporaryUserData() { + return TemporaryUserDataStore.instance().get(getRequestingUser().getUserId()); } - 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 ValidatedToken getAuthorizationToken() { + return (ValidatedToken)getRequest().getAttributeMap().get(Utilities.BEARER_TOKEN_KEY); } protected boolean isSessionUserAdmin() { List adminEmails = getWdkModel().getModelConfig().getAdminEmails(); - return adminEmails.contains(getSessionUser().getEmail()); + return adminEmails.contains(getRequestingUser().getEmail()); } protected void assertAdmin() { @@ -178,7 +180,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()); } /** @@ -215,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/AnswerService.java b/Service/src/main/java/org/gusdb/wdk/service/service/AnswerService.java index d284af881a..83deb7d4de 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/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..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,11 +258,12 @@ 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) .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 @@ -336,7 +337,7 @@ public JSONObject getFilterParamOntologyTermSummary( .builder() .putAll(contextParamValues) .buildValidated( - getSessionUser(), + getRequestingUser(), question.getQuery(), StepContainer.emptyContainer(), ValidationLevel.DISPLAYABLE, @@ -391,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 2368504baa..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 @@ -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; @@ -13,39 +15,41 @@ 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.events.Events; +import org.gusdb.fgputil.Tuples.TwoTuple; 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.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.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.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.OAuthClient; -import org.gusdb.wdk.session.OAuthUtil; 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); + public static final int EXPIRATION_3_YEARS_SECS = 3 * 365 * 24 * 60 * 60; + private static final String REFERRER_HEADER_KEY = "Referer"; // OAuth2 parameter keys @@ -54,19 +58,54 @@ 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="; + // 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()); + getTemporaryUserData().put(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()); + return EncryptionUtil.encrypt(saltedString); + } + @GET @Path("login") public Response processOauthLogin( @@ -75,8 +114,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(); } @@ -84,8 +125,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(); } @@ -99,29 +140,29 @@ public Response processOauthLogin( throw new WdkModelException(errorMessage); } - String storedStateToken = (String) getSession().getAttribute(OAuthUtil.STATE_TOKEN_KEY); - getSession().removeAttribute(OAuthUtil.STATE_TOKEN_KEY); + // get state token off session and remove; only needed for this request + 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)) { throw new WdkModelException("Unable to log in; state token missing, incorrect, or expired."); } - OAuthClient client = new OAuthClient(modelConfig); - - // 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 + 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); // login successful; create redirect response - return getSuccessResponse(newUser, oldUser, redirectUrl, true); + 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); @@ -135,12 +176,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(); //} @@ -150,25 +192,27 @@ 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(); } - // log in the user - User newUser = wdkModel.getUserFactory().login(oldUser, request.getEmail(), request.getPassword()); + // Use passed credentials to get the bearer token, then convert to User + 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); - return getSuccessResponse(newUser, oldUser, redirectUrl, false); + return getSuccessResponse(bearerToken, newUser, oldUser, redirectUrl, false); } catch (JSONException e) { throw new RequestMisformatException(e.getMessage()); } - catch (Exception ex) { + catch (InvalidPropertiesException ex) { LOG.error("Could not authenticate user's identity. Exception thrown: ", ex); return createJsonResponse(false, "Invalid username or password", null).build(); } @@ -183,7 +227,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 @@ -191,20 +237,27 @@ 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(); - 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()); - redirectUrl = getSuccessRedirectUrl(redirectUrl, newUser, loginCookie); + + TemporaryUserData tmpData = getTemporaryUserData(); + + synchronized(tmpData) { + + // 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, bearerTokenCookie); + return (isRedirectResponse ? createRedirectResponse(redirectUrl) : createJsonResponse(true, null, redirectUrl) - ).cookie(loginCookie.toJaxRsCookie()).build(); + ) + .cookie(bearerTokenCookie.toJaxRsCookie()) + .build(); } } @@ -216,7 +269,7 @@ private Response getSuccessResponse(User newUser, User oldUser, * @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 bearerTokenCookie) { return redirectUrl; } @@ -225,17 +278,15 @@ 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(); - getSession().invalidate(); - - // get a new session and add new guest user to it - SessionProxy session = getSession(); - User newUser = getWdkModel().getUserFactory().createUnregistedUser(UnregisteredUserType.GUEST); - session.setAttribute(Utilities.WDK_USER_KEY, newUser); + User oldUser = getRequestingUser(); + getTemporaryUserData().invalidate(); - // throw new user event - Events.triggerAndWait(new NewUserEvent(newUser, oldUser, session), - new WdkRuntimeException("Unable to complete WDK user assignement.")); + // 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(); // create and append logout cookies to response Set logoutCookies = new HashSet<>(); @@ -245,13 +296,25 @@ public Response processLogout() throws WdkModelException { extraCookie.setMaxAge(-1); logoutCookies.add(extraCookie); } + ResponseBuilder builder = createRedirectResponse(getContextUri()); 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 * @@ -294,90 +357,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/TemporaryFileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/TemporaryFileService.java index eb100dd847..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 = getSession(); + 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 = getSession(); + 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); } @@ -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); } /** @@ -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/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/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/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/ProfileService.java b/Service/src/main/java/org/gusdb/wdk/service/service/user/ProfileService.java index 8a116fff41..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 @@ -1,46 +1,35 @@ 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; import javax.ws.rs.core.MediaType; -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.oauth2.exception.InvalidPropertiesException; 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.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; 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; -/* - * 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."; - public ProfileService(@PathParam(USER_ID_PATH_PARAM) String uid) { super(uid); } @@ -51,10 +40,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,8 +54,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 { - return UserFormatter.getUserJson(user, isSessionUser, includePrefs, propNames); + 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); } /** @@ -84,119 +73,28 @@ 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); - // save user to session - getSession().setAttribute(Utilities.WDK_USER_KEY, user); - return getProfileUpdateResponse(loginCookie); + User newUser = new BasicUser(oldUser); + newUser.setEmail(request.getEmail()); + newUser.setPropertyValues(request.getProfileMap()); + + // save user to OAuth + newUser = getWdkModel().getUserFactory().saveUser(oldUser, newUser, getAuthorizationToken()); + + 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), getPropertiesConfig(), 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(); - } - - private List getPropertiesConfig() { - return getWdkModel().getModelConfig().getAccountDB().getUserPropertyNames(); - } - - @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 - * 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/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())); 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 488930d45c..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 @@ -1,9 +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; @@ -13,18 +14,20 @@ 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.config.ModelConfigAccountDB; 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; @@ -48,28 +51,36 @@ 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); - List configuredUserProps = getWdkModel().getModelConfig().getAccountDB().getUserPropertyNames(); + Collection configuredUserProps = User.USER_PROPERTIES.values(); 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())) .build(); } catch (InvalidUsernameOrEmailException e) { + throw new ConflictException(e.getMessage()); + } + catch (InvalidPropertiesException e) { throw new DataValidationException(e.getMessage()); } catch (JSONException e) { @@ -94,19 +105,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); + } } /** @@ -133,15 +141,10 @@ 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(); + 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(); } catch (JSONException e) { throw new RequestMisformatException(e.getMessage());