bug5048: integrated CrossDomainStorage with UserService; not tested yet...

This commit is contained in:
Axel Uhl committed 2019-11-08 13:10:57 +01:00
1 parent 956709dc77
commit 83ddb13865
22 files changed
+499 -170

No files matched your search

@@ -0,0 +1,48 @@
package com.google.gwt.user.client.rpc.core.com.sap.sse.security.ui.shared;
import com.google.gwt.user.client.rpc.CustomFieldSerializer;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.sap.sse.security.ui.shared.SecurityServiceSharingDTO;
public class SecurityServiceSharingDTO_CustomFieldSerializer extends CustomFieldSerializer<SecurityServiceSharingDTO> {
@Override
public boolean hasCustomInstantiateInstance() {
return true;
}
@Override
public SecurityServiceSharingDTO instantiateInstance(SerializationStreamReader streamReader) throws SerializationException {
return instantiate(streamReader);
}
public static SecurityServiceSharingDTO instantiate(SerializationStreamReader streamReader) throws SerializationException {
final String jSessionIdCookieDomain = streamReader.readString();
final String baseUrlForCrossDomainStorage = streamReader.readString();
return new SecurityServiceSharingDTO(jSessionIdCookieDomain, baseUrlForCrossDomainStorage);
}
public static void deserialize(SerializationStreamReader streamReader, SecurityServiceSharingDTO instance)
throws SerializationException {
// handled by instantiateInstance
}
@Override
public void deserializeInstance(SerializationStreamReader streamReader, SecurityServiceSharingDTO instance)
throws SerializationException {
deserialize(streamReader, instance);
}
@Override
public void serializeInstance(SerializationStreamWriter streamWriter, SecurityServiceSharingDTO instance)
throws SerializationException {
serialize(streamWriter, instance);
}
public static void serialize(SerializationStreamWriter streamWriter, SecurityServiceSharingDTO instance)
throws SerializationException {
streamWriter.writeString(instance.getjSessionIdCookieDomain());
streamWriter.writeString(instance.getBaseUrlForCrossDomainStorage());
}
}
@@ -35,7 +35,7 @@ public class UserSettingsPresenter<C extends ClientFactoryWithDispatch & ErrorAn
@Override
public void loadData() {
if(clientFactory.getAuthenticationManager().getAuthenticationContext().isLoggedIn()) {
if (clientFactory.getAuthenticationManager().getAuthenticationContext().isLoggedIn()) {
clientFactory.getUserService().getAllPreferences(new AsyncCallback<Map<String,String>>() {
@Override
public void onSuccess(Map<String, String> result) {
@@ -63,7 +63,7 @@ public class UserSettingsPresenter<C extends ClientFactoryWithDispatch & ErrorAn
allKeys.addAll(userSettings.keySet());
allKeys.addAll(localSettings.keySet());
currentlyShownEntries.clear();
for(String key : allKeys) {
for (String key : allKeys) {
currentlyShownEntries.add(new UserSettingsEntry(key, userSettings.get(key), localSettings.get(key)));
}
this.updateData();
@@ -74,9 +74,9 @@ public class UserSettingsPresenter<C extends ClientFactoryWithDispatch & ErrorAn
if (Storage.isLocalStorageSupported()) {
localSettings = new HashMap<String, String>();
Storage localStorage = Storage.getLocalStorageIfSupported();
for(int i = 0; i< localStorage.getLength(); i++) {
for (int i = 0; i< localStorage.getLength(); i++) {
String key = localStorage.key(i);
if(key.startsWith(SailingSettingsConstants.USER_SETTINGS_UI)) {
if (key.startsWith(SailingSettingsConstants.USER_SETTINGS_UI)) {
String value = localStorage.getItem(key);
localSettings.put(key, value);
}
@@ -321,11 +321,10 @@ public class CompetitorFilterPanel extends FlowPanel implements KeyUpHandler, Fi
private void storeCompetitorsFilterSets(CompetitorsFilterSets newCompetitorsFilterSets) {
Storage localStorage = Storage.getLocalStorageIfSupported();
if(localStorage != null) {
if (localStorage != null) {
// delete old value
localStorage.removeItem(LOCAL_STORAGE_COMPETITORS_FILTER_SETS_KEY);
// store the competiors filter set
// store the competitors filter set
CompetitorsFilterSetsJsonDeSerializer serializer = new CompetitorsFilterSetsJsonDeSerializer();
JSONObject jsonObject = serializer.serialize(newCompetitorsFilterSets);
localStorage.setItem(LOCAL_STORAGE_COMPETITORS_FILTER_SETS_KEY, jsonObject.toString());
@@ -57,6 +57,7 @@ Export-Package: com.google.gwt.user.client.rpc.core.java.util,
com.sap.sse.gwt.client.shared.settings,
com.sap.sse.gwt.client.suggestion,
com.sap.sse.gwt.client.useragent,
com.sap.sse.gwt.client.xdstorage,
com.sap.sse.gwt.common,
com.sap.sse.gwt.dispatch.client,
com.sap.sse.gwt.dispatch.client.system,
@@ -8,7 +8,23 @@ import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.shared.HandlerRegistration;
/**
* Represents the browser's localStorage and sessionStorage objects and provides eventing support.
* Represents the browser's localStorage and sessionStorage objects and provides eventing support. Other than
* {@link com.google.gwt.storage.client.Storage}, here the {@link #registerAsStorageEventHandler() event registration}
* works across browser tabs and windows, as it should, whereas for the GWT-provided
* {@link com.google.gwt.storage.client.Storage} this only seems to work within a single browser tab for all browsers
* classified as "safari" which includes all Chrome versoins, and therefore is useless for our purposes and defeats the
* specification of this web browser feature.
* <p>
*
* There is a theoretical possibility to override GWT's replacement rules leading to this strange behavior. See
* {@code Storage.gwt.xml}. It replaces the correct native storage eventing implementation by a proprietary one
* which then only works inside the same tab, and the replacement rules include, in addition to ancient browser types,
* all "safari" browsers, causing the problem. Such replacement rules may be overridable in our own .gwt.xml module
* specifications, but in a quick test we were unable to modify the GWT Storage's behavior to suit our needs.<p>
*
* This class is a "drop-in replacement" for {@link com.google.gwt.storage.client.Storage} together with
* {@link StorageEvent} "drop-in replacing" {@link com.google.gwt.storage.client.StorageEvent}, and then have compatible
* methods.
*
* @author Axel Uhl (D043530)
*
@@ -6,6 +6,7 @@ import com.google.gwt.dom.client.Document;
import com.google.gwt.event.shared.HandlerRegistration;
import com.sap.sse.gwt.client.Storage;
import com.sap.sse.gwt.client.xdstorage.impl.CrossDomainStorageFallingBackToLocalAfterTimeout;
import com.sap.sse.gwt.client.xdstorage.impl.LocalStorage;
import com.sap.sse.gwt.client.xdstorage.impl.StorageMessagingEntryPoint;
/**
@@ -37,10 +38,20 @@ public interface CrossDomainStorage {
}
/**
* Like {@link #create(Document, String)}, inserting the {@code iframe} into the {@link Document#get() current document}.
* If {@code baseUrlForStorageMessagingEntryPoint} is {@code null}, a {@link LocalStorage} will be returned.
* Otherwise, the method behaves like {@link #create(Document, String)}, inserts the {@code iframe} into the
* {@link Document#get() current document} and returns a {@link CrossDomainStorageFallingBackToLocalAfterTimeout}
* which only resorts to a {@link LocalStorage} again in case the iframe doesn't load within an appropriate
* timeout starting with the first request.
*/
static CrossDomainStorage create(String baseUrlForStorageMessagingEntryPoint) {
return create(Document.get(), baseUrlForStorageMessagingEntryPoint);
final CrossDomainStorage result;
if (baseUrlForStorageMessagingEntryPoint == null) {
result = new LocalStorage();
} else {
result = create(Document.get(), baseUrlForStorageMessagingEntryPoint);
}
return result;
}
HandlerRegistration addStorageEventHandler(final CrossDomainStorageEvent.Handler handler);
@@ -1,5 +1,7 @@
package com.sap.sse.gwt.client.xdstorage;
import com.sap.sse.gwt.client.Storage;
/**
* A change event sent to a {@link Handler} object that has been {@link Storage#addStorageEventHandler(Handler)
* registered} with the browser's {@link Storage}. It will receive events from both, the session storage and the local
@@ -0,0 +1,186 @@
package com.sap.sse.gwt.client.xdstorage;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.sap.sse.gwt.client.messaging.MessagePort;
import com.sap.sse.gwt.client.xdstorage.CrossDomainStorageEvent.Handler;
import com.sap.sse.gwt.client.xdstorage.impl.CrossDomainStorageImpl;
/**
* Exposes the {@link CrossDomainStorage} interface immediately after object creation, but may start out without an
* actual implementation objects to which all requests are delegated. Until this delegation target becomes available,
* requests are enqueued. They are executed in order once the delegation target is
* {@link #setStorageToUse(CrossDomainStorage) set}.
* <p>
*
* Optionally, a timeout can be set, defining how long to wait for the delegation target to be set after the first
* {@link CrossDomainStorage} method call has been received, together with a {@link Supplier} for a
* {@link CrossDomainStorage} delegation target implementation to default to if the timeout expires. If no such
* timeout is set, requests could potentially remain queued forever.
*
* @author Axel Uhl (d043530)
*
*/
public class DelegatingCrossDomainStorageFuture implements CrossDomainStorage {
/**
* Starts out as {@code null}. It can be set using {@link #setStorageToUse(CrossDomainStorage)} which will then also
* process any requests queued.
* <p>
*
* When a request comes in through any of the {@link CrossDomainStorage} methods and this field is still
* {@code null} and a timeout has been defined, a timer is started with the {@link #timeoutInMillis}
* duration measured in milliseconds, and the request is recorded in the {@link #queuedWhileWaitingForStorage} queue.
* When the timer times out, or the storage to use is provided---whichever comes first---the queued requests
* will be executed with the now decided {@link CrossDomainStorage} which is then also set to this field. In case
* the timer timed out, a default {@link Supplier} will be asked to supply its value which is then stored to
* this field and used from that point forward.
*/
private CrossDomainStorage storageToUse;
/**
* When request methods are called through any of the {@link CrossDomainStorage} interface methods and the
* {@link #portToStorageMessagingEntryPoint} is not ready yet (still {@code null}), requests are queued in this list
* until the moment when the message port is set after the iframe has announced its readiness, or the timer has timed
* out, whichever comes first. Then, the requests are worked down, one after the other, using the {@link #crossDomainStorage}
* through the iframe if it became ready in time, or using the {@link #fallbackLocalStorage} otherwise.
*/
private final List<Consumer<CrossDomainStorage>> queuedWhileWaitingForStorage;
/**
* The timeout timer. Set if the first request is received through any of the {@link CrossDomainStorage} methods and the
* {@link #storageToUse} is not yet set. Cancelled (if not {@code null} and reset to {@code null} when the {@link MessagePort}
* connecting to the {@code iframe} has connected successfully and the {@link #storageToUse} is set to the "real"
* {@link CrossDomainStorageImpl}.
*/
private Timer timer;
/**
* If -1, no timeout applies and requests will remain in the queue until a non-{@code null} delegation target is set using
*/
private final int timeoutInMillis;
private final Supplier<CrossDomainStorage> defaultSupplier;
/**
* Creates an instance with no timeout.
*/
public DelegatingCrossDomainStorageFuture() {
this(/* timeout in millis */ -1, /* default storage provider */ null);
}
/**
* Creates an instance with a specified timeout and a supplier of a {@link CrossDomainStorage} in case the timeout
* expires.
*
* @param timeoutInMillis
* if -1, no timeout logic applies
* @param defaultSupplier
* must be non-{@code null} if {@code timeoutInMillis} is not -1
*/
public DelegatingCrossDomainStorageFuture(final int timeoutInMillis, Supplier<CrossDomainStorage> defaultSupplier) {
this.queuedWhileWaitingForStorage = new LinkedList<>();
if (defaultSupplier == null && timeoutInMillis != -1) {
throw new IllegalArgumentException("If a timeout of "+timeoutInMillis+"ms is provided, a valid default supplier must be provided, too.");
}
this.timeoutInMillis = timeoutInMillis;
this.defaultSupplier = defaultSupplier;
}
public void setStorageToUse(CrossDomainStorage storageToUse) {
if (storageToUse == null) {
throw new NullPointerException("Must set a valid, non-null storage");
}
if (this.storageToUse != null) {
throw new IllegalStateException("The storage can only be set once");
}
this.storageToUse = storageToUse;
if (!queuedWhileWaitingForStorage.isEmpty()) {
GWT.log("now executing "+queuedWhileWaitingForStorage.size()+" queued requests for cross-domain storage");
for (final Consumer<CrossDomainStorage> queuedRequest : queuedWhileWaitingForStorage) {
queuedRequest.accept(storageToUse);
}
queuedWhileWaitingForStorage.clear();
}
}
private void runOrEnqueue(Consumer<CrossDomainStorage> request) {
if (storageToUse != null) {
request.accept(storageToUse);
} else {
queuedWhileWaitingForStorage.add(request);
if (timeoutInMillis != -1) {
ensureTimerIsRunning();
}
}
}
private void ensureTimerIsRunning() {
if (timer == null) {
GWT.log("cross-domain storage iframe not ready yet; waiting "+timeoutInMillis+"ms for message port");
timer = new Timer() {
@Override
public void run() {
timer = null;
if (storageToUse == null) {
GWT.log("cross-domain storage timeout expired, using local storage "
+ Window.Location.getProtocol() + "//" + Window.Location.getHost()
+ " instead of cross-domain storage");
setStorageToUse(defaultSupplier.get());
}
}
};
timer.schedule(timeoutInMillis);
}
}
@Override
public HandlerRegistration addStorageEventHandler(Handler handler) {
final HandlerRegistration[] result = new HandlerRegistration[1];
runOrEnqueue(storage->result[0]=storage.addStorageEventHandler(handler));
// not entirely safe if the addStorageEventHandler call above is really enqueued and the
// HandlerRegistration.removeHandler call occurs before successful connection; the handler removal
// then would fail with result[0] not yet being initialized
return () -> runOrEnqueue(storage->{
if (result[0] != null) {
result[0].removeHandler();
}
});
}
@Override
public void setItem(String key, String value, Consumer<Void> callback) {
runOrEnqueue(storage->storage.setItem(key, value, callback));
}
@Override
public void getItem(String key, Consumer<String> callback) {
runOrEnqueue(storage->storage.getItem(key, callback));
}
@Override
public void removeItem(String key, Consumer<Void> callback) {
runOrEnqueue(storage->storage.removeItem(key, callback));
}
@Override
public void clear(Consumer<Void> callback) {
runOrEnqueue(storage->storage.clear(callback));
}
@Override
public void key(int index, Consumer<String> callback) {
runOrEnqueue(storage->storage.key(index, callback));
}
@Override
public void getLength(Consumer<Integer> callback) {
runOrEnqueue(storage->storage.getLength(callback));
}
}
@@ -1,19 +1,11 @@
package com.sap.sse.gwt.client.xdstorage.impl;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Consumer;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.sap.sse.gwt.client.messaging.MessagePort;
import com.sap.sse.gwt.client.xdstorage.CrossDomainStorage;
import com.sap.sse.gwt.client.xdstorage.CrossDomainStorageEvent.Handler;
import com.sap.sse.gwt.client.xdstorage.DelegatingCrossDomainStorageFuture;
public class CrossDomainStorageFallingBackToLocalAfterTimeout implements CrossDomainStorage {
public class CrossDomainStorageFallingBackToLocalAfterTimeout extends DelegatingCrossDomainStorageFuture {
/**
* The path under which the HTML document running the {@link StorageMessagingEntryPoint} can be loaded.
*/
@@ -26,137 +18,14 @@ public class CrossDomainStorageFallingBackToLocalAfterTimeout implements CrossDo
*/
private static final int TIMEOUT_FOR_IFRAME_TO_RESPOND_IN_MILLIS = 5000;
/**
* Starts out as {@code null}. The constructor makes an effort to create an {@code iframe} element in the body of
* the {@link Document} passed to the {@link #CrossDomainStorageFallingBackToLocalAfterTimeout(Document, String)
* constructor}. Once the {@code iframe} has been established and called back to its parent window, the
* {@link MessagePort} used for communicating with that {@code iframe} is passed to a consumer callback which sets
* this field to a {@link CrossDomainStorageImpl} object using that {@link MessagePort}.
* <p>
*
* When a request comes in through any of the {@link CrossDomainStorage} methods and this field is still
* {@code null}, a timer is started with the {@link #TIMEOUT_FOR_IFRAME_TO_RESPOND_IN_MILLIS} duration, and the request is
* recorded in the {@link #queuedWhileWaitingForIframe} queue. When the timer times out, or the {@link MessagePort}
* is established---whichever comes first---the queued requests will be executed with the now decided
* {@link CrossDomainStorage} which is then also set to this field. In case the timer timed out, this will be
* a {@link LocalStorage} which does not use any {@link MessagePort} but maps all requests to the local storage
* under the local origin.
*/
private CrossDomainStorage storageToUse;
/**
* When request methods are called through any of the {@link CrossDomainStorage} interface methods and the
* {@link #portToStorageMessagingEntryPoint} is not ready yet (still {@code null}), requests are queued in this list
* until the moment when the message port is set after the iframe has announced its readiness, or the timer has timed
* out, whichever comes first. Then, the requests are worked down, one after the other, using the {@link #crossDomainStorage}
* through the iframe if it became ready in time, or using the {@link #fallbackLocalStorage} otherwise.
*/
private final List<Consumer<CrossDomainStorage>> queuedWhileWaitingForIframe;
/**
* The timeout timer. Set if the first request is received through any of the {@link CrossDomainStorage} methods and the
* {@link #storageToUse} is not yet set. Cancelled (if not {@code null} and reset to {@code null} when the {@link MessagePort}
* connecting to the {@code iframe} has connected successfully and the {@link #storageToUse} is set to the "real"
* {@link CrossDomainStorageImpl}.
*/
private Timer timer;
public CrossDomainStorageFallingBackToLocalAfterTimeout(Document documentInWhichToInsertMessagingIframe,
String baseUrlForStorageMessagingEntryPoint) {
this.queuedWhileWaitingForIframe = new LinkedList<>();
super(TIMEOUT_FOR_IFRAME_TO_RESPOND_IN_MILLIS, ()->new LocalStorage());
MessagePort.createInDocument(documentInWhichToInsertMessagingIframe,
baseUrlForStorageMessagingEntryPoint+(baseUrlForStorageMessagingEntryPoint.endsWith("/")?"":"/")+STORAGE_MESSAGING_ENTRY_POINT_PATH,
result->{
GWT.log("connection to cross-domain storage at "+baseUrlForStorageMessagingEntryPoint+" established");
if (timer != null) {
GWT.log("cancelling cross-domain storage timout listener that would have resorted to local storage");
timer.cancel();
timer = null;
}
setStorageToUseAndWorkDownQueue(new CrossDomainStorageImpl(result, baseUrlForStorageMessagingEntryPoint));
setStorageToUse(new CrossDomainStorageImpl(result, baseUrlForStorageMessagingEntryPoint));
});
}
private void setStorageToUseAndWorkDownQueue(CrossDomainStorage storageToUse) {
this.storageToUse = storageToUse;
if (!queuedWhileWaitingForIframe.isEmpty()) {
GWT.log("now executing "+queuedWhileWaitingForIframe.size()+" queued requests for cross-domain storage");
for (final Consumer<CrossDomainStorage> queuedRequest : queuedWhileWaitingForIframe) {
queuedRequest.accept(storageToUse);
}
queuedWhileWaitingForIframe.clear();
}
}
private void runOrEnqueue(Consumer<CrossDomainStorage> request) {
if (storageToUse != null) {
request.accept(storageToUse);
} else {
queuedWhileWaitingForIframe.add(request);
ensureTimerIsRunning();
}
}
private void ensureTimerIsRunning() {
if (timer == null) {
GWT.log("cross-domain storage iframe not ready yet; waiting "+TIMEOUT_FOR_IFRAME_TO_RESPOND_IN_MILLIS+" for message port");
timer = new Timer() {
@Override
public void run() {
timer = null;
if (storageToUse == null) {
GWT.log("cross-domain storage timeout expired, using local storage "
+ Window.Location.getProtocol() + "//" + Window.Location.getHost()
+ " instead of cross-domain storage");
setStorageToUseAndWorkDownQueue(new LocalStorage());
}
}
};
timer.schedule(TIMEOUT_FOR_IFRAME_TO_RESPOND_IN_MILLIS);
}
}
@Override
public HandlerRegistration addStorageEventHandler(Handler handler) {
final HandlerRegistration[] result = new HandlerRegistration[1];
runOrEnqueue(storage->result[0]=storage.addStorageEventHandler(handler));
// not entirely safe if the addStorageEventHandler call above is really enqueued and the
// HandlerRegistration.removeHandler call occurs before successful connection; the handler removal
// then would fail with result[0] not yet being initialized
return () -> runOrEnqueue(storage->{
if (result[0] != null) {
result[0].removeHandler();
}
});
}
@Override
public void setItem(String key, String value, Consumer<Void> callback) {
runOrEnqueue(storage->storage.setItem(key, value, callback));
}
@Override
public void getItem(String key, Consumer<String> callback) {
runOrEnqueue(storage->storage.getItem(key, callback));
}
@Override
public void removeItem(String key, Consumer<Void> callback) {
runOrEnqueue(storage->storage.removeItem(key, callback));
}
@Override
public void clear(Consumer<Void> callback) {
runOrEnqueue(storage->storage.clear(callback));
}
@Override
public void key(int index, Consumer<String> callback) {
runOrEnqueue(storage->storage.key(index, callback));
}
@Override
public void getLength(Consumer<Integer> callback) {
runOrEnqueue(storage->storage.getLength(callback));
}
}
@@ -25,8 +25,7 @@ Require-Bundle: javax.servlet;bundle-version="3.1.0",
com.sap.sse.security.common,
com.sap.sse.mail
Web-ContextPath: /security/ui
Export-Package: com.sap.sse.security.shared,
com.sap.sse.security.ui.authentication,
Export-Package: com.sap.sse.security.ui.authentication,
com.sap.sse.security.ui.authentication.app,
com.sap.sse.security.ui.authentication.confirm,
com.sap.sse.security.ui.authentication.create,
@@ -1,9 +1,10 @@
package com.sap.sse.security.shared;
package com.google.gwt.user.client.rpc.core.com.sap.sse.security.shared;
import com.google.gwt.user.client.rpc.CustomFieldSerializer;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.sap.sse.security.shared.UnauthorizedException;
public class UnauthorizedException_CustomFieldSerializer extends CustomFieldSerializer<UnauthorizedException> {
@Override
@@ -1,9 +1,10 @@
package com.sap.sse.security.shared;
package com.google.gwt.user.client.rpc.core.com.sap.sse.security.shared;
import com.google.gwt.user.client.rpc.CustomFieldSerializer;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.sap.sse.security.shared.UserGroupManagementException;
public class UserGroupManagementException_CustomFieldSerializer extends CustomFieldSerializer<UserGroupManagementException> {
@Override
@@ -1,9 +1,10 @@
package com.sap.sse.security.shared;
package com.google.gwt.user.client.rpc.core.com.sap.sse.security.shared;
import com.google.gwt.user.client.rpc.CustomFieldSerializer;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.sap.sse.security.shared.UserManagementException;
public class UserManagementException_CustomFieldSerializer extends CustomFieldSerializer<UserManagementException> {
@Override
@@ -0,0 +1,48 @@
package com.google.gwt.user.client.rpc.core.com.sap.sse.security.ui.shared;
import com.google.gwt.user.client.rpc.CustomFieldSerializer;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.sap.sse.security.ui.shared.SecurityServiceSharingDTO;
public class SecurityServiceSharingDTO_CustomFieldSerializer extends CustomFieldSerializer<SecurityServiceSharingDTO> {
@Override
public boolean hasCustomInstantiateInstance() {
return true;
}
@Override
public SecurityServiceSharingDTO instantiateInstance(SerializationStreamReader streamReader) throws SerializationException {
return instantiate(streamReader);
}
public static SecurityServiceSharingDTO instantiate(SerializationStreamReader streamReader) throws SerializationException {
final String jSessionIdCookieDomain = streamReader.readString();
final String baseUrlForCrossDomainStorage = streamReader.readString();
return new SecurityServiceSharingDTO(jSessionIdCookieDomain, baseUrlForCrossDomainStorage);
}
public static void deserialize(SerializationStreamReader streamReader, SecurityServiceSharingDTO instance)
throws SerializationException {
// handled by instantiateInstance
}
@Override
public void deserializeInstance(SerializationStreamReader streamReader, SecurityServiceSharingDTO instance)
throws SerializationException {
deserialize(streamReader, instance);
}
@Override
public void serializeInstance(SerializationStreamWriter streamWriter, SecurityServiceSharingDTO instance)
throws SerializationException {
serialize(streamWriter, instance);
}
public static void serialize(SerializationStreamWriter streamWriter, SecurityServiceSharingDTO instance)
throws SerializationException {
streamWriter.writeString(instance.getjSessionIdCookieDomain());
streamWriter.writeString(instance.getBaseUrlForCrossDomainStorage());
}
}
@@ -29,6 +29,7 @@ import com.sap.sse.security.shared.dto.UserGroupDTO;
import com.sap.sse.security.shared.dto.WildcardPermissionWithSecurityDTO;
import com.sap.sse.security.ui.oauth.client.CredentialDTO;
import com.sap.sse.security.ui.oauth.shared.OAuthException;
import com.sap.sse.security.ui.shared.SecurityServiceSharingDTO;
import com.sap.sse.security.ui.shared.SuccessInfo;
public interface UserManagementService extends RemoteService {
@@ -218,4 +219,6 @@ public interface UserManagementService extends RemoteService {
throws UserManagementException, org.apache.shiro.authz.UnauthorizedException;
Boolean userGroupExists(String userGroupName) throws org.apache.shiro.authz.UnauthorizedException;
SecurityServiceSharingDTO getSharingConfiguration();
}
@@ -24,6 +24,7 @@ import com.sap.sse.security.shared.dto.UserDTO;
import com.sap.sse.security.shared.dto.UserGroupDTO;
import com.sap.sse.security.shared.dto.WildcardPermissionWithSecurityDTO;
import com.sap.sse.security.ui.oauth.client.CredentialDTO;
import com.sap.sse.security.ui.shared.SecurityServiceSharingDTO;
import com.sap.sse.security.ui.shared.SuccessInfo;
public interface UserManagementServiceAsync {
@@ -193,4 +194,10 @@ public interface UserManagementServiceAsync {
void getRolesAndPermissionsForUser(String username, AsyncCallback<RolesAndPermissionsForUserDTO> callback);
void userGroupExists(String userGroupName, AsyncCallback<Boolean> callback);
/**
* Provides information about whether and how the security service to which this RPC service talks is shared
* across a server landscape with different domain/sub-domain constellations.
*/
void getSharingConfiguration(AsyncCallback<SecurityServiceSharingDTO> callback);
}
@@ -4,11 +4,11 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.logging.Logger;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Random;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.sap.sse.common.Duration;
import com.sap.sse.common.TimePoint;
@@ -24,6 +24,8 @@ import com.sap.sse.gwt.client.Storage;
import com.sap.sse.gwt.client.StorageEvent;
import com.sap.sse.gwt.client.StorageEvent.Handler;
import com.sap.sse.gwt.client.async.MarkedAsyncCallback;
import com.sap.sse.gwt.client.xdstorage.CrossDomainStorage;
import com.sap.sse.gwt.client.xdstorage.DelegatingCrossDomainStorageFuture;
import com.sap.sse.security.shared.HasPermissions;
import com.sap.sse.security.shared.HasPermissions.Action;
import com.sap.sse.security.shared.HasPermissions.DefaultActions;
@@ -39,6 +41,7 @@ import com.sap.sse.security.shared.impl.SecuredSecurityTypes;
import com.sap.sse.security.shared.impl.SecuredSecurityTypes.ServerActions;
import com.sap.sse.security.ui.client.i18n.StringMessages;
import com.sap.sse.security.ui.oauth.client.util.ClientUtils;
import com.sap.sse.security.ui.shared.SecurityServiceSharingDTO;
import com.sap.sse.security.ui.shared.SuccessInfo;
/**
@@ -69,7 +72,6 @@ public class UserService {
*/
private static final String LOCAL_STORAGE_UPDATE_KEY = "current-user-has-changed";
/**
* Storage key to remember when a user was authenticated or dismissed the login hint the last time.
*/
@@ -97,17 +99,53 @@ public class UserService {
private ServerInfoDTO serverInfo;
private final Set<HasPermissions> allKnownHasPermissions;
/**
* The storage configured based on what {@link UserManagementServiceAsync#getSharingConfiguration(AsyncCallback)} has returned.
*/
private final DelegatingCrossDomainStorageFuture crossDomainStorage;
public UserService(UserManagementServiceAsync userManagementService) {
this.id = ""+(System.currentTimeMillis() * Random.nextInt()); // something pretty random
this.id = UUID.randomUUID().toString();
this.userManagementService = userManagementService;
handlers = new HashSet<>();
allKnownHasPermissions = new HashSet<>();
crossDomainStorage = new DelegatingCrossDomainStorageFuture();
initializeCrossDomainStorage();
Util.addAll(SecuredSecurityTypes.getAllInstances(), allKnownHasPermissions);
registerStorageEventHandler();
updateUser(/* notifyOtherInstances */ false);
}
private void initializeCrossDomainStorage() {
assert userManagementService != null;
assert crossDomainStorage != null;
userManagementService.getSharingConfiguration(new AsyncCallback<SecurityServiceSharingDTO>() {
@Override
public void onFailure(Throwable caught) {
Notification.notify(caught.getMessage(), NotificationType.ERROR);
}
@Override
public void onSuccess(SecurityServiceSharingDTO result) {
crossDomainStorage.setStorageToUse(CrossDomainStorage.create(result.getBaseUrlForCrossDomainStorage()));
}
});
}
/**
* Use this instead of {@link Storage#getLocalStorageIfSupported()}. The resulting {@link CrossDomainStorage} object
* is configured by the server settings for sharing or isolation of the security service. Note that the methods all
* work asynchronously, talking to a callback that the caller needs to pass. It can also happen that first requests
* initially have to wait a few milliseconds because the configuration needs to be loaded from the back-end first
* (which happens immediately when this {@link UserService} is constructed), and if a shared storage is configured,
* an {@code iframe} needs to be loaded, and request processing is delayed until the loading of that {@code iframe}
* has completed.
*/
public CrossDomainStorage getStorage() {
return crossDomainStorage;
}
private void registerStorageEventHandler() {
Storage.addStorageEventHandler(new Handler() {
@Override
@@ -295,18 +333,18 @@ public class UserService {
*/
public void getPreference(String key,
final AsyncCallback<String> callback) {
String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side
String username = getCurrentUser().getName();
getUserManagementService().getPreference(username, key, callback);
}
public void getPreferences(List<String> keys,
final AsyncCallback<Map<String, String>> callback) {
String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side
String username = getCurrentUser().getName();
getUserManagementService().getPreferences(username, keys, callback);
}
public void getAllPreferences(final AsyncCallback<Map<String, String>> callback) {
String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side
String username = getCurrentUser().getName();
getUserManagementService().getAllPreferences(username, callback);
}
@@ -319,13 +357,13 @@ public class UserService {
* Serialized settings as {@link String} containing the preferences
*/
public void setPreference(String key, String serializedSettings, final AsyncCallback<Void> callback) {
String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side
String username = getCurrentUser().getName();
getUserManagementService().setPreference(username, key, serializedSettings, callback);
}
public void setPreferences(Map<String, String> keyValuePairs,
final AsyncCallback<Void> callback) {
String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side
String username = getCurrentUser().getName();
getUserManagementService().setPreferences(username, keyValuePairs, callback);
}
@@ -338,16 +376,15 @@ public class UserService {
* @see AbstractGenericSerializableSettings
*/
public void unsetPreference(String key) {
String username = getCurrentUser().getName(); // TODO: Can username be determined via session on server-side
String username = getCurrentUser().getName();
getUserManagementService().unsetPreference(username, key, new AsyncCallback<Void>() {
@Override
public void onFailure(Throwable caught) {
// TODO What to do in case of failure?
Notification.notify(caught.getMessage(), NotificationType.ERROR);
}
@Override
public void onSuccess(Void result) {
// TODO Do anything in case of success?
}
});
}
@@ -67,6 +67,7 @@ import com.sap.sse.security.shared.impl.UserGroupImpl;
import com.sap.sse.security.ui.client.UserManagementService;
import com.sap.sse.security.ui.oauth.client.CredentialDTO;
import com.sap.sse.security.ui.oauth.shared.OAuthException;
import com.sap.sse.security.ui.shared.SecurityServiceSharingDTO;
import com.sap.sse.security.ui.shared.SuccessInfo;
public class UserManagementServiceImpl extends RemoteServiceServlet implements UserManagementService {
@@ -1152,4 +1153,10 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U
.getStringPermissionForTypeRelativeIdentifier(DefaultActions.READ, typeRelativeObjectIdentifier));
});
}
@Override
public SecurityServiceSharingDTO getSharingConfiguration() {
return new SecurityServiceSharingDTO(getSecurityService().getSharedAcrossSubdomainsOf(),
getSecurityService().getBaseUrlForCrossDomainStorage());
}
}
@@ -0,0 +1,46 @@
package com.sap.sse.security.ui.shared;
import java.io.Serializable;
import com.sap.sse.gwt.client.xdstorage.CrossDomainStorage;
import com.sap.sse.security.ui.client.UserManagementServiceAsync;
/**
* A security service ({@code SecurityService}) can be operated in isolated or shared mode. When in shared mode, the
* Shiro configuration will deliver session cookies with a shared parent domain so that it will be used regardless the
* sub-domain that the user is currently navigating in. In isolated mode the session cookie's domain will be specific to
* the server URL used to access the application.
* <p>
*
* Likewise, the {@link CrossDomainStorage} needs to be configured differently, depending on whether the service
* operates in isolated or shared mode. When in isolated mode, the "origin" used to identify the application's local and
* session storage is the URL used to access the application. When in shared mode, different sub-domains would lead to
* different local storages, so instead a common origin from which the {@code /gwt-base/StorageMessaging.html} entry
* point is available is used to manage the storage. See {@link CrossDomainStorage} for details.
* <p>
*
* This data transfer object describes the configuration provided by the server, available from the
* {@link UserManagementServiceAsync#getSharingConfiguration(com.google.gwt.user.client.rpc.AsyncCallback)} method.
*
* @author Axel Uhl (d043530)
*
*/
public class SecurityServiceSharingDTO implements Serializable {
private static final long serialVersionUID = 264755515738862492L;
private final String jSessionIdCookieDomain;
private final String baseUrlForCrossDomainStorage;
public SecurityServiceSharingDTO(String jSessionIdCookieDomain, String baseUrlForCrossDomainStorage) {
super();
this.jSessionIdCookieDomain = jSessionIdCookieDomain;
this.baseUrlForCrossDomainStorage = baseUrlForCrossDomainStorage;
}
public String getjSessionIdCookieDomain() {
return jSessionIdCookieDomain;
}
public String getBaseUrlForCrossDomainStorage() {
return baseUrlForCrossDomainStorage;
}
}
@@ -624,13 +624,25 @@ public interface SecurityService extends ReplicableWithObjectInputStream<Replica
/**
* In case this security service is shared across multiple replica sets that are published under different
* subdomains, session cookies and the local store shall be arranged such that sessions and content are identified
* regardless the subdomain used. For example, if there are two events, A, and B, published through
* {@code a.sapsailing.com} and {@code b.sapsailing.com}, respectively, by default the full subdomain name will be
* sub-domains, session cookies and the local store shall be arranged such that sessions and content are identified
* regardless the sub-domain used. For example, if there are two events, A, and B, published through
* {@code a.sapsailing.com} and {@code b.sapsailing.com}, respectively, by default the full sub-domain name will be
* used for the {@code JSESSIONID} cookie as well as for the identification of content in the browser's local store.
*
* @return
*/
String getSharedAcrossSubdomainsOf();
/**
* In the browser client, a {@code CrossDomainStorage} implementation is used to either run requests for a local
* browser storage against the application's local storage under the URL/origin used to access the application, or a
* shared local storage across several sub-domains may be configured so that data stored while in the context of one
* sub-domain is also available to the application when loaded from a different sub-domain.
*
* @return the base URL where the {@code /gwt-base/StorageMessaging.html} entry point can be found; if {@code null},
* clients should configure their {@code CrossDomainStorage} such that the application's origin is used for
* isolated local storage; otherwise, the result should be used to get a {@code CrossDomainStorage}
* implementation that accesses a local store shared across all application instances that use the same base
* URL for cross-domain storage.
*/
String getBaseUrlForCrossDomainStorage();
}
@@ -72,10 +72,26 @@ public class Activator implements BundleActivator {
* <p>
*
* The value can be specified by a system property whose name is provided by
* {@link #SHARED_ACROSS_SUBDOMAINS_OF_PROPERTY_NAME}.
* {@link #SHARED_ACROSS_SUBDOMAINS_OF_PROPERTY_NAME}. If a non-{@code null} value is provided for
* {@link #baseUrlForCrossDomainStorage} then a non-{@code null} value should be provided here, too.
*/
private final String sharedAcrossSubdomainsOf;
public static final String BASE_URL_FOR_CROSS_DOMAIN_STORAGE_PROPERTY_NAME = "security.baseUrlForCrossDomainStorage";
/**
* Should be {@code null} if the security service instantiated by the bundle activated by this activator is not
* shared across different sub-domains; otherwise, the base URL where {@code /gwt-base/StorageMessaging.html} can be
* loaded from shall be provided, and this base URL will then form the "origin" for the local browser storage used
* for session storage and other local data stored in the browser which will then be available to all servers using
* the same setting fo rthis field. Example value: {@code "https://www.sapsailing.com"}.
* <p>
*
* The value can be specified by a system property whose name is provided by
* {@link #BASE_URL_FOR_CROSS_DOMAIN_STORAGE_PROPERTY_NAME}. If a non-{@code null} value is provided for
* {@link #sharedAcrossSubdomainsOf} then a non-{@code null} value should be provided here, too.
*/
private final String baseUrlForCrossDomainStorage;
public static void setTestStores(UserStore theTestUserStore, AccessControlStore theTestAccessControlStore) {
testUserStore = theTestUserStore;
testAccessControlStore = theTestAccessControlStore;
@@ -101,6 +117,13 @@ public class Activator implements BundleActivator {
public Activator() {
sharedAcrossSubdomainsOf = System.getProperty(SHARED_ACROSS_SUBDOMAINS_OF_PROPERTY_NAME);
baseUrlForCrossDomainStorage = System.getProperty(BASE_URL_FOR_CROSS_DOMAIN_STORAGE_PROPERTY_NAME);
if ((sharedAcrossSubdomainsOf == null) != (baseUrlForCrossDomainStorage == null)) {
logger.warning("This looks like an inconsistent configuration. The security service uses "
+ sharedAcrossSubdomainsOf + " as shared cookie domain but " + baseUrlForCrossDomainStorage
+ " as cross-domain storage origin. " + " Either leave both null or set both consistently, e.g., "
+ "to \"sapsailing.com\" and \"https://www.sapsailing.com\", respectively");
}
}
/**
@@ -150,7 +173,7 @@ public class Activator implements BundleActivator {
hasPermissionsProviderTracker.open();
SecurityServiceImpl initialSecurityService = new SecurityServiceImpl(
ServiceTrackerFactory.createAndOpen(context, MailService.class), userStore, accessControlStore,
new OSGIHasPermissionsProvider(hasPermissionsProviderTracker), sharedAcrossSubdomainsOf);
new OSGIHasPermissionsProvider(hasPermissionsProviderTracker), sharedAcrossSubdomainsOf, baseUrlForCrossDomainStorage);
initialSecurityService.initialize();
securityService.complete(initialSecurityService);
registration = context.registerService(SecurityService.class.getName(), initialSecurityService, null);
@@ -188,6 +188,8 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
private String sharedAcrossSubdomainsOf;
private String baseUrlForCrossDomainStorage;
static {
shiroConfiguration = new Ini();
shiroConfiguration.loadFromPath("classpath:shiro.ini");
@@ -219,7 +221,8 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
*/
public SecurityServiceImpl(ServiceTracker<MailService, MailService> mailServiceTracker, UserStore userStore,
AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider) {
this(mailServiceTracker, userStore, accessControlStore, hasPermissionsProvider, /* sharedAcrossSubdomainsOf */ null);
this(mailServiceTracker, userStore, accessControlStore, hasPermissionsProvider,
/* sharedAcrossSubdomainsOf */ null, /* baseUrlForCrossDomainStorage */ null);
}
/**
@@ -229,10 +232,12 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
* be shared as well.
*/
public SecurityServiceImpl(ServiceTracker<MailService, MailService> mailServiceTracker, UserStore userStore,
AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, String sharedAcrossSubdomainsOf) {
AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider,
String sharedAcrossSubdomainsOf, String baseUrlForCrossDomainStorage) {
logger.info("Initializing Security Service with user store " + userStore);
operationsSentToMasterForReplication = new HashSet<>();
this.sharedAcrossSubdomainsOf = sharedAcrossSubdomainsOf;
this.baseUrlForCrossDomainStorage = baseUrlForCrossDomainStorage;
this.operationExecutionListeners = new ConcurrentHashMap<>();
this.store = userStore;
this.accessControlStore = accessControlStore;
@@ -2086,6 +2091,7 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
}
logger.info("Reading isSharedAcrossSubdomains...");
sharedAcrossSubdomainsOf = (String) is.readObject();
baseUrlForCrossDomainStorage = (String) is.readObject();
logger.info("...as "+sharedAcrossSubdomainsOf);
logger.info("Done filling SecurityService");
}
@@ -2096,6 +2102,7 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
objectOutputStream.writeObject(store);
objectOutputStream.writeObject(accessControlStore);
objectOutputStream.writeObject(sharedAcrossSubdomainsOf);
objectOutputStream.writeObject(baseUrlForCrossDomainStorage);
}
@Override
@@ -2459,4 +2466,9 @@ public class SecurityServiceImpl implements ReplicableSecurityService, ClearStat
public String getSharedAcrossSubdomainsOf() {
return sharedAcrossSubdomainsOf;
}
@Override
public String getBaseUrlForCrossDomainStorage() {
return baseUrlForCrossDomainStorage;
}
}