Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,27 @@
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;

/**
* Provides the user and scenario specific environment for an {@link Interaction}.
*
* @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 {

Expand All @@ -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.
Expand All @@ -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();


/**
Expand Down Expand Up @@ -146,5 +156,16 @@ public static <T> UnaryOperator<T> combine(Stream<UnaryOperator<T>> mappers) {
return mappers.reduce(t -> t, (a,b) -> a.andThen(b)::apply);
}


/**
* For internal usage, not API.
*
* <p>
* Instead, use {@link #withUser(UserMemento)}, which honours the value semantics of this class.
* </p>
*
* @param user
*/
void replaceUser(UserMemento user) {
this.user = user;
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* Instead, use {@link InteractionContext#withUser(UserMemento)}, which honours the value semantics of this class.
* </p>
*/
public static void replaceUserIn(InteractionContext interactionContext, UserMemento userMemento) {
interactionContext.replaceUser(userMemento);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -106,9 +106,9 @@ public static class ImpersonateWithRolesDomainEvent extends ActionDomainEvent {
* an {@link ImpersonateMenuAdvisor} implementation to provide the
* choices.
* </p>
*
* @param userName
* @param userName
* @param roleNames
* @param multiTenancyToken
*/
@Action(
domainEvent = ImpersonateWithRolesDomainEvent.class,
Expand All @@ -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<String> roleNames) {
final List<String> 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() {
Expand All @@ -145,6 +146,9 @@ public void impersonateWithRoles(
@MemberSupport public List<String> default1ImpersonateWithRoles(final String userName) {
return impersonateMenuAdvisor().roleNamesFor(userName);
}
@MemberSupport public String default2ImpersonateWithRoles(final String userName, final List<String> roleNames) {
return impersonateMenuAdvisor().multiTenancyTokenFor(userName);
}

private ImpersonateMenuAdvisor impersonateMenuAdvisor() {
// this is safe because there will always be at least one implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>
* This will result in the simpler {@link ImpersonateMenu#impersonate(String)} (which simply allows a
Expand All @@ -20,7 +20,7 @@ public interface ImpersonateMenuAdvisor {
* <p>
* 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)}.
* </p>
*/
List<String> allUserNames();
Expand All @@ -31,7 +31,7 @@ public interface ImpersonateMenuAdvisor {
* <p>
* 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)}.
* </p>
*/
List<String> allRoleNames();
Expand All @@ -42,9 +42,20 @@ public interface ImpersonateMenuAdvisor {
* <p>
* 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)}.
* </p>
*/
List<String> roleNamesFor(final String username);

/**
* Returns the multi-tenancy token of the specified username.
*
* <p>
* The {@link ImpersonateMenu} uses this to select the defaults
* for the rolenames (list) argument of
* {@link ImpersonateMenu#impersonateWithRoles(String, List, String)}.
* </p>
*/
String multiTenancyTokenFor(final String username);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*
* <p>
* The interpretation of this token is implementation-specific.
* </p>
*/
@Property(optionality = Optionality.OPTIONAL)
@PropertyLayout(fieldSetId = "security", sequence = "3")
@Getter @With(onMethod_ = {@Programmatic})
@Nullable
String multiTenancyToken;


private static final String DEFAULT_AUTH_VALID_CODE = "";

/**
Expand Down Expand Up @@ -282,6 +296,7 @@ public UserMementoBuilder asBuilder() {
.avatarUrl(avatarUrl)
.impersonating(impersonating)
.realName(realName)
.multiTenancyToken(multiTenancyToken)
.roles(roles);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ under the License.
</bs3:tab>
</bs3:tabGroup>
<cpt:fieldSet name="Details" id="details"/>
<cpt:fieldSet name="Authentication" id="authentication"/>
<cpt:fieldSet name="Security" id="security"/>
</bs3:col>
<bs3:col span="6">
<cpt:collection id="roles"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public class UserService {

/**
* Returns the details about the current user, either the &quot;effective&quot; user (if being
* {@link #impersonateUser(String, List) impersonated}) otherwise the &quot;real&quot; user (as obtained from
* {@link #impersonateUser(String, List, String) impersonated}) otherwise the &quot;real&quot; user (as obtained from
* the {@link InteractionContext} of the current thread).
*/
public Optional<UserMemento> currentUser() {
Expand Down Expand Up @@ -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() {
Expand All @@ -157,7 +157,7 @@ public boolean isImpersonating() {
* This means that the result of this call varies on a request-by-request basis.
* </p>
*
* @see #impersonateUser(String, List)
* @see #impersonateUser(String, List, String)
* @see #isImpersonating()
* @see #stopImpersonating()
*
Expand Down Expand Up @@ -199,17 +199,20 @@ private Optional<ImpersonatedUserHolder> 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<String> roles) {
public void impersonateUser(
final String userName,
final List<String> 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)
)
);
}

Expand All @@ -225,11 +228,11 @@ public void impersonateUser(final String userName, final List<String> roles) {
*
* <p>
* 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.
* </p>
*
* @see #supportsImpersonation()
* @see #impersonateUser(String, List)
* @see #impersonateUser(String, List, String)
* @see #isImpersonating()
*/
public void stopImpersonating() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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");
Expand All @@ -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");
Expand All @@ -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");

}
}
Expand Down
Loading