You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/07/19 08:51:10 UTC
[isis] 01/02: ISIS-2793: rewrites keycloak, draft documentation
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch ISIS-2803-keycloak-rewrite
in repository https://gitbox.apache.org/repos/asf/isis.git
commit 5e72ac4d94b872a480a9e76c4384bfeab8ade326
Author: danhaywood <da...@haywood-associates.co.uk>
AuthorDate: Sat Jul 17 16:45:01 2021 +0100
ISIS-2793: rewrites keycloak, draft documentation
this also has a commented out LoginSuccessHandlerUNUSED that is unused and can probably be removed
---
.../apache/isis/core/config/IsisConfiguration.java | 45 ++++-
.../login/LoginSuccessHandlerUNUSED.java | 36 ++--
mavendeps/webapp/pom.xml | 9 +-
security/keycloak/pom.xml | 34 +++-
.../adoc/modules/keycloak/images/account-mgmt.png | Bin 0 -> 324215 bytes
.../modules/keycloak/images/add-realm-prompt.png | Bin 0 -> 51244 bytes
.../images/add-sven-to-regular-user-role.png | Bin 0 -> 181297 bytes
.../keycloak/images/add-sven-user-prompt.png | Bin 0 -> 127568 bytes
.../modules/keycloak/images/client-app-config.png | Bin 0 -> 192250 bytes
.../adoc/modules/keycloak/images/client-secret.png | Bin 0 -> 101319 bytes
.../keycloak/images/create-regular-user-role.png | Bin 0 -> 54651 bytes
.../keycloak/images/create-simpleapp-client.png | Bin 0 -> 123580 bytes
.../keycloak/images/define-simpleapp-realm.png | Bin 0 -> 60911 bytes
.../modules/keycloak/images/logged-in-as-sven.png | Bin 0 -> 329658 bytes
.../images/login-to-admin-console-prompt.png | Bin 0 -> 54678 bytes
.../keycloak/images/login-to-admin-console.png | Bin 0 -> 100389 bytes
.../modules/keycloak/images/sven-credentials.png | Bin 0 -> 98429 bytes
.../modules/keycloak/images/test-sven-login.png | Bin 0 -> 86866 bytes
.../main/adoc/modules/keycloak/pages/about.adoc | 182 +++++++++++++++++++--
.../keycloak/IsisModuleSecurityKeycloak.java | 127 +++++++++++++-
.../authentication/AuthenticatorKeycloak.java | 61 -------
.../keycloak/handler/LogoutHandlerForKeycloak.java | 60 +++++++
.../services/KeycloakOauth2UserService.java | 103 ++++++++++++
.../keycloak/webmodule/KeycloakFilter.java | 101 ------------
.../keycloak/webmodule/WebModuleKeycloak.java | 73 ---------
.../viewer/IsisModuleViewerWicketViewer.java | 2 +
.../services/ImpersonatedUserHolderForWicket.java | 77 +++++++++
27 files changed, 618 insertions(+), 292 deletions(-)
diff --git a/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java b/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
index aed0f79..63c069b 100644
--- a/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
+++ b/core/config/src/main/java/org/apache/isis/core/config/IsisConfiguration.java
@@ -42,10 +42,6 @@ import javax.validation.Payload;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.validation.annotation.Validated;
-
import org.apache.isis.applib.IsisModuleApplib;
import org.apache.isis.applib.annotation.ActionLayout;
import org.apache.isis.applib.annotation.DomainObject;
@@ -60,7 +56,6 @@ import org.apache.isis.applib.services.userreg.EmailNotificationService;
import org.apache.isis.applib.services.userreg.UserRegistrationService;
import org.apache.isis.applib.services.userui.UserMenu;
import org.apache.isis.commons.internal.context._Context;
-import org.apache.isis.core.config.IsisConfiguration.Viewer;
import org.apache.isis.core.config.metamodel.facets.DefaultViewConfiguration;
import org.apache.isis.core.config.metamodel.facets.EditingObjectsConfiguration;
import org.apache.isis.core.config.metamodel.facets.PublishingPolicies.ActionPublishingPolicy;
@@ -69,13 +64,15 @@ import org.apache.isis.core.config.metamodel.facets.PublishingPolicies.PropertyP
import org.apache.isis.core.config.metamodel.services.ApplicationFeaturesInitConfiguration;
import org.apache.isis.core.config.metamodel.specloader.IntrospectionMode;
import org.apache.isis.core.config.viewer.wicket.DialogMode;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.validation.annotation.Validated;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@@ -137,6 +134,42 @@ public class IsisConfiguration {
}
+ private final Keycloak keycloak = new Keycloak();
+ @Data
+ public static class Keycloak {
+ /**
+ * The name of the realm for the Apache Isis application, as configured in
+ * Keycloak.
+ */
+ private String realm;
+
+ /**
+ * The base URL for the keycloak server.
+ *
+ * <p>
+ * For example, if running a keycloak using Docker container, such as:
+ * <pre>
+ * docker run -p 9090:8080 \
+ * -e KEYCLOAK_USER=admin \
+ * -e KEYCLOAK_PASSWORD=admin \
+ * quay.io/keycloak/keycloak:14.0.0
+ * </pre>,
+ *
+ * then the URL would be "http://localhost:9090/auth".
+ * </p>
+ */
+ private String baseUrl;
+
+ /**
+ * Specifies where users will be redirected after authenticating successfully if they
+ * have not visited a secured page prior to authenticating or {@code alwaysUse} is
+ * true.
+ */
+ private String loginSuccessUrl = "/wicket";
+ }
+
+
+
}
private final Applib applib = new Applib();
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java b/core/security/src/main/java/org/apache/isis/core/security/authentication/login/LoginSuccessHandlerUNUSED.java
similarity index 51%
copy from security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
copy to core/security/src/main/java/org/apache/isis/core/security/authentication/login/LoginSuccessHandlerUNUSED.java
index 213a0c7..c2034eb 100644
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/authentication/login/LoginSuccessHandlerUNUSED.java
@@ -16,32 +16,24 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.isis.security.keycloak;
-
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
-
-import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
-import org.apache.isis.security.keycloak.authentication.AuthenticatorKeycloak;
-import org.apache.isis.security.keycloak.webmodule.WebModuleKeycloak;
-import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
+package org.apache.isis.core.security.authentication.login;
/**
- * Configuration Bean to support Isis Security using Shiro.
+ * To allow login to be completed.
*
- * @since 2.0 {@index}
+ * <p>
+ * Provided as a hook for some of the more sophisticated
+ * {@link org.apache.isis.core.security.authentication.Authenticator}s
+ * (eg keycloak) to synchronise the state of the
+ * {@link org.apache.isis.core.security.authentication.manager.AuthenticationManager}
+ * on successful external login.
+ * </p>
*/
-@Configuration
-@Import({
- // modules
- IsisModuleCoreRuntimeServices.class,
- IsisModuleCoreWebapp.class,
-
- // @Service's
- AuthenticatorKeycloak.class,
- WebModuleKeycloak.class,
+public interface LoginSuccessHandlerUNUSED {
-})
-public class IsisModuleSecurityKeycloak {
+ /**
+ * Indicates a successful login
+ */
+ void onSuccess();
}
diff --git a/mavendeps/webapp/pom.xml b/mavendeps/webapp/pom.xml
index 9d097f3..798ba28 100644
--- a/mavendeps/webapp/pom.xml
+++ b/mavendeps/webapp/pom.xml
@@ -100,10 +100,6 @@
<dependency>
<groupId>org.apache.isis.viewer</groupId>
- <artifactId>isis-viewer-restfulobjects-viewer</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.isis.viewer</groupId>
<artifactId>isis-viewer-restfulobjects-jaxrsresteasy4</artifactId>
</dependency>
@@ -115,10 +111,15 @@
<groupId>org.apache.isis.security</groupId>
<artifactId>isis-security-bypass</artifactId>
</dependency>
+
+ <!--
+ we no longer include isis-security-keycloak in order to reduce the
+ 3rd party dependencies.
<dependency>
<groupId>org.apache.isis.security</groupId>
<artifactId>isis-security-keycloak</artifactId>
</dependency>
+ -->
<!--
we no longer include isis-security-shiro in order to reduce the
diff --git a/security/keycloak/pom.xml b/security/keycloak/pom.xml
index c6cd4fd..1239ed6 100644
--- a/security/keycloak/pom.xml
+++ b/security/keycloak/pom.xml
@@ -7,9 +7,9 @@
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
-
+
http://www.apache.org/licenses/LICENSE-2.0
-
+
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@@ -53,12 +53,12 @@
</dependencyManagement>
<dependencies>
-
+
<dependency>
<groupId>org.apache.isis.core</groupId>
<artifactId>isis-core-runtime</artifactId>
</dependency>
-
+
<dependency>
<groupId>org.apache.isis.core</groupId>
<artifactId>isis-core-runtimeservices</artifactId>
@@ -69,6 +69,32 @@
<artifactId>isis-core-webapp</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.isis.security</groupId>
+ <artifactId>isis-security-spring</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-security</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-oauth2-client</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-oauth2-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.security</groupId>
+ <artifactId>spring-security-oauth2-jose</artifactId>
+ </dependency>
+
+
<!-- TESTING -->
<dependency>
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/account-mgmt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/account-mgmt.png
new file mode 100644
index 0000000..d261f44
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/account-mgmt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/add-realm-prompt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/add-realm-prompt.png
new file mode 100644
index 0000000..d41cfde
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/add-realm-prompt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-to-regular-user-role.png b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-to-regular-user-role.png
new file mode 100644
index 0000000..3e44896
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-to-regular-user-role.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-user-prompt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-user-prompt.png
new file mode 100644
index 0000000..d5022ca
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/add-sven-user-prompt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/client-app-config.png b/security/keycloak/src/main/adoc/modules/keycloak/images/client-app-config.png
new file mode 100644
index 0000000..4e9a445
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/client-app-config.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/client-secret.png b/security/keycloak/src/main/adoc/modules/keycloak/images/client-secret.png
new file mode 100644
index 0000000..d5645c3
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/client-secret.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/create-regular-user-role.png b/security/keycloak/src/main/adoc/modules/keycloak/images/create-regular-user-role.png
new file mode 100644
index 0000000..3cce286
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/create-regular-user-role.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/create-simpleapp-client.png b/security/keycloak/src/main/adoc/modules/keycloak/images/create-simpleapp-client.png
new file mode 100644
index 0000000..8acfb7d
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/create-simpleapp-client.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/define-simpleapp-realm.png b/security/keycloak/src/main/adoc/modules/keycloak/images/define-simpleapp-realm.png
new file mode 100644
index 0000000..e089f38
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/define-simpleapp-realm.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/logged-in-as-sven.png b/security/keycloak/src/main/adoc/modules/keycloak/images/logged-in-as-sven.png
new file mode 100644
index 0000000..39fd92c
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/logged-in-as-sven.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console-prompt.png b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console-prompt.png
new file mode 100644
index 0000000..4006ddf
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console-prompt.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console.png b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console.png
new file mode 100644
index 0000000..f739eaa
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/login-to-admin-console.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/sven-credentials.png b/security/keycloak/src/main/adoc/modules/keycloak/images/sven-credentials.png
new file mode 100644
index 0000000..15b2ad9
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/sven-credentials.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/images/test-sven-login.png b/security/keycloak/src/main/adoc/modules/keycloak/images/test-sven-login.png
new file mode 100644
index 0000000..4e2f47c
Binary files /dev/null and b/security/keycloak/src/main/adoc/modules/keycloak/images/test-sven-login.png differ
diff --git a/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc b/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc
index c6eb1e6..f4dd010 100644
--- a/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc
+++ b/security/keycloak/src/main/adoc/modules/keycloak/pages/about.adoc
@@ -4,15 +4,32 @@
:page-partial:
-This guide describes the configuration of the Keycloak implementation of Apache Isis' `Authenticator and `Authorizor` APIs.
+This guide describes the configuration of the Keycloak implementation of Apache Isis' `Authenticator` API.
+It does _not_ however provide any implementation of xref:refguide:core:index/security/authorization/Authorizor.adoc[Authorizor] SPI.
+You will therefore need to configure an alternative implementation, eg the xref:bypass:about.adoc[Bypass] implementation (to disable authorisation checks completely), or use the xref:secman:about.adoc[SecMan] implementation.
+
+
+== Dependency
+
+In the webapp module of your application, add the following dependency:
+
+[source,xml]
+.pom.xml
+----
+<dependencies>
+ <dependency>
+ <groupId>org.apache.isis.security</groupId>
+ <artifactId>isis-security-keycloak</artifactId>
+ </dependency>
+</dependencies>
+----
-include::docs:mavendeps:partial$setup-and-configure-mavendeps-webapp.adoc[leveloffset=+1]
== Update AppManifest
-In your application's `AppManifest` (top-level Spring `@Configuration` used to bootstrap the app), import the
+In your application's `AppManifest` (top-level Spring `@Configuration` used to bootstrap the app), import the `IsisModuleSecurityKeycloak` module and remove any other `IsisModuleSecurityXxx` modules.
[source,java]
.AppManifest.java
@@ -27,24 +44,163 @@ public class AppManifest {
}
----
-Make sure that no other `IsisModuleSecurityXxx` module is imported.
+Also, as this module provides no implementation of the xref:refguide:core:index/security/authorization/Authorizor.adoc[Authorizor] SPI, instead you will need some an alternative implementation, such as the xref:bypass:about.adoc[Bypass] implementation.
+(Note: this will in effect disable authorisation checks).
+
+[source,java]
+.AppManifest.java
+----
+@Configuration
+@Import({
+ ...
+ IsisModuleSecurityKeycloak.class, // <.>
+ AuthorizorBypass.class, // <.>
+ ...
+})
+public class AppManifest {
+}
+----
+<.> make sure that no other `IsisModuleSecurityXxx` module is imported.
+<.> or some other implementation of `Authorizor`.
+
-== Design
-The module configures a filter that expects Keycloak to set three `X-Auth-Xxx` headers:
-* `X-Auth-Userid` - is used as the username
-* `X-Auth-Roles` - is a comma-separated set of roles.
+[#walk-through]
+== Walk-through
+
+For simplicity, we'll run Keycloak in Docker with an in-memory database.
+Obviously in production you would need a persistent database.
+
+NOTE: These notes were adapted from the tutorial provided on link:https://www.keycloak.org/getting-started/getting-started-docker[keycloak's website].
+
+
+=== Startup keycloak and login as keycloak admin
+
+* Start up keycloak; we'll run on port _9090_:
+
-The `org.apache.isis.viewer.wicket.roles.USER` role -- as required by xref:vw::about.adoc[Web UI (Wicket viewer)] -- is automatically added to this list of roles.
+[source,bash]
+----
+docker run -p 9090:8080 \
+ -e KEYCLOAK_USER=admin \
+ -e KEYCLOAK_PASSWORD=admin \
+ quay.io/keycloak/keycloak:14.0.0
+----
-* `X-Auth-Subject` - is unused
+* login to the Admin console:
++
+image::login-to-admin-console.png[width=300px]
++
+and
++
+image::login-to-admin-console-prompt.png[width=300px]
-The user and roles are accessible programmatically from the xref:refguide:applib:index/services/user/UserMemento.adoc[UserMemento] obtained from xref:refguide:applib:index/services/user/UserService.adoc[UserService] domain service.
+=== Create a realm for simpleapp
-== Walk-through
+WARNING: TODO: clean up these screenshots, make consistent with text.
+
+* create a realm:
++
+image::add-realm-prompt.png[width=250px]
++
+and:
++
+image::define-simpleapp-realm.png[width=400px]
+
+
+=== Create a client
+
+* create the client:
++
+image::create-simpleapp-client.png[width=400px]
+
+* specify _Access Type_ = confidential, and _Valid Redirect URI_ for the client:
++
+image::client-app-config.png[width=400px]
+
+* copy the secret from the "credentials" tab:
++
+image::client-secret.png[width=600px]
+
+=== Create 'regular-user' role in the realm
+
+* create role:
++
+image::create-regular-user-role.png[width=400px]
+
+
+=== Configure the application as a Keycloak client
+
+* the keycloak config:
++
+[source,properties]
+.config/application.properties
+----
+isis.security.keycloak.realm=demo #<.>
+isis.security.keycloak.base-url=http://localhost:9090/auth #<.>
+
+kc.realm-url=${isis.security.keycloak.base-url}/realms/${isis.security.keycloak.realm} #<.>
+
+spring.security.oauth2.client.registration.demo.client-id=app-demo #<.>
+spring.security.oauth2.client.registration.demo.client-name=Demo App
+spring.security.oauth2.client.registration.demo.client-secret=e3f519b4-0272-4261-9912-8b7453ac4ecd #<.>
+
+
+spring.security.oauth2.client.registration.demo.provider=keycloak #<.>
+spring.security.oauth2.client.registration.demo.authorization-grant-type=authorization_code
+spring.security.oauth2.client.registration.demo.scope=openid, profile
+spring.security.oauth2.client.registration.demo.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
+spring.security.oauth2.client.provider.keycloak.authorization-uri=${kc.realm-url}/protocol/openid-connect/auth
+spring.security.oauth2.client.provider.keycloak.jwk-set-uri=${kc.realm-url}/protocol/openid-connect/certs
+spring.security.oauth2.client.provider.keycloak.token-uri=${kc.realm-url}/protocol/openid-connect/token
+spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
+----
+
+<.> as defined in keycloak.
+The registration properties below must specify this property as the `registration` key.
+<.> URL where keycloak is running
+<.> application-defined property, just to reduce the boilerplate below
+<.> must match the client name entered in the admin console
+<.> as taken from the credential tab of the realm
+<.> remaining property values are boilerplate and should not need to change.
++
+IMPORTANT: Make sure though to change the key itself: `spring.security.oauth2.client.registration.xxx` where "xxx" is the name of the realm being registered to Spring Security.
+
+
+
+=== Create sven user in the realm
+
+* add sven user:
++
+image::add-sven-user-prompt.png[width=400px]
+
+* add credentials (password):
++
+image::sven-credentials.png[width=400px]
+
+* check that the account is setup by navigating to link:http://localhost:9090/auth/realms/simpleapp/account/[]:
++
+image::account-mgmt.png[width=800px]
++
+sign-in:
++
+image::test-sven-login.png[width=300px]
+
+* should be logged in ok:
++
+image::logged-in-as-sven.png[width=800px]
+
+* add to 'regular-user' role:
++
+image::add-sven-to-regular-user-role.png[width=800px]
+
+
+
+== Resources:
-WARNING: TODO - show how this fits together.
+* link:https://www.keycloak.org/docs/latest/securing_apps/index.html#_spring_boot_adapter[Keycloak documentation].
+* link:https://www.baeldung.com/spring-boot-keycloak[baeldung article].
+* link:https://dzone.com/articles/secure-spring-boot-application-with-keycloak[Dzone article]
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
index 213a0c7..1cc66e8 100644
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/IsisModuleSecurityKeycloak.java
@@ -18,16 +18,35 @@
*/
package org.apache.isis.security.keycloak;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.context.annotation.Import;
+import java.util.List;
+import org.apache.isis.core.config.IsisConfiguration;
import org.apache.isis.core.runtimeservices.IsisModuleCoreRuntimeServices;
-import org.apache.isis.security.keycloak.authentication.AuthenticatorKeycloak;
-import org.apache.isis.security.keycloak.webmodule.WebModuleKeycloak;
+import org.apache.isis.core.security.authentication.login.LoginSuccessHandlerUNUSED;
import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
+import org.apache.isis.security.keycloak.handler.LogoutHandlerForKeycloak;
+import org.apache.isis.security.keycloak.services.KeycloakOauth2UserService;
+import org.apache.isis.security.spring.IsisModuleSecuritySpring;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
+import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+import static org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
/**
- * Configuration Bean to support Isis Security using Shiro.
+ * Configuration Bean to support Isis Security using Keycloak.
*
* @since 2.0 {@index}
*/
@@ -37,11 +56,103 @@ import org.apache.isis.core.webapp.IsisModuleCoreWebapp;
IsisModuleCoreRuntimeServices.class,
IsisModuleCoreWebapp.class,
- // @Service's
- AuthenticatorKeycloak.class,
- WebModuleKeycloak.class,
+ // services
+ LogoutHandlerForKeycloak.class,
+
+ // builds on top of Spring
+ IsisModuleSecuritySpring.class,
})
+@EnableWebSecurity
public class IsisModuleSecurityKeycloak {
+
+ @Bean
+ public WebSecurityConfigurerAdapter webSecurityConfigurer(
+ final IsisConfiguration isisConfiguration,
+ final KeycloakOauth2UserService keycloakOidcUserService,
+ final List<LoginSuccessHandlerUNUSED> loginSuccessHandlersUNUSED,
+ final List<LogoutHandler> logoutHandlers
+ ) {
+ val realm = isisConfiguration.getSecurity().getKeycloak().getRealm();
+ return new KeycloakWebSecurityConfigurerAdapter(keycloakOidcUserService, logoutHandlers, isisConfiguration
+ );
+ }
+
+// @RequiredArgsConstructor
+// public static class AuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
+//
+// final List<LoginSuccessHandler> loginSuccessHandlers;
+//
+// @Override
+// public void onAuthenticationSuccess(
+// final HttpServletRequest request,
+// final HttpServletResponse response,
+// final Authentication authentication) throws ServletException, IOException {
+// super.onAuthenticationSuccess(request, response, authentication);
+// loginSuccessHandlers.forEach(LoginSuccessHandler::onSuccess);
+// }
+// }
+
+ @Bean
+ KeycloakOauth2UserService keycloakOidcUserService(OAuth2ClientProperties oauth2ClientProperties) {
+
+ // TODO use default JwtDecoder - where to grab?
+ val jwtDecoder = new NimbusJwtDecoderJwkSupport(
+ oauth2ClientProperties.getProvider().get("keycloak").getJwkSetUri());
+
+ val authoritiesMapper = new SimpleAuthorityMapper();
+ authoritiesMapper.setConvertToUpperCase(true);
+
+ return new KeycloakOauth2UserService(jwtDecoder, authoritiesMapper);
+ }
+
+ @RequiredArgsConstructor
+ public static class KeycloakWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
+
+ private final KeycloakOauth2UserService keycloakOidcUserService;
+ private final List<LogoutHandler> logoutHandlers;
+ private final IsisConfiguration isisConfiguration;
+
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+
+ val successUrl = isisConfiguration.getSecurity().getKeycloak().getLoginSuccessUrl();
+ val realm = isisConfiguration.getSecurity().getKeycloak().getRealm();
+
+ val httpSecurityLogoutConfigurer =
+ http
+ .sessionManagement()
+ .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
+ .and()
+
+ .authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+
+ // responsibility to propagate logout to Keycloak is performed by
+ // LogoutHandlerForKeycloak (called by Isis' LogoutMenu, not by Spring)
+ // this is to ensure that Isis can invalidate the http session eagerly and not preserve it in
+ // the SecurityContextPersistenceFilter (which uses http session to do its work)
+ .logout()
+ .logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
+
+ logoutHandlers.forEach(httpSecurityLogoutConfigurer::addLogoutHandler);
+
+ httpSecurityLogoutConfigurer
+ .and()
+
+ // This is the point where OAuth2 login of Spring 5 gets enabled
+ .oauth2Login()
+ .defaultSuccessUrl(successUrl, true)
+// .successHandler(new AuthSuccessHandler(loginSuccessHandlers))
+ .successHandler(new SavedRequestAwareAuthenticationSuccessHandler())
+ .userInfoEndpoint()
+ .oidcUserService(keycloakOidcUserService)
+ .and()
+
+ .loginPage(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/" + realm);
+ }
+ }
}
+
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/authentication/AuthenticatorKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/authentication/AuthenticatorKeycloak.java
deleted file mode 100644
index 1c63166..0000000
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/authentication/AuthenticatorKeycloak.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.isis.security.keycloak.authentication;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Singleton;
-
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.apache.isis.applib.services.iactn.InteractionProvider;
-import org.apache.isis.applib.services.iactnlayer.InteractionContext;
-import org.apache.isis.core.security.authentication.AuthenticationRequest;
-import org.apache.isis.core.security.authentication.Authenticator;
-
-/**
- * @since 2.0 {@index}
- */
-@Service
-@Named("isis.security.AuthenticatorKeycloak")
-@javax.annotation.Priority(PriorityPrecedence.EARLY)
-@Qualifier("Keycloak")
-@Singleton
-public class AuthenticatorKeycloak implements Authenticator {
-
- @Inject private InteractionProvider interactionProvider;
-
- @Override
- public final boolean canAuthenticate(final Class<? extends AuthenticationRequest> authenticationRequestClass) {
- return true;
- }
-
- @Override
- public InteractionContext authenticate(final AuthenticationRequest request, final String code) {
- // HTTP request filters should already have taken care of Authentication creation
- return interactionProvider.currentInteractionContext().orElse(null);
- }
-
- @Override
- public void logout(final InteractionContext session) {
- }
-
-}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/LogoutHandlerForKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/LogoutHandlerForKeycloak.java
new file mode 100644
index 0000000..36513d9
--- /dev/null
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/handler/LogoutHandlerForKeycloak.java
@@ -0,0 +1,60 @@
+package org.apache.isis.security.keycloak.handler;
+
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import org.apache.isis.core.security.authentication.logout.LogoutHandler;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import lombok.val;
+
+/**
+ * Propagates logouts to Keycloak.
+ *
+ * <p>
+ * Necessary because Spring Security 5 (currently) doesn't support
+ * end-session-endpoints.
+ * </p>
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class LogoutHandlerForKeycloak implements LogoutHandler {
+
+ private final RestTemplate restTemplate;
+
+ public LogoutHandlerForKeycloak() {
+ this(new RestTemplate());
+ }
+
+ @Override public void logout() {
+ val authentication = SecurityContextHolder.getContext().getAuthentication();
+ if (authentication != null) {
+ propagateLogoutToKeycloak((OidcUser) authentication.getPrincipal());
+ }
+
+ }
+ private void propagateLogoutToKeycloak(OidcUser user) {
+
+ val endSessionEndpoint = String.format("%s/protocol/openid-connect/logout", user.getIssuer());
+
+ val builder = UriComponentsBuilder
+ .fromUriString(endSessionEndpoint)
+ .queryParam("id_token_hint", user.getIdToken().getTokenValue());
+
+ val logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class);
+ if (logoutResponse.getStatusCode().is2xxSuccessful()) {
+ log.info("Successfully logged out in Keycloak");
+ } else {
+ log.info("Could not propagate logout to Keycloak");
+ }
+ }
+
+ @Override public boolean isHandlingCurrentThread() {
+ return true;
+ }
+}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/services/KeycloakOauth2UserService.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/services/KeycloakOauth2UserService.java
new file mode 100644
index 0000000..fe7e67d
--- /dev/null
+++ b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/services/KeycloakOauth2UserService.java
@@ -0,0 +1,103 @@
+package org.apache.isis.security.keycloak.services;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.util.CollectionUtils;
+
+import lombok.RequiredArgsConstructor;
+import lombok.val;
+
+@RequiredArgsConstructor
+public class KeycloakOauth2UserService extends OidcUserService {
+
+ private final static OAuth2Error INVALID_REQUEST = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
+
+ final JwtDecoder jwtDecoder;
+ final GrantedAuthoritiesMapper authoritiesMapper;
+
+ /**
+ * Augments {@link OidcUserService#loadUser(OidcUserRequest)} to add authorities
+ * provided by Keycloak.
+ * <p>
+ * Needed because {@link OidcUserService#loadUser(OidcUserRequest)} (currently)
+ * does not provide a hook for adding custom authorities from a
+ * {@link OidcUserRequest}.
+ */
+ @Override
+ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
+
+ OidcUser user = super.loadUser(userRequest);
+
+ Set<GrantedAuthority> authorities = new LinkedHashSet<>();
+ authorities.addAll(user.getAuthorities());
+ authorities.addAll(extractKeycloakAuthorities(userRequest));
+
+ return new DefaultOidcUser(authorities, userRequest.getIdToken(), user.getUserInfo(), "preferred_username");
+ }
+
+ /**
+ * Extracts {@link GrantedAuthority GrantedAuthorities} from the AccessToken in
+ * the {@link OidcUserRequest}.
+ *
+ * @param userRequest
+ * @return
+ */
+ private Collection<? extends GrantedAuthority> extractKeycloakAuthorities(OidcUserRequest userRequest) {
+
+ Jwt token = parseJwt(userRequest.getAccessToken().getTokenValue());
+
+ // Would be great if Spring Security would provide something like a pluggable
+ // OidcUserRequestAuthoritiesExtractor interface to hide the junk below...
+
+ @SuppressWarnings("unchecked")
+ val resourceMap = (Map<String, Object>) token.getClaims().get("resource_access");
+ String clientId = userRequest.getClientRegistration().getClientId();
+
+ @SuppressWarnings("unchecked")
+ val clientResource = (Map<String, Map<String, Object>>) resourceMap.get(clientId);
+ if (CollectionUtils.isEmpty(clientResource)) {
+ return Collections.emptyList();
+ }
+
+ @SuppressWarnings("unchecked")
+ List<String> clientRoles = (List<String>) clientResource.get("roles");
+ if (CollectionUtils.isEmpty(clientRoles)) {
+ return Collections.emptyList();
+ }
+
+ Collection<? extends GrantedAuthority> authorities = AuthorityUtils
+ .createAuthorityList(clientRoles.toArray(new String[0]));
+ if (authoritiesMapper == null) {
+ return authorities;
+ }
+
+ return authoritiesMapper.mapAuthorities(authorities);
+ }
+
+ private Jwt parseJwt(String accessTokenValue) {
+ try {
+ // Token is already verified by spring security infrastructure
+ return jwtDecoder.decode(accessTokenValue);
+ } catch (JwtException e) {
+ throw new OAuth2AuthenticationException(INVALID_REQUEST, e);
+ }
+ }
+}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/KeycloakFilter.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/KeycloakFilter.java
deleted file mode 100644
index 83f6f34..0000000
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/KeycloakFilter.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.isis.security.keycloak.webmodule;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Enumeration;
-import java.util.List;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.springframework.beans.factory.annotation.Autowired;
-
-import org.apache.isis.applib.services.iactnlayer.InteractionContext;
-import org.apache.isis.applib.services.iactnlayer.InteractionService;
-import org.apache.isis.applib.services.user.UserMemento;
-import org.apache.isis.applib.services.user.UserMemento.AuthenticationSource;
-
-import lombok.val;
-
-/**
- * @since 2.0 {@index}
- */
-public class KeycloakFilter implements Filter {
-
- @Autowired private InteractionService interactionService;
-
- @Override
- public void doFilter(
- final ServletRequest servletRequest,
- final ServletResponse servletResponse,
- final FilterChain filterChain) throws IOException, ServletException {
-
- final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
- final HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
- final String userid = header(httpServletRequest, "X-Auth-Userid");
- final String rolesHeader = header(httpServletRequest, "X-Auth-Roles");
- final String subjectHeader = header(httpServletRequest, "X-Auth-Subject");
- if(userid == null || rolesHeader == null || subjectHeader == null) {
- httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- return;
- }
- final List<String> roles = toClaims(rolesHeader);
-
- val user = UserMemento.ofNameAndRoleNames(userid, roles.stream())
- .withAuthenticationSource(AuthenticationSource.EXTERNAL)
- .withAuthenticationCode(subjectHeader);
-
- interactionService.run(
- InteractionContext.ofUserWithSystemDefaults(user),
- ()->filterChain.doFilter(servletRequest, servletResponse));
- }
-
- static List<String> toClaims(final String claimsHeader) {
- final List<String> roles = asRoles(claimsHeader);
- roles.add("org.apache.isis.viewer.wicket.roles.USER");
- return roles;
- }
-
- static List<String> asRoles(String claimsHeader) {
- final List<String> roles = new ArrayList<>();
- if(claimsHeader != null) {
- roles.addAll(Arrays.asList(claimsHeader.split(",")));
- }
- return roles;
- }
-
- private String header(final HttpServletRequest httpServletRequest, final String headerName) {
- final Enumeration<String> headerNames = httpServletRequest.getHeaderNames();
- while(headerNames.hasMoreElements()) {
- final String header = headerNames.nextElement();
- if(header.toLowerCase().equals(headerName.toLowerCase())) {
- return httpServletRequest.getHeader(header);
- }
- }
- return null;
- }
-}
diff --git a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/WebModuleKeycloak.java b/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/WebModuleKeycloak.java
deleted file mode 100644
index 760dbd6..0000000
--- a/security/keycloak/src/main/java/org/apache/isis/security/keycloak/webmodule/WebModuleKeycloak.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.isis.security.keycloak.webmodule;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.servlet.ServletContext;
-import javax.servlet.ServletContextListener;
-import javax.servlet.ServletException;
-
-import org.apache.isis.applib.annotation.PriorityPrecedence;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.stereotype.Service;
-
-import org.apache.isis.applib.services.inject.ServiceInjector;
-import org.apache.isis.commons.collections.Can;
-import org.apache.isis.core.webapp.modules.WebModuleAbstract;
-
-import lombok.Getter;
-
-/**
- * WebModule to enable support for Keycloak.
- *
- * @since 2.0 {@index}
- */
-@Service
-@Named("isis.security.WebModuleKeycloak")
-@javax.annotation.Priority(PriorityPrecedence.FIRST + 100)
-@Qualifier("Keycloak")
-public final class WebModuleKeycloak extends WebModuleAbstract {
-
- private static final String KEYCLOAK_FILTER_NAME = "KeycloakFilter";
-
- @Getter
- private final String name = "Keycloak";
-
- @Inject
- public WebModuleKeycloak(ServiceInjector serviceInjector) {
- super(serviceInjector);
- }
-
- @Override
- public Can<ServletContextListener> init(ServletContext ctx) throws ServletException {
-
- registerFilter(ctx, KEYCLOAK_FILTER_NAME, KeycloakFilter.class)
- .ifPresent(filterReg -> {
- filterReg.addMappingForUrlPatterns(
- null,
- false, // filter is forced first
- "/*");
-
- });
-
- return Can.empty(); // registers no listeners
- }
-
-}
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java
index 9997e8f..9d5e4db 100644
--- a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java
+++ b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/IsisModuleViewerWicketViewer.java
@@ -33,6 +33,7 @@ import org.apache.isis.viewer.wicket.viewer.services.BookmarkUiServiceWicket;
import org.apache.isis.viewer.wicket.viewer.services.DeepLinkServiceWicket;
import org.apache.isis.viewer.wicket.viewer.services.HintStoreUsingWicketSession;
import org.apache.isis.viewer.wicket.viewer.services.ImageResourceCacheClassPath;
+import org.apache.isis.viewer.wicket.viewer.services.ImpersonatedUserHolderForWicket;
import org.apache.isis.viewer.wicket.viewer.services.LocaleProviderWicket;
import org.apache.isis.viewer.wicket.viewer.services.TranslationsResolverWicket;
import org.apache.isis.viewer.wicket.viewer.services.WicketViewerSettingsDefault;
@@ -53,6 +54,7 @@ import org.apache.isis.viewer.wicket.viewer.webmodule.WebModuleWicket;
ComponentFactoryRegistryDefault.class,
DeepLinkServiceWicket.class,
ImageResourceCacheClassPath.class,
+ ImpersonatedUserHolderForWicket.class,
LocaleProviderWicket.class,
HintStoreUsingWicketSession.class,
ObjectMementoServiceWicket.class,
diff --git a/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/ImpersonatedUserHolderForWicket.java b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/ImpersonatedUserHolderForWicket.java
new file mode 100644
index 0000000..660758e
--- /dev/null
+++ b/viewers/wicket/viewer/src/main/java/org/apache/isis/viewer/wicket/viewer/services/ImpersonatedUserHolderForWicket.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.isis.viewer.wicket.viewer.services;
+
+import java.util.Optional;
+
+import javax.inject.Named;
+
+import org.apache.wicket.Session;
+import org.springframework.stereotype.Component;
+
+import org.apache.isis.applib.annotation.PriorityPrecedence;
+import org.apache.isis.applib.services.user.ImpersonatedUserHolder;
+import org.apache.isis.applib.services.user.UserMemento;
+
+/**
+ * Implementation that supports impersonation, using the Wicket {@link Session}
+ * to store the value.
+ *
+ * @since 2.0 {@index}
+ */
+@Component
+@Named("isis.webapp.ImpersonatedUserHolderForWicket")
+@javax.annotation.Priority(PriorityPrecedence.MIDPOINT - 100)
+public class ImpersonatedUserHolderForWicket implements ImpersonatedUserHolder {
+
+ private static final String HTTP_SESSION_KEY_IMPERSONATED_USER =
+ ImpersonatedUserHolderForWicket.class.getName() + "#userMemento";
+
+ @Override
+ public boolean supportsImpersonation() {
+ return session().isPresent();
+ }
+
+ @Override
+ public void setUserMemento(final UserMemento userMemento) {
+ session()
+ .ifPresent(session->
+ session.setAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER, userMemento));
+ }
+
+ @Override
+ public Optional<UserMemento> getUserMemento() {
+ return session()
+ .map(session->session.getAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER))
+ .filter(UserMemento.class::isInstance)
+ .map(UserMemento.class::cast);
+ }
+
+ @Override
+ public void clearUserMemento() {
+ session()
+ .ifPresent(session->
+ session.removeAttribute(HTTP_SESSION_KEY_IMPERSONATED_USER));
+ }
+
+ private static Optional<Session> session() {
+ return Optional.ofNullable(Session.get());
+ }
+
+}