diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO_CustomFieldSerializer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO_CustomFieldSerializer.java new file mode 100755 index 00000000000..5775780e914 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO_CustomFieldSerializer.java @@ -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 { + @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()); + } +} \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/settings/UserSettingsPresenter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/settings/UserSettingsPresenter.java index d5c8d505015..dd863a605b1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/settings/UserSettingsPresenter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/shared/places/user/profile/settings/UserSettingsPresenter.java @@ -35,7 +35,7 @@ public class UserSettingsPresenter>() { @Override public void onSuccess(Map result) { @@ -63,7 +63,7 @@ public class UserSettingsPresenter(); 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); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilterPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilterPanel.java index 7c46aa7f192..9404f72ccf1 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilterPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/CompetitorFilterPanel.java @@ -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()); diff --git a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF index 2a5a6d559ef..4c1cd5587c3 100644 --- a/java/com.sap.sse.gwt/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.gwt/META-INF/MANIFEST.MF @@ -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, diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/Storage.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/Storage.java index 33a81a787af..5199d1eda9e 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/Storage.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/Storage.java @@ -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. + *

+ * + * 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.

+ * + * 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) * diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorage.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorage.java index e8f2946ac90..53cc0bf24c4 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorage.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorage.java @@ -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); diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorageEvent.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorageEvent.java index 14fcc05028b..1cc8ebf802f 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorageEvent.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/CrossDomainStorageEvent.java @@ -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 diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/DelegatingCrossDomainStorageFuture.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/DelegatingCrossDomainStorageFuture.java new file mode 100755 index 00000000000..d2b5e32d5d0 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/DelegatingCrossDomainStorageFuture.java @@ -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}. + *

+ * + * 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. + *

+ * + * 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> 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 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 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 queuedRequest : queuedWhileWaitingForStorage) { + queuedRequest.accept(storageToUse); + } + queuedWhileWaitingForStorage.clear(); + } + } + + private void runOrEnqueue(Consumer 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 callback) { + runOrEnqueue(storage->storage.setItem(key, value, callback)); + } + + @Override + public void getItem(String key, Consumer callback) { + runOrEnqueue(storage->storage.getItem(key, callback)); + } + + @Override + public void removeItem(String key, Consumer callback) { + runOrEnqueue(storage->storage.removeItem(key, callback)); + } + + @Override + public void clear(Consumer callback) { + runOrEnqueue(storage->storage.clear(callback)); + } + + @Override + public void key(int index, Consumer callback) { + runOrEnqueue(storage->storage.key(index, callback)); + } + + @Override + public void getLength(Consumer callback) { + runOrEnqueue(storage->storage.getLength(callback)); + } +} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/impl/CrossDomainStorageFallingBackToLocalAfterTimeout.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/impl/CrossDomainStorageFallingBackToLocalAfterTimeout.java index c8338f3c966..a1277a5540d 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/impl/CrossDomainStorageFallingBackToLocalAfterTimeout.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/xdstorage/impl/CrossDomainStorageFallingBackToLocalAfterTimeout.java @@ -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}. - *

- * - * 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> 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 queuedRequest : queuedWhileWaitingForIframe) { - queuedRequest.accept(storageToUse); - } - queuedWhileWaitingForIframe.clear(); - } - } - - private void runOrEnqueue(Consumer 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 callback) { - runOrEnqueue(storage->storage.setItem(key, value, callback)); - } - - @Override - public void getItem(String key, Consumer callback) { - runOrEnqueue(storage->storage.getItem(key, callback)); - } - - @Override - public void removeItem(String key, Consumer callback) { - runOrEnqueue(storage->storage.removeItem(key, callback)); - } - - @Override - public void clear(Consumer callback) { - runOrEnqueue(storage->storage.clear(callback)); - } - - @Override - public void key(int index, Consumer callback) { - runOrEnqueue(storage->storage.key(index, callback)); - } - - @Override - public void getLength(Consumer callback) { - runOrEnqueue(storage->storage.getLength(callback)); - } } diff --git a/java/com.sap.sse.security.ui/META-INF/MANIFEST.MF b/java/com.sap.sse.security.ui/META-INF/MANIFEST.MF index 8c007ed2103..5374aa4e408 100644 --- a/java/com.sap.sse.security.ui/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.security.ui/META-INF/MANIFEST.MF @@ -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, diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UnauthorizedException_CustomFieldSerializer.java b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UnauthorizedException_CustomFieldSerializer.java similarity index 92% rename from java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UnauthorizedException_CustomFieldSerializer.java rename to java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UnauthorizedException_CustomFieldSerializer.java index 77e4f38aa6e..c0be84c90f6 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UnauthorizedException_CustomFieldSerializer.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UnauthorizedException_CustomFieldSerializer.java @@ -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 { @Override diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UserGroupManagementException_CustomFieldSerializer.java b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UserGroupManagementException_CustomFieldSerializer.java similarity index 92% rename from java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UserGroupManagementException_CustomFieldSerializer.java rename to java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UserGroupManagementException_CustomFieldSerializer.java index aeae063dc0f..6cf0cf68932 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UserGroupManagementException_CustomFieldSerializer.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UserGroupManagementException_CustomFieldSerializer.java @@ -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 { @Override diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UserManagementException_CustomFieldSerializer.java b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UserManagementException_CustomFieldSerializer.java similarity index 92% rename from java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UserManagementException_CustomFieldSerializer.java rename to java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UserManagementException_CustomFieldSerializer.java index ba7702ec7c9..44a552f0831 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/shared/UserManagementException_CustomFieldSerializer.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/shared/UserManagementException_CustomFieldSerializer.java @@ -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 { @Override diff --git a/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO_CustomFieldSerializer.java b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO_CustomFieldSerializer.java new file mode 100755 index 00000000000..5775780e914 --- /dev/null +++ b/java/com.sap.sse.security.ui/src/main/java/com/google/gwt/user/client/rpc/core/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO_CustomFieldSerializer.java @@ -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 { + @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()); + } +} \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java index a5a2457986b..b21df5941b5 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementService.java @@ -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(); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java index 155c5491364..25e8e24dc92 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserManagementServiceAsync.java @@ -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 callback); void userGroupExists(String userGroupName, AsyncCallback 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 callback); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java index 05788c3705d..75621b6bf98 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java @@ -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 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() { + @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 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 keys, final AsyncCallback> 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> 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 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 keyValuePairs, final AsyncCallback 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() { @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? } }); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java index bac9132c002..fc736acf2b0 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java @@ -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()); + } } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO.java new file mode 100755 index 00000000000..94280e5efc3 --- /dev/null +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/shared/SecurityServiceSharingDTO.java @@ -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. + *

+ * + * 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. + *

+ * + * 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; + } +} diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java index e2c7d1aa8ae..951e9bab6b7 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityService.java @@ -624,13 +624,25 @@ public interface SecurityService extends ReplicableWithObjectInputStream * * 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"}. + *

+ * + * 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); diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java index 75ae57a24ff..effa7e3bc25 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/impl/SecurityServiceImpl.java @@ -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 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 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; + } }