diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContext.java b/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContext.java
index 1d75cb79b9c..c7d30f90f6b 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContext.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContext.java
@@ -28,10 +28,14 @@
import org.apache.isis.applib.services.iactn.Interaction;
import org.apache.isis.applib.services.user.UserMemento;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
import lombok.Builder;
+import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
+import lombok.ToString;
import lombok.With;
/**
@@ -39,7 +43,12 @@
*
* @since 2.0 {@index}
*/
-@lombok.Value @Builder
+@Getter
+@lombok.experimental.FieldDefaults(makeFinal=false, level= AccessLevel.PRIVATE)
+@AllArgsConstructor
+@ToString
+@EqualsAndHashCode
+@Builder
@RequiredArgsConstructor
public class InteractionContext implements Serializable {
@@ -66,11 +75,12 @@ public static InteractionContext ofUserWithSystemDefaults(
/**
* The (programmatically) simulated (or actual) user.
*
- * @apiNote immutable, allows an {@link Interaction} to (logically) run with its
+ * @apiNote practically immutable, allows an {@link Interaction} to (logically) run with its
* own simulated (or actual) user
+ *
*/
@With @Getter @Builder.Default
- @NonNull UserMemento user = UserMemento.system();
+ @NonNull /*final*/ UserMemento user = UserMemento.system();
/**
* The (programmatically) simulated (or actual) clock.
@@ -79,13 +89,13 @@ public static InteractionContext ofUserWithSystemDefaults(
* own simulated (or actual) clock
*/
@With @Getter @Builder.Default
- @NonNull VirtualClock clock = VirtualClock.system();
+ @NonNull final VirtualClock clock = VirtualClock.system();
@With @Getter @Builder.Default
- @NonNull Locale locale = Locale.getDefault();
+ @NonNull final Locale locale = Locale.getDefault();
@With @Getter @Builder.Default
- @NonNull TimeZone timeZone = TimeZone.getDefault();
+ @NonNull final TimeZone timeZone = TimeZone.getDefault();
/**
@@ -146,5 +156,16 @@ public static UnaryOperator combine(Stream> mappers) {
return mappers.reduce(t -> t, (a,b) -> a.andThen(b)::apply);
}
-
+ /**
+ * For internal usage, not API.
+ *
+ *
+ * Instead, use {@link #withUser(UserMemento)}, which honours the value semantics of this class.
+ *
+ *
+ * @param user
+ */
+ void replaceUser(UserMemento user) {
+ this.user = user;
+ }
}
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContextUtil.java b/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContextUtil.java
new file mode 100644
index 00000000000..4f38b42f018
--- /dev/null
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/iactnlayer/InteractionContextUtil.java
@@ -0,0 +1,22 @@
+package org.apache.isis.applib.services.iactnlayer;
+
+import org.apache.isis.applib.services.user.UserMemento;
+
+import lombok.experimental.UtilityClass;
+
+
+@UtilityClass
+public class InteractionContextUtil{
+
+ /**
+ * For internal usage, not formal API.
+ *
+ *
+ * Instead, use {@link InteractionContext#withUser(UserMemento)}, which honours the value semantics of this class.
+ *
+ */
+ public static void replaceUserIn(InteractionContext interactionContext, UserMemento userMemento) {
+ interactionContext.replaceUser(userMemento);
+ }
+
+}
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java
index f6fc22700ea..fbf6d27b4f7 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenu.java
@@ -81,7 +81,7 @@ public void impersonate(
final String userName) {
// TODO: should use an SPI for each configured viewer to add in its own role if necessary.
- this.userService.impersonateUser(userName, Collections.singletonList("org.apache.isis.viewer.wicket.roles.USER"));
+ this.userService.impersonateUser(userName, Collections.singletonList("org.apache.isis.viewer.wicket.roles.USER"), null);
this.messageService.informUser("Now impersonating " + userName);
}
@MemberSupport public boolean hideImpersonate() {
@@ -106,9 +106,9 @@ public static class ImpersonateWithRolesDomainEvent extends ActionDomainEvent {
* an {@link ImpersonateMenuAdvisor} implementation to provide the
* choices.
*
- *
- * @param userName
+ * @param userName
* @param roleNames
+ * @param multiTenancyToken
*/
@Action(
domainEvent = ImpersonateWithRolesDomainEvent.class,
@@ -120,14 +120,15 @@ public static class ImpersonateWithRolesDomainEvent extends ActionDomainEvent {
@ActionLayout(sequence = "100.2", cssClassFa = "fa-mask")
public void impersonateWithRoles(
final String userName,
- final List roleNames) {
+ final List roleNames,
+ final String multiTenancyToken) {
// TODO: should use an SPI for each configured viewer to add in its own role if necessary.
- val roleNames2 = new ArrayList<>(roleNames);
- if(!roleNames2.contains("org.apache.isis.viewer.wicket.roles.USER")) {
- roleNames2.add("org.apache.isis.viewer.wicket.roles.USER");
+ val roleNamesCopy = new ArrayList<>(roleNames);
+ if(!roleNamesCopy.contains("org.apache.isis.viewer.wicket.roles.USER")) {
+ roleNamesCopy.add("org.apache.isis.viewer.wicket.roles.USER");
}
- this.userService.impersonateUser(userName, roleNames2);
+ this.userService.impersonateUser(userName, roleNamesCopy, multiTenancyToken);
this.messageService.informUser("Now impersonating " + userName);
}
@MemberSupport public boolean hideImpersonateWithRoles() {
@@ -145,6 +146,9 @@ public void impersonateWithRoles(
@MemberSupport public List default1ImpersonateWithRoles(final String userName) {
return impersonateMenuAdvisor().roleNamesFor(userName);
}
+ @MemberSupport public String default2ImpersonateWithRoles(final String userName, final List roleNames) {
+ return impersonateMenuAdvisor().multiTenancyTokenFor(userName);
+ }
private ImpersonateMenuAdvisor impersonateMenuAdvisor() {
// this is safe because there will always be at least one implementation.
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenuAdvisor.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenuAdvisor.java
index f5c13510463..8922044e76c 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenuAdvisor.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/ImpersonateMenuAdvisor.java
@@ -3,7 +3,7 @@
import java.util.List;
/**
- * Enables {@link ImpersonateMenu#impersonateWithRoles(String, List)}, to provides choices for user and roles.
+ * Enables {@link ImpersonateMenu#impersonateWithRoles(String, List, String)}, to provides choices for user and roles.
*
*
* This will result in the simpler {@link ImpersonateMenu#impersonate(String)} (which simply allows a
@@ -20,7 +20,7 @@ public interface ImpersonateMenuAdvisor {
*
* The {@link ImpersonateMenu} uses this to provide a choices
* (drop-down) for the username (string) argument of
- * {@link ImpersonateMenu#impersonateWithRoles(String, List)}.
+ * {@link ImpersonateMenu#impersonateWithRoles(String, List, String)}.
*
*/
List allUserNames();
@@ -31,7 +31,7 @@ public interface ImpersonateMenuAdvisor {
*
* The {@link ImpersonateMenu} uses this to provide a choices
* (drop-down) for the rolenames (list) argument of
- * {@link ImpersonateMenu#impersonateWithRoles(String, List)}.
+ * {@link ImpersonateMenu#impersonateWithRoles(String, List, String)}.
*
*/
List allRoleNames();
@@ -42,9 +42,20 @@ public interface ImpersonateMenuAdvisor {
*
* The {@link ImpersonateMenu} uses this to select the defaults
* for the rolenames (list) argument of
- * {@link ImpersonateMenu#impersonateWithRoles(String, List)}.
+ * {@link ImpersonateMenu#impersonateWithRoles(String, List, String)}.
*
*/
List roleNamesFor(final String username);
+ /**
+ * Returns the multi-tenancy token of the specified username.
+ *
+ *
+ * The {@link ImpersonateMenu} uses this to select the defaults
+ * for the rolenames (list) argument of
+ * {@link ImpersonateMenu#impersonateWithRoles(String, List, String)}.
+ *
+ */
+ String multiTenancyTokenFor(final String username);
+
}
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java
index 38825f7a824..5b5c78bb21e 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.java
@@ -183,7 +183,7 @@ public void on(UserMemento.TitleUiEvent ev) {
* where the validity of the session is defined by headers in the request.
*/
@Property
- @PropertyLayout(fieldSetId = "authentication", sequence = "1")
+ @PropertyLayout(fieldSetId = "security", sequence = "1")
@Getter @Builder.Default @With(onMethod_ = {@Programmatic})
@NonNull
AuthenticationSource authenticationSource = AuthenticationSource.DEFAULT;
@@ -205,11 +205,25 @@ public boolean isExternal() {
@Property(optionality = Optionality.OPTIONAL)
- @PropertyLayout(fieldSetId = "authentication", sequence = "2")
+ @PropertyLayout(fieldSetId = "security", sequence = "2")
@Getter @Builder.Default @With(onMethod_ = {@Programmatic})
boolean impersonating = false;
+ /**
+ * Indicates which tenancy (or tenancies) this user has access to.
+ *
+ *
+ * The interpretation of this token is implementation-specific.
+ *
+ */
+ @Property(optionality = Optionality.OPTIONAL)
+ @PropertyLayout(fieldSetId = "security", sequence = "3")
+ @Getter @With(onMethod_ = {@Programmatic})
+ @Nullable
+ String multiTenancyToken;
+
+
private static final String DEFAULT_AUTH_VALID_CODE = "";
/**
@@ -282,6 +296,7 @@ public UserMementoBuilder asBuilder() {
.avatarUrl(avatarUrl)
.impersonating(impersonating)
.realName(realName)
+ .multiTenancyToken(multiTenancyToken)
.roles(roles);
}
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.layout.fallback.xml b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.layout.fallback.xml
index 1b2d38608b4..abe976c7b04 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.layout.fallback.xml
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserMemento.layout.fallback.xml
@@ -53,7 +53,7 @@ under the License.
-
+
diff --git a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java
index e0316233fc1..f5437b944e8 100644
--- a/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java
+++ b/api/applib/src/main/java/org/apache/isis/applib/services/user/UserService.java
@@ -83,7 +83,7 @@ public class UserService {
/**
* Returns the details about the current user, either the "effective" user (if being
- * {@link #impersonateUser(String, List) impersonated}) otherwise the "real" user (as obtained from
+ * {@link #impersonateUser(String, List, String) impersonated}) otherwise the "real" user (as obtained from
* the {@link InteractionContext} of the current thread).
*/
public Optional currentUser() {
@@ -138,7 +138,7 @@ public String currentUserNameElseNobody() {
*
* @see #currentUser()
* @see #supportsImpersonation()
- * @see #impersonateUser(String, List)
+ * @see #impersonateUser(String, List, String)
* @see #stopImpersonating()
*/
public boolean isImpersonating() {
@@ -157,7 +157,7 @@ public boolean isImpersonating() {
* This means that the result of this call varies on a request-by-request basis.
*
*
- * @see #impersonateUser(String, List)
+ * @see #impersonateUser(String, List, String)
* @see #isImpersonating()
* @see #stopImpersonating()
*
@@ -199,17 +199,20 @@ private Optional impersonatingHolder() {
* @see #supportsImpersonation()
* @see #isImpersonating()
* @see #stopImpersonating()
- *
* @param userName - the name of the user to be impersonated
* @param roles - the collection of roles for the impersonated user to have.
+ * @param multiTenancyToken
*/
- public void impersonateUser(final String userName, final List roles) {
+ public void impersonateUser(
+ final String userName,
+ final List roles,
+ final String multiTenancyToken) {
impersonatingHolder().ifPresent(x ->
- {
- val userMemento = UserMemento.ofNameAndRoleNames(userName, roles)
- .withImpersonating(true);
- x.setUserMemento(userMemento);
- }
+ x.setUserMemento(
+ UserMemento.ofNameAndRoleNames(userName, roles)
+ .withImpersonating(true)
+ .withMultiTenancyToken(multiTenancyToken)
+ )
);
}
@@ -225,11 +228,11 @@ public void impersonateUser(final String userName, final List roles) {
*
*
* Intended to be called at some point after
- * {@link #impersonateUser(String, List)} would have been called.
+ * {@link #impersonateUser(String, List, String)} would have been called.
*
*
* @see #supportsImpersonation()
- * @see #impersonateUser(String, List)
+ * @see #impersonateUser(String, List, String)
* @see #isImpersonating()
*/
public void stopImpersonating() {
diff --git a/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java b/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java
index 504ac666ec6..8cef9dfa414 100644
--- a/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java
+++ b/api/applib/src/test/java/org/apache/isis/applib/services/user/UserMemento_Test.java
@@ -214,6 +214,29 @@ void user_and_roles_preserved_and_impersonating_flag_set() throws MalformedURLEx
}
}
+ @Nested
+ class withTenancyToken {
+
+ @Test
+ void user_and_roles_preserved_and_impersonating_flag_set() throws MalformedURLException {
+
+ // given
+ val userMemento = UserMemento.ofNameAndRoleNames("fredflintstone", "CAVEMAN", "HUSBAND");
+
+ // when
+ val userMemento2 = userMemento.withMultiTenancyToken("/ITA");
+
+ // then copy created
+ Assertions.assertThat(userMemento2).isNotSameAs(userMemento);
+
+ // then copy correct
+ Assertions.assertThat(userMemento2.getMultiTenancyToken()).isEqualTo("/ITA");
+
+ // then original unchanged
+ Assertions.assertThat(userMemento.getMultiTenancyToken()).isNull();
+ }
+ }
+
@Nested
class all_the_withers {
@@ -225,7 +248,9 @@ void happy_case() throws MalformedURLException {
.withRoleAdded("CAVEMAN")
.withRoleAdded("HUSBAND")
.withAvatarUrl(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"))
- .withRealName("Fred Flintstone");
+ .withRealName("Fred Flintstone")
+ .withMultiTenancyToken("/USA/Bedrock")
+ ;
// then
Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
@@ -250,6 +275,7 @@ void happy_case() throws MalformedURLException {
Assertions.assertThat(userMemento2.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
Assertions.assertThat(userMemento2.getRealName()).isEqualTo("Fred Flintstone");
Assertions.assertThat(userMemento2.isImpersonating()).isTrue();
+ Assertions.assertThat(userMemento2.getMultiTenancyToken()).isEqualTo("/USA/Bedrock");
// then original unchanged
Assertions.assertThat(userMemento.getName()).isEqualTo("fredflintstone");
@@ -259,6 +285,7 @@ void happy_case() throws MalformedURLException {
Assertions.assertThat(userMemento.getAvatarUrl()).isEqualTo(new java.net.URL("https://upload.wikimedia.org/wikipedia/en/a/ad/Fred_Flintstone.png"));
Assertions.assertThat(userMemento.getRealName()).isEqualTo("Fred Flintstone");
Assertions.assertThat(userMemento.isImpersonating()).isFalse();
+ Assertions.assertThat(userMemento.getMultiTenancyToken()).isEqualTo("/USA/Bedrock");
}
}
diff --git a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonateMenuAdvisorDefault.java b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonateMenuAdvisorDefault.java
index e83e0211676..11ef312b6f0 100644
--- a/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonateMenuAdvisorDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/isis/core/runtimeservices/user/ImpersonateMenuAdvisorDefault.java
@@ -17,7 +17,7 @@
*
* This has the effect that the
* {@link org.apache.isis.applib.services.user.ImpersonateMenu}'s
- * {@link org.apache.isis.applib.services.user.ImpersonateMenu#impersonateWithRoles(String, List) impersonateWithRoles}
+ * {@link org.apache.isis.applib.services.user.ImpersonateMenu#impersonateWithRoles(String, List, String) impersonateWithRoles}
* action will be hidden.
*
*/
@@ -38,9 +38,13 @@ public List allRoleNames() {
}
@Override
- public List roleNamesFor(
- final String username) {
+ public List roleNamesFor(final String username) {
return Collections.emptyList();
}
+ @Override
+ public String multiTenancyTokenFor(String username) {
+ return null;
+ }
+
}
diff --git a/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java b/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java
index 8f03b81ce02..5457f185962 100644
--- a/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java
+++ b/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/AuthenticationManager.java
@@ -35,7 +35,9 @@
import org.apache.isis.applib.annotation.PriorityPrecedence;
import org.apache.isis.applib.exceptions.unrecoverable.NoAuthenticatorException;
import org.apache.isis.applib.services.iactnlayer.InteractionContext;
+import org.apache.isis.applib.services.iactnlayer.InteractionContextUtil;
import org.apache.isis.applib.services.iactnlayer.InteractionService;
+import org.apache.isis.applib.services.user.UserMemento;
import org.apache.isis.applib.util.ToString;
import org.apache.isis.commons.collections.Can;
import org.apache.isis.commons.internal.base._Timing;
@@ -61,16 +63,19 @@ public class AuthenticationManager {
private final @NonNull InteractionService interactionService;
private final @NonNull RandomCodeGenerator randomCodeGenerator;
private final @NonNull Can registrars;
+ private final @NonNull List userMementoRefiners;
@Inject
public AuthenticationManager(
final List authenticators,
// needs @Lazy due to circular provisioning dependency
final @Lazy InteractionService anonymousInteractionFactory,
- final RandomCodeGenerator randomCodeGenerator) {
+ final RandomCodeGenerator randomCodeGenerator,
+ final List userMementoRefiners) {
this.interactionService = anonymousInteractionFactory;
this.randomCodeGenerator = randomCodeGenerator;
this.authenticators = Can.ofCollection(authenticators);
+ this.userMementoRefiners = userMementoRefiners;
if (this.authenticators.isEmpty()) {
throw new NoAuthenticatorException("No authenticators specified");
}
@@ -102,13 +107,13 @@ public AuthenticationManager(
return interactionService.callAnonymous(()->{
for (val authenticator : compatibleAuthenticators) {
- val authentication = authenticator.authenticate(request, getUnusedRandomCode());
- if (authentication != null) {
- val userMemento = authentication.getUser();
+ val interactionContext = authenticator.authenticate(request, getUnusedRandomCode());
+ if (interactionContext != null) {
+ val userMemento = refineUserWithin(interactionContext);
userByValidationCode.put(
userMemento.getAuthenticationCode(),
userMemento.getName());
- return authentication;
+ return interactionContext;
}
}
@@ -116,6 +121,27 @@ public AuthenticationManager(
});
}
+ /**
+ * Iterates over all available {@link UserMementoRefiner}s; if at the end the {@link UserMemento} has been changed,
+ * then replaces (using a private API) the {@link UserMemento} held within {@link InteractionContext}.
+ *
+ * @param interactionContext
+ * @return
+ */
+ @NonNull
+ private UserMemento refineUserWithin(InteractionContext interactionContext) {
+ val userMementoOrig = interactionContext.getUser();
+ UserMemento userMemento = userMementoOrig;
+ for (UserMementoRefiner refiner : userMementoRefiners) {
+ final UserMemento refined = refiner.refine(userMemento);
+ userMemento = refined != null ? refined : userMemento;
+ }
+ if(userMemento != userMementoOrig) {
+ InteractionContextUtil.replaceUserIn(interactionContext, userMemento);
+ }
+ return interactionContext.getUser();
+ }
+
private String getUnusedRandomCode() {
val stopWatch = _Timing.now();
diff --git a/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/UserMementoRefiner.java b/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/UserMementoRefiner.java
new file mode 100644
index 00000000000..cdd895f8ebd
--- /dev/null
+++ b/core/security/src/main/java/org/apache/isis/core/security/authentication/manager/UserMementoRefiner.java
@@ -0,0 +1,78 @@
+/*
+ * 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.core.security.authentication.manager;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import org.apache.isis.applib.annotation.PriorityPrecedence;
+import org.apache.isis.applib.exceptions.unrecoverable.NoAuthenticatorException;
+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.util.ToString;
+import org.apache.isis.commons.collections.Can;
+import org.apache.isis.commons.internal.base._Timing;
+import org.apache.isis.commons.internal.collections._Maps;
+import org.apache.isis.core.security.authentication.AuthenticationRequest;
+import org.apache.isis.core.security.authentication.Authenticator;
+import org.apache.isis.core.security.authentication.standard.RandomCodeGenerator;
+import org.apache.isis.core.security.authentication.standard.Registrar;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.val;
+
+/**
+ * SPI provided by the internal {@link AuthenticationManager}, allowing the {@link UserMemento} representing an
+ * authenticated (logged in) user to be refined.
+ *
+ *
+ * As {@link UserMemento} is immutable, the implementation must return a new instance. This new instance replaces
+ * the {@link InteractionContext#getUser() user reference held} by {@link InteractionContext}.
+ *
+ *
+ *
+ * Originally introduced to allow SecMan to update the {@link UserMemento#getMultiTenancyToken() multi-tenancy}
+ * token of {@link UserMemento}.
+ *
+ */
+public interface UserMementoRefiner {
+
+ /**
+ * Return either the same (unchanged) or an updated {@link UserMemento} to use instead of the original within the
+ * owning {@link InteractionContext}.
+ *
+ * @param userMemento - to be refined
+ * @return the userMemento that is refined, or else unchanged.
+ */
+ UserMemento refine(final UserMemento userMemento);
+
+}
diff --git a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepository.java b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepository.java
index 1ddfdd978ac..5ef5383b2b9 100644
--- a/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepository.java
+++ b/extensions/security/secman/applib/src/main/java/org/apache/isis/extensions/secman/applib/permission/dom/ApplicationPermissionRepository.java
@@ -98,7 +98,7 @@ ApplicationPermission newPermissionNoCheck(
* Uses the {@link UserMemento#getRoles() roles} held within the provided {@link UserMemento}.
*
*
- * Added to support {@link org.apache.isis.applib.services.user.ImpersonateMenu#impersonateWithRoles(String, List) impersonation by role}.
+ * Added to support {@link org.apache.isis.applib.services.user.ImpersonateMenu#impersonateWithRoles(String, List, String) impersonation by role}.
*
*
* @see #findByRoleNames(List)
diff --git a/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/IsisModuleExtSecmanIntegration.java b/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/IsisModuleExtSecmanIntegration.java
index 23eb2a8b80a..ff1c7665344 100644
--- a/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/IsisModuleExtSecmanIntegration.java
+++ b/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/IsisModuleExtSecmanIntegration.java
@@ -26,6 +26,7 @@
import org.apache.isis.extensions.secman.integration.facets.TenantedAuthorizationPostProcessor;
import org.apache.isis.extensions.secman.integration.spiimpl.ImpersonateMenuAdvisorForSecman;
import org.apache.isis.extensions.secman.integration.spiimpl.TableColumnVisibilityServiceForSecman;
+import org.apache.isis.extensions.secman.integration.usermementorefiner.UserMementoRefinerFromApplicationUser;
import org.apache.isis.extensions.secman.integration.userreg.UserRegistrationServiceForSecman;
/**
@@ -43,6 +44,8 @@
ImpersonateMenuAdvisorForSecman.class, //not activated by default yet
UserRegistrationServiceForSecman.class,
+ UserMementoRefinerFromApplicationUser.class,
+
})
public class IsisModuleExtSecmanIntegration {
diff --git a/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/spiimpl/ImpersonateMenuAdvisorForSecman.java b/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/spiimpl/ImpersonateMenuAdvisorForSecman.java
index 8e293f2762f..142a19f9bd5 100644
--- a/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/spiimpl/ImpersonateMenuAdvisorForSecman.java
+++ b/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/spiimpl/ImpersonateMenuAdvisorForSecman.java
@@ -7,10 +7,10 @@
import javax.inject.Inject;
import javax.inject.Named;
-import org.apache.isis.applib.annotation.PriorityPrecedence;
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.message.MessageService;
import org.apache.isis.applib.services.user.ImpersonateMenuAdvisor;
import org.apache.isis.applib.services.user.UserService;
@@ -68,4 +68,15 @@ public List roleNamesFor(
.collect(Collectors.toList());
}
+ @Override
+ public String multiTenancyTokenFor(String username) {
+ if(username == null) {
+ return null;
+ }
+ val applicationUser =
+ applicationUserRepository.findByUsername(username)
+ .orElseThrow(RuntimeException::new);
+ return applicationUser.getAtPath();
+ }
+
}
diff --git a/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/usermementorefiner/UserMementoRefinerFromApplicationUser.java b/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/usermementorefiner/UserMementoRefinerFromApplicationUser.java
new file mode 100644
index 00000000000..2b29aa2b32e
--- /dev/null
+++ b/extensions/security/secman/integration/src/main/java/org/apache/isis/extensions/secman/integration/usermementorefiner/UserMementoRefinerFromApplicationUser.java
@@ -0,0 +1,45 @@
+/*
+ * 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.extensions.secman.integration.usermementorefiner;
+
+import javax.inject.Inject;
+
+import org.springframework.stereotype.Service;
+
+import org.apache.isis.applib.services.user.UserMemento;
+import org.apache.isis.core.security.authentication.manager.UserMementoRefiner;
+import org.apache.isis.extensions.secman.applib.user.dom.ApplicationUserRepository;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+
+@Service
+@Log4j2
+@RequiredArgsConstructor(onConstructor_ = {@Inject})
+public class UserMementoRefinerFromApplicationUser implements UserMementoRefiner {
+
+ private final ApplicationUserRepository applicationUserRepository;
+
+ @Override
+ public UserMemento refine(UserMemento userMemento) {
+ return applicationUserRepository.findByUsername(userMemento.getName())
+ .map(applicationUser -> userMemento.withMultiTenancyToken(applicationUser.getAtPath()))
+ .orElse(userMemento);
+ }
+}
diff --git a/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverter.java b/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverter.java
index 39efb07a3b6..535a3c2a3e0 100644
--- a/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverter.java
+++ b/security/spring/src/main/java/org/apache/isis/security/spring/authconverters/AuthenticationConverter.java
@@ -24,7 +24,7 @@
* All known converters are checked one by one, but checking stops once one
* converter has successively converted the {@link Authentication} into a
* {@link UserMemento} (in other words, chain-of-responsibility pattern).
- * Use the {@link org.springframework.core.Ordered} to influence the order
+ * Use the {@link javax.annotation.Priority} annotation to influence the order
* in which converter implementations are checked.
*
*
diff --git a/viewers/common/src/main/java/org/apache/isis/viewer/common/applib/mixins/Object_impersonateWithRoles.java b/viewers/common/src/main/java/org/apache/isis/viewer/common/applib/mixins/Object_impersonateWithRoles.java
index f326abffba3..2bc0da9f2d1 100644
--- a/viewers/common/src/main/java/org/apache/isis/viewer/common/applib/mixins/Object_impersonateWithRoles.java
+++ b/viewers/common/src/main/java/org/apache/isis/viewer/common/applib/mixins/Object_impersonateWithRoles.java
@@ -37,7 +37,7 @@
import lombok.val;
/**
- * Same as {@link ImpersonateMenu#impersonateWithRoles(String, List)},
+ * Same as {@link ImpersonateMenu#impersonateWithRoles(String, List, String)},
* but implemented as a mixin so that can be invoked while accessing an object.
*
* @since 2.0 {@index}
@@ -67,8 +67,9 @@ public static class ActionDomainEvent
public Object act(
final String userName,
- final List roleNames) {
- impersonateMenu.impersonateWithRoles(userName, roleNames);
+ final List roleNames,
+ final String multiTenancyToken) {
+ impersonateMenu.impersonateWithRoles(userName, roleNames, multiTenancyToken);
return holder;
}
@@ -94,16 +95,18 @@ public Object act(
return null;
}
- @MemberSupport public List choices1Act(
- final String userName) {
+ @MemberSupport public List choices1Act(final String userName) {
return impersonateMenu.choices1ImpersonateWithRoles(userName);
}
- @MemberSupport public List default1Act(
- final String userName) {
+ @MemberSupport public List default1Act(final String userName) {
return impersonateMenu.default1ImpersonateWithRoles(userName);
}
+ @MemberSupport public String default2Act(final String userName, final List roleNames) {
+ return impersonateMenu.default2ImpersonateWithRoles(userName, roleNames);
+ }
+
@Inject ImpersonateMenu impersonateMenu;
}