Vojtech Szocs has uploaded a new change for review.
Change subject: webadmin: Use existing Engine session for REST API integration
......................................................................
webadmin: Use existing Engine session for REST API integration
Before this patch
=================
* WebAdmin login triggers creation of separate (logical) Engine session
through creation of new (physical) REST session using HTTP basic auth
-> REST session is acquired using current WebAdmin user credentials
-> REST session ID is provided to all UI plugins
* above causes two separate user login operations with same credentials
and therefore two "user has logged in" events in Engine server log
* acquired (physical) REST session, as well as corresponding (logical)
Engine session, are _NOT_ closed upon WebAdmin logout, even though
these sessions were created with WebAdmin user credentials
Assume the user has logged into WebAdmin, there will be two separate
Engine sessions:
(1) Engine session for WebAdmin user, created through WebAdmin code
running login user command
(2) Engine session for UI plugins, created through WebAdmin code
requesting new REST session (using HTTP basic auth, passing
same WebAdmin user credentials)
Assume the user has logged out of WebAdmin, Engine session (2) along
with the corresponding physical (REST) session will still be active.
After this patch
================
* WebAdmin login _DOES NOT_ trigger creation of separate (logical)
Engine session -> instead, it reuses existing Engine user session
-> REST session is still acquired, but instead of HTTP basic auth
credentials, it passes existing Engine session auth token
-> REST session ID is provided to all UI plugins
* above ensures single user login operation upon WebAdmin login
* since REST session maps to existing Engine session, WebAdmin user
logout makes REST session unusable, even if the REST session itself
is still alive
Existing UI plugin API/behavior regarding REST integration remains
unchanged - WebAdmin login creates new REST session (now mapped to
existing Engine session), JSESSIONID cookie for /api is stored in
in the browser and "RestApiSessionAcquired" hook is used to broadcast
REST session ID (JSESSIONID) value to all UI plugins.
Important change for UI plugin developers
=========================================
REST session ID passed to UI plugins via "RestApiSessionAcquired"
hook, also represented by JSESSIONID cookie for /api, will become
unusable after WebAdmin logout.
Since UI plugins are active (invoked by the infra) only while the
user is authenticated, this shouldn't impact UI plugins that use
provided REST session (cookie) to talk directly with Engine.
Change-Id: Ic3905b3b5834a0f7327321e93064274df0d1db65
Bug-Url: https://bugzilla.redhat.com/1161734
Bug-Url: https://bugzilla.redhat.com/1161730
Signed-off-by: Vojtech Szocs <[email protected]>
---
M
frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/Frontend.java
M
frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/FrontendLoginHandler.java
M
frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/FrontendActionTest.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
5 files changed, 88 insertions(+), 67 deletions(-)
git pull ssh://gerrit.ovirt.org:29418/ovirt-engine refs/changes/85/35185/1
diff --git
a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/Frontend.java
b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/Frontend.java
index f99f86c..c0bbcd3 100644
---
a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/Frontend.java
+++
b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/Frontend.java
@@ -825,7 +825,7 @@
result.setCanDoActionMessages((ArrayList<String>)
translateError(result));
callback.getDel().onSuccess(callback.getModel(), result);
if (getLoginHandler() != null && result.getSucceeded()) {
- getLoginHandler().onLoginSuccess(userName, password,
profileName);
+ getLoginHandler().onLoginSuccess();
}
}
diff --git
a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/FrontendLoginHandler.java
b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/FrontendLoginHandler.java
index 1d340f0..64bc091 100644
---
a/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/FrontendLoginHandler.java
+++
b/frontend/webadmin/modules/frontend/src/main/java/org/ovirt/engine/ui/frontend/FrontendLoginHandler.java
@@ -2,7 +2,7 @@
public interface FrontendLoginHandler {
- void onLoginSuccess(String userName, String password, String domain);
+ void onLoginSuccess();
void onLogout();
diff --git
a/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/FrontendActionTest.java
b/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/FrontendActionTest.java
index 9312a18..b0d2326 100644
---
a/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/FrontendActionTest.java
+++
b/frontend/webadmin/modules/frontend/src/test/java/org/ovirt/engine/ui/frontend/FrontendActionTest.java
@@ -811,7 +811,7 @@
returnValue.setSucceeded(true);
callbackAction.getValue().onSuccess(returnValue);
verify(mockAsyncCallback).onSuccess(model, returnValue);
- verify(mockLoginHandler).onLoginSuccess(testUser, testPassword,
testProfile);
+ verify(mockLoginHandler).onLoginSuccess();
verify(mockFrontendFailureEvent, never()).raise(eq(Frontend.class),
(FrontendFailureEventArgs) any());
}
@@ -841,7 +841,7 @@
returnValue.setSucceeded(false); // Yes I know this is the default,
just to be sure.
callbackAction.getValue().onSuccess(returnValue);
verify(mockAsyncCallback).onSuccess(model, returnValue);
- verify(mockLoginHandler, never()).onLoginSuccess(testUser,
testPassword, testProfile);
+ verify(mockLoginHandler, never()).onLoginSuccess();
verify(mockFrontendFailureEvent, never()).raise(eq(Frontend.class),
(FrontendFailureEventArgs) any());
}
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 99603f6..40e9a0b 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
@@ -3,7 +3,6 @@
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.ovirt.engine.ui.common.system.ClientStorage;
import org.ovirt.engine.ui.common.utils.HttpUtils;
import org.ovirt.engine.ui.frontend.Frontend;
import org.ovirt.engine.ui.frontend.communication.StorageCallback;
@@ -29,13 +28,19 @@
* <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.
+ * <b>
+ * Important: acquired (physical) REST API session maps to current user's
(logical) Engine session.
+ * </b>
+ * <p>
+ * This means that lifetime of acquired REST API session corresponds to
lifetime of current user's
+ * Engine session. Once the user logs out, corresponding Engine session will
expire and any unclosed
+ * physical sessions that map to it will become unusable.
* <p>
* Triggers {@link RestApiSessionAcquiredEvent} upon acquiring or reusing REST
API session.
*/
public class RestApiSessionManager {
- private static class RestApiCallback implements RequestCallback {
+ private static class RestApiRequestCallback implements RequestCallback {
@Override
public void onResponseReceived(Request request, Response response) {
@@ -65,22 +70,21 @@
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$
+ private static final String ENGINE_AUTH_TOKEN_HEADER =
"OVIRT-INTERNAL-ENGINE-AUTH-TOKEN"; //$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;
-
+ private String restApiSessionTimeout = DEFAULT_SESSION_TIMEOUT;
private String restApiSessionId;
@Inject
- public RestApiSessionManager(EventBus eventBus, ClientStorage
clientStorage) {
+ public RestApiSessionManager(EventBus eventBus) {
this.eventBus = eventBus;
- this.clientStorage = clientStorage;
+
// Note that the slash at the end of the URL is not just a whim. With
the trailing slash the browser will only
// send authentication headers to URLs ending in api/, otherwise it
will send them to URLs ending in /, and
// this causes problems in other applications, for example in the
reports application.
@@ -88,30 +92,45 @@
}
public void setSessionTimeout(String sessionTimeout) {
- this.sessionTimeout = sessionTimeout;
+ this.restApiSessionTimeout = sessionTimeout;
}
- String getSessionTimeout() {
- return sessionTimeout != null ? sessionTimeout :
DEFAULT_SESSION_TIMEOUT;
+ /**
+ * Build HTTP request to acquire new or keep-alive existing REST API
session.
+ * <p>
+ * The {@code engineAuthToken} is required only when creating new session.
Once the session
+ * is created, {@code Prefer:persistent-auth} ensures that client receives
the JSESSIONID
+ * cookie used to associate any subsequent requests with that session.
+ */
+ RequestBuilder createRequest(String engineAuthToken) {
+ RequestBuilder builder = new RequestBuilder(RequestBuilder.GET,
restApiBaseUrl);
+
+ // Map this (physical) REST API session to current user's (logical)
Engine session
+ if (engineAuthToken != null) {
+ builder.setHeader(ENGINE_AUTH_TOKEN_HEADER, engineAuthToken);
+ }
+
+ // Control REST API session timeout
+ builder.setHeader("Session-TTL", restApiSessionTimeout); //$NON-NLS-1$
+
+ // Express additional preferences for serving this request
+ builder.setHeader("Prefer", "persistent-auth, csrf-protection");
//$NON-NLS-1$ //$NON-NLS-2$
+
+ // Add CSRF token, this is needed due to Prefer:csrf-protection
+ String sessionId = getSessionId();
+ if (sessionId != null) {
+ builder.setHeader(SESSION_ID_HEADER, sessionId);
+ }
+
+ return builder;
}
- void sendRequest(RequestBuilder requestBuilder, RestApiCallback callback) {
+ void sendRequest(RequestBuilder requestBuilder, RestApiRequestCallback
callback) {
try {
requestBuilder.sendRequest(null, callback);
} catch (RequestException e) {
// Request failed to initiate, nothing we can do about it
}
- }
-
- RequestBuilder createRequest() {
- RequestBuilder requestBuilder = new RequestBuilder(RequestBuilder.GET,
restApiBaseUrl);
- requestBuilder.setHeader("Prefer", "persistent-auth,
csrf-protection"); //$NON-NLS-1$ //$NON-NLS-2$
- requestBuilder.setHeader("Session-TTL", getSessionTimeout());
//$NON-NLS-1$
- String sessionId = getSessionId();
- if (sessionId != null) {
- requestBuilder.setHeader(SESSION_ID_HEADER, sessionId);
- }
- return requestBuilder;
}
void scheduleKeepAliveHeartbeat() {
@@ -121,16 +140,10 @@
String sessionId = getSessionId();
if (sessionId != null) {
- // The session is still in use
- RequestBuilder requestBuilder = createRequest();
+ // The browser takes care of sending JSESSIONID cookie for
this request automatically
+ sendRequest(createRequest(null), new
RestApiRequestCallback());
- // 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
+ // The session is still in use, proceed with the heartbeat
return true;
} else {
// The session has been released, cancel the heartbeat
@@ -141,18 +154,14 @@
}
/**
- * Acquires new REST API session using the given credentials.
+ * Acquires new REST API session that maps to current user's Engine
session.
*/
- public void acquireSession(String userNameWithDomain, String password) {
- RequestBuilder requestBuilder = createRequest();
- requestBuilder.setUser(userNameWithDomain);
- requestBuilder.setPassword(password);
-
- sendRequest(requestBuilder, new RestApiCallback() {
+ public void acquireSession(String engineAuthToken) {
+ sendRequest(createRequest(engineAuthToken), new
RestApiRequestCallback() {
@Override
protected void processResponse(Response response) {
- // 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)
+ // Obtain session ID from response header, as we're unable to
access the
+ // JSESSIONID cookie directly (cookie is set for REST API
specific path)
String sessionIdFromHeader = HttpUtils.getHeader(response,
SESSION_ID_HEADER);
if (sessionIdFromHeader != null) {
@@ -165,17 +174,16 @@
}
/**
- * Attempts to reuse existing REST API session that was previously
{@linkplain #acquireSession acquired}.
+ * Attempts to reuse existing REST API session that was previously
acquired.
*/
public void reuseSession() {
- //If reuseSession is called right after setSessionId, then
getSessionId() without the callback will not
- //be null. If it is null then reuseSession was called from an
automatic login (as restApiSessionId is null
- //can we can utilize the async call to retrieve it from the backend.
+ // If reuseSession is called right after setSessionId, then
getSessionId() without the callback will not
+ // be null. If it is null then reuseSession was called from an
automatic login (as restApiSessionId is null
+ // can we can utilize the async call to retrieve it from the backend.
if (getSessionId() != null) {
processSessionId(getSessionId());
} else {
getSessionId(new StorageCallback() {
-
@Override
public void onSuccess(String result) {
if (result != null) {
@@ -190,41 +198,42 @@
public void onFailure(Throwable caught) {
processSessionIdException();
}
-
- private void processSessionIdException() {
- RestApiSessionManager.logger.severe("Engine REST API
session ID is not available"); //$NON-NLS-1$
- }
});
}
}
- private void processSessionId(String sessionId) {
+ void processSessionId(String sessionId) {
RestApiSessionAcquiredEvent.fire(eventBus, sessionId);
scheduleKeepAliveHeartbeat();
}
+ void processSessionIdException() {
+ logger.severe("Engine REST API session ID is not available");
//$NON-NLS-1$
+ }
+
/**
- * Releases REST API session currently in use.
+ * Releases existing REST API session.
+ * <p>
+ * Note that we're not closing (physical) REST API session via HTTP
request since the user
+ * logout operation already triggered (logical) Engine session expiry.
Even if the physical
+ * session is still alive (JSESSIONID cookie still valid), it won't work
when the associated
+ * logical session is dead.
*/
public void releaseSession() {
- clearSessionId();
+ setSessionId(null);
}
String getSessionId() {
return restApiSessionId;
}
- void getSessionId(final StorageCallback callback) {
+ void getSessionId(StorageCallback callback) {
Frontend.getInstance().retrieveFromHttpSession(SESSION_ID_KEY,
callback);
}
void setSessionId(String sessionId) {
Frontend.getInstance().storeInHttpSession(SESSION_ID_KEY, sessionId);
restApiSessionId = sessionId;
- }
-
- void clearSessionId() {
- setSessionId(null);
}
}
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 af7c241..adf2f8d 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
@@ -1,6 +1,9 @@
package org.ovirt.engine.ui.webadmin.system;
import org.ovirt.engine.core.common.mode.ApplicationMode;
+import org.ovirt.engine.core.common.queries.VdcQueryParametersBase;
+import org.ovirt.engine.core.common.queries.VdcQueryReturnValue;
+import org.ovirt.engine.core.common.queries.VdcQueryType;
import org.ovirt.engine.ui.common.auth.AutoLoginData;
import org.ovirt.engine.ui.common.auth.CurrentUser;
import org.ovirt.engine.ui.common.system.BaseApplicationInit;
@@ -9,8 +12,10 @@
import org.ovirt.engine.ui.common.uicommon.FrontendFailureEventListener;
import org.ovirt.engine.ui.common.uicommon.model.CommonModelManager;
import org.ovirt.engine.ui.common.uicommon.model.ModelInitializedEvent;
+import org.ovirt.engine.ui.frontend.AsyncQuery;
import org.ovirt.engine.ui.frontend.Frontend;
import org.ovirt.engine.ui.frontend.FrontendLoginHandler;
+import org.ovirt.engine.ui.frontend.INewAsyncCallback;
import org.ovirt.engine.ui.uicommonweb.ITypeResolver;
import org.ovirt.engine.ui.uicommonweb.ReportInit;
import org.ovirt.engine.ui.uicommonweb.auth.CurrentUserRole;
@@ -112,17 +117,24 @@
super.initFrontend();
ReportInit.getInstance().initHandlers(eventBus);
+
// Configure REST API integration for UI plugin infrastructure
frontend.setLoginHandler(new FrontendLoginHandler() {
@Override
- public void onLoginSuccess(final String userName, final String
password, final String domain) {
+ public void onLoginSuccess() {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
- final String domainToken = "@"; //$NON-NLS-1$
- restApiSessionManager.acquireSession(
- userName.contains(domainToken) ? userName :
userName + domainToken + domain,
- password);
+ frontend.runQuery(VdcQueryType.GetEngineSessionIdToken,
+ new VdcQueryParametersBase(),
+ new AsyncQuery(new INewAsyncCallback() {
+ @Override
+ public void onSuccess(Object model, Object
returnValue) {
+ String engineAuthToken = (String)
((VdcQueryReturnValue) returnValue).getReturnValue();
+
restApiSessionManager.acquireSession(engineAuthToken);
+ }
+ })
+ );
}
});
}
--
To view, visit http://gerrit.ovirt.org/35185
To unsubscribe, visit http://gerrit.ovirt.org/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ic3905b3b5834a0f7327321e93064274df0d1db65
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