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; }