Vojtech Szocs has uploaded a new change for review.
Change subject: webadmin: (WiP) Improve UI Plugin vs. REST API integration
......................................................................
webadmin: (WiP) Improve UI Plugin vs. REST API integration
Patch currently Work-in-Progress.
This patch provides client-side fix for following use case:
a. assume restapi-session-timeout > engine-session-timeout
- UI Plugin infra uses restapi-session-timeout=360min
- by default, engine-session-timeout=30min
b. user logs into WebAdmin, WebAdmin's UI Plugin infra acquires
REST API session (using GUI login credentials)
c. user is inactive for engine-session-timeout [min]
- Engine session will be invalidated
- however, REST API session is still active
d. WebAdmin detects that Engine session is invalid and takes
the user back to login screen
e. user logs into WebAdmin again, WebAdmin's UI Plugin infra
tries to acquire REST API session again
- REST API session (JSESSIONID cookie) is still active
so backend will reuse it
- REST API backend attempts to validate the Engine session,
the Engine session is invalid so backend sends HTTP 401
"Auth Required" response to client
f. as a result:
- user sees "Auth Required" browser-specific popup in browser
- UI Plugin vs. REST API integration is broken for current
user login session, i.e. WebAdmin didn't receive JSESSIONID
response header from REST API backend
This patch makes following changes:
- detect current Engine session timeout, embed it into WebAdmin
host page and read it during WebAdmin startup
- tell RestApiSessionManager to acquire REST API session using
timeout = current-engine-session-timeout
- while the user stays authenticated in WebAdmin GUI, keep
REST API *and* Engine session alive via heartbeat requests [1]
This has following implications on existing UI plugins:
- REST API session timeout is no longer 360min (it's now equal
to current-engine-session-timeout) so plugins and/or other
systems utilizing REST API session should be prepared to deal
with shorter timeout periods [2]
- plugins and/or other systems utilizing REST API session can
now rely on session keep-alive behavior implemented in GUI [3]
[1] keep-alive behavior re-introduced after changes in patch
http://gerrit.ovirt.org/#/c/14411/
[2] in practice, REST API session is usable only as long as
the associated Engine session is active
[3] keep-alive active as long as the user stays authenticated
while having WebAdmin GUI open in the browser
Change-Id: I0b913e78c0ddb54011670c421d6ff5d12c965d6b
Bug-Url: https://bugzilla.redhat.com/1011058
Signed-off-by: Vojtech Szocs <[email protected]>
---
M
frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServlet.java
M
frontend/webadmin/modules/frontend/src/main/resources/META-INF/resources/GwtHostPage.jsp
M
frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServletTest.java
A
frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/EngineSessionTimeoutData.java
M
frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/RestApiSessionManager.java
M
frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/system/ApplicationInit.java
6 files changed, 115 insertions(+), 7 deletions(-)
git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/04/20404/1
diff --git
a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServlet.java
b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServlet.java
index 835b31e..7f50d2f 100644
---
a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServlet.java
+++
b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServlet.java
@@ -32,6 +32,7 @@
protected static final String ATTR_APPLICATION_MODE = "applicationMode";
//$NON-NLS-1$
protected static final String ATTR_PLUGIN_DEFS = "pluginDefinitions";
//$NON-NLS-1$
+ protected static final String ATTR_ENGINE_SESSION_TIMEOUT =
"engineSessionTimeout"; //$NON-NLS-1$
@Override
protected String getSelectorScriptName() {
@@ -54,6 +55,10 @@
List<PluginData> pluginData = getPluginData();
request.setAttribute(ATTR_PLUGIN_DEFS,
getPluginDefinitionsArray(pluginData));
+ // Set attribute for engineSessionTimeout object
+ Integer engineSessionTimeout = getEngineSessionTimeout();
+ request.setAttribute(ATTR_ENGINE_SESSION_TIMEOUT,
getEngineSessionTimeoutObject(engineSessionTimeout));
+
super.doGet(request, response);
}
@@ -66,6 +71,9 @@
// Update based on pluginDefinitions array
digest.update(request.getAttribute(ATTR_PLUGIN_DEFS).toString().getBytes());
+
+ // Update based on engineSessionTimeout object
+
digest.update(request.getAttribute(ATTR_ENGINE_SESSION_TIMEOUT).toString().getBytes());
return digest;
}
@@ -102,4 +110,15 @@
return arr;
}
+ protected Integer getEngineSessionTimeout() {
+ // TODO retrieve actual config value
+ return 30;
+ }
+
+ protected ObjectNode getEngineSessionTimeoutObject(Integer
engineSessionTimeout) {
+ ObjectNode obj = createObjectNode();
+ obj.put("value", String.valueOf(engineSessionTimeout)); //$NON-NLS-1$
+ return obj;
+ }
+
}
diff --git
a/frontend/webadmin/modules/frontend/src/main/resources/META-INF/resources/GwtHostPage.jsp
b/frontend/webadmin/modules/frontend/src/main/resources/META-INF/resources/GwtHostPage.jsp
index 6179221..5028812 100644
---
a/frontend/webadmin/modules/frontend/src/main/resources/META-INF/resources/GwtHostPage.jsp
+++
b/frontend/webadmin/modules/frontend/src/main/resources/META-INF/resources/GwtHostPage.jsp
@@ -19,6 +19,9 @@
<c:if test="${requestScope['pluginDefinitions'] != null}">
var pluginDefinitions = <c:out
value="${requestScope['pluginDefinitions']}" escapeXml="false"/>;
</c:if>
+ <c:if test="${requestScope['engineSessionTimeout'] != null}">
+ var engineSessionTimeout = <c:out
value="${requestScope['engineSessionTimeout']}" escapeXml="false"/>;
+ </c:if>
<c:if test="${requestScope['messages'] != null}">
var messages = <c:out value="${requestScope['messages']}"
escapeXml="false"/>;
</c:if>
diff --git
a/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServletTest.java
b/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServletTest.java
index 84119ab..4455abd 100644
---
a/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServletTest.java
+++
b/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/server/gwt/WebAdminHostPageServletTest.java
@@ -43,6 +43,9 @@
// Cannot use @Mock since ArrayNode is final
private ArrayNode mockPluginDefinitionsArray;
+ @Mock
+ private ObjectNode mockEngineSessionTimeoutObject;
+
@Before
public void setUpMockRequest() {
ObjectMapper mapper = new ObjectMapper();
@@ -53,6 +56,7 @@
when(mockApplicationModeObject.toString()).thenReturn(APPLICATION_MODE);
when(mockRequest.getAttribute(WebAdminHostPageServlet.ATTR_APPLICATION_MODE)).thenReturn(mockApplicationModeObject);
when(mockRequest.getAttribute(WebAdminHostPageServlet.ATTR_PLUGIN_DEFS)).thenReturn(mockPluginDefinitionsArray);
+
when(mockRequest.getAttribute(WebAdminHostPageServlet.ATTR_ENGINE_SESSION_TIMEOUT)).thenReturn(mockEngineSessionTimeoutObject);
}
@Override
@@ -81,9 +85,11 @@
public void testDoGet_ExtraAttributes_WithoutUserInfoObject() throws
IOException, ServletException {
doReturn(mockApplicationModeObject).when(testServlet).getApplicationModeObject(any(Integer.class));
doReturn(mockPluginDefinitionsArray).when(testServlet).getPluginDefinitionsArray(anyListOf(PluginData.class));
+
doReturn(mockEngineSessionTimeoutObject).when(testServlet).getEngineSessionTimeoutObject(any(Integer.class));
testServlet.doGet(mockRequest, mockResponse);
verify(mockRequest).setAttribute(WebAdminHostPageServlet.ATTR_APPLICATION_MODE,
mockApplicationModeObject);
verify(mockRequest).setAttribute(WebAdminHostPageServlet.ATTR_PLUGIN_DEFS,
mockPluginDefinitionsArray);
+
verify(mockRequest).setAttribute(WebAdminHostPageServlet.ATTR_ENGINE_SESSION_TIMEOUT,
mockEngineSessionTimeoutObject);
}
@Test
@@ -125,4 +131,10 @@
}
}
+ @Test
+ public void testGetEngineSessionTimeoutObject() {
+ ObjectNode result =
testServlet.getEngineSessionTimeoutObject(Integer.valueOf(30));
+ assertEquals(result.get("value").asText(), "30"); //$NON-NLS-1$
//$NON-NLS-2$
+ }
+
}
diff --git
a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/EngineSessionTimeoutData.java
b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/EngineSessionTimeoutData.java
new file mode 100644
index 0000000..92a3f35
--- /dev/null
+++
b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/EngineSessionTimeoutData.java
@@ -0,0 +1,21 @@
+package org.ovirt.engine.ui.webadmin.plugin.restapi;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Overlay type for {@code engineSessionTimeout} global JS object.
+ */
+public final class EngineSessionTimeoutData extends JavaScriptObject {
+
+ protected EngineSessionTimeoutData() {
+ }
+
+ public static native EngineSessionTimeoutData instance() /*-{
+ return $wnd.engineSessionTimeout;
+ }-*/;
+
+ public native String getValue() /*-{
+ return this.value;
+ }-*/;
+
+}
diff --git
a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/RestApiSessionManager.java
b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/RestApiSessionManager.java
index c67dd0e..684e36a 100644
---
a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/RestApiSessionManager.java
+++
b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/plugin/restapi/RestApiSessionManager.java
@@ -6,6 +6,8 @@
import org.ovirt.engine.ui.common.system.ClientStorage;
import org.ovirt.engine.ui.frontend.utils.FrontendUrlUtils;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
@@ -21,6 +23,7 @@
* <ul>
* <li>acquire new session upon successful user authentication (classic login)
* <li>reuse existing session if the user is already authenticated (auto login)
+ * <li>keep the current session alive while the user stays authenticated
* </ul>
* <p>
* Note that the REST API session is not closed upon user logout, as there
might be other systems still working with it.
@@ -56,19 +59,32 @@
private static final Logger logger =
Logger.getLogger(RestApiSessionManager.class.getName());
- private static final String SESSION_TIMEOUT = "360"; //$NON-NLS-1$
private static final String SESSION_ID_HEADER = "JSESSIONID"; //$NON-NLS-1$
private static final String SESSION_ID_KEY = "RestApiSessionId";
//$NON-NLS-1$
+ private static final String DEFAULT_SESSION_TIMEOUT = "30"; //$NON-NLS-1$
+
+ // Heartbeat (delay) between REST API keep-alive requests
+ private static final int SESSION_HEARTBEAT_MS = 1000 * 60; // 1 minute
private final EventBus eventBus;
private final ClientStorage clientStorage;
private final String restApiBaseUrl;
+
+ private String sessionTimeout;
@Inject
public RestApiSessionManager(EventBus eventBus, ClientStorage
clientStorage) {
this.eventBus = eventBus;
this.clientStorage = clientStorage;
this.restApiBaseUrl = FrontendUrlUtils.getRootURL() + "api";
//$NON-NLS-1$
+ }
+
+ public void setSessionTimeout(String sessionTimeout) {
+ this.sessionTimeout = sessionTimeout;
+ }
+
+ String getSessionTimeout() {
+ return sessionTimeout != null ? sessionTimeout :
DEFAULT_SESSION_TIMEOUT;
}
void sendRequest(RequestBuilder requestBuilder, RestApiCallback callback) {
@@ -79,22 +95,52 @@
}
}
+ RequestBuilder createRequest() {
+ RequestBuilder requestBuilder = new RequestBuilder(RequestBuilder.GET,
restApiBaseUrl);
+ requestBuilder.setHeader("Prefer", "persistent-auth"); //$NON-NLS-1$
//$NON-NLS-2$
+ requestBuilder.setHeader("Session-TTL", getSessionTimeout());
//$NON-NLS-1$
+ return requestBuilder;
+ }
+
+ void scheduleKeepAliveHeartbeat() {
+ Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
+ @Override
+ public boolean execute() {
+ String sessionId = getSessionId();
+
+ if (sessionId != null) {
+ // The session is still in use
+ RequestBuilder requestBuilder = createRequest();
+
+ // Note: the browser takes care of sending JSESSIONID
cookie for this request automatically
+ sendRequest(requestBuilder, new RestApiCallback() {
+ // No response post-processing, as we expect existing
REST API (and associated Engine)
+ // session to stay alive by means of keep-alive
requests
+ });
+
+ // Proceed with the heartbeat
+ return true;
+ } else {
+ // The session has been released, cancel the heartbeat
+ return false;
+ }
+ }
+ }, SESSION_HEARTBEAT_MS);
+ }
+
/**
* Acquires new REST API session using the given credentials.
*/
public void acquireSession(String userNameWithDomain, String password) {
- RequestBuilder requestBuilder = new RequestBuilder(RequestBuilder.GET,
restApiBaseUrl);
- requestBuilder.setHeader("Prefer", "persistent-auth"); //$NON-NLS-1$
//$NON-NLS-2$
- requestBuilder.setHeader("Session-TTL", SESSION_TIMEOUT); //$NON-NLS-1$
+ RequestBuilder requestBuilder = createRequest();
requestBuilder.setUser(userNameWithDomain);
requestBuilder.setPassword(password);
sendRequest(requestBuilder, new RestApiCallback() {
@Override
protected void processResponse(Response response) {
- // Obtain the session ID from response header, as we're unable
- // to access Engine REST API JSESSIONID cookie value directly
- // (cookie is set for different path than WebAdmin host page)
+ // Obtain session ID from response header, as we're unable to
access REST API
+ // JSESSIONID cookie directly (cookie set for different path
than WebAdmin page)
String sessionIdFromHeader =
response.getHeader(SESSION_ID_HEADER);
if (sessionIdFromHeader != null) {
@@ -114,6 +160,7 @@
if (sessionId != null) {
RestApiSessionAcquiredEvent.fire(eventBus, sessionId);
+ scheduleKeepAliveHeartbeat();
} else {
RestApiSessionManager.logger.severe("Engine REST API session ID is
not available"); //$NON-NLS-1$
}
diff --git
a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/system/ApplicationInit.java
b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/system/ApplicationInit.java
index aaaeb06..2521cf3 100644
---
a/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/system/ApplicationInit.java
+++
b/frontend/webadmin/modules/webadmin/src/main/java/org/ovirt/engine/ui/webadmin/system/ApplicationInit.java
@@ -19,6 +19,7 @@
import org.ovirt.engine.ui.uicompat.EventArgs;
import org.ovirt.engine.ui.uicompat.IEventListener;
import org.ovirt.engine.ui.webadmin.ApplicationDynamicMessages;
+import org.ovirt.engine.ui.webadmin.plugin.restapi.EngineSessionTimeoutData;
import org.ovirt.engine.ui.webadmin.plugin.restapi.RestApiSessionManager;
import org.ovirt.engine.ui.webadmin.uimode.UiModeData;
@@ -52,6 +53,11 @@
if (uiModeData != null) {
handleUiMode(uiModeData);
}
+
+ EngineSessionTimeoutData engineSessionTimeoutData =
EngineSessionTimeoutData.instance();
+ if (engineSessionTimeoutData != null) {
+
restApiSessionManager.setSessionTimeout(engineSessionTimeoutData.getValue());
+ }
}
@Override
--
To view, visit http://gerrit.ovirt.org/20404
To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I0b913e78c0ddb54011670c421d6ff5d12c965d6b
Gerrit-PatchSet: 1
Gerrit-Project: ovirt-engine
Gerrit-Branch: master
Gerrit-Owner: Vojtech Szocs <[email protected]>
_______________________________________________
Engine-patches mailing list
[email protected]
http://lists.ovirt.org/mailman/listinfo/engine-patches