diff --git a/src/main/java/org/graylog/plugins/auth/sso/HeaderRoleUtil.java b/src/main/java/org/graylog/plugins/auth/sso/HeaderRoleUtil.java
new file mode 100644
index 0000000..17e070e
--- /dev/null
+++ b/src/main/java/org/graylog/plugins/auth/sso/HeaderRoleUtil.java
@@ -0,0 +1,86 @@
+/**
+ * This file is part of Graylog Archive.
+ *
+ * Graylog Archive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Graylog Archive is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Graylog Archive. If not, see .
+ */
+package org.graylog.plugins.auth.sso;
+
+import org.graylog2.database.NotFoundException;
+import org.graylog2.shared.users.Role;
+import org.graylog2.users.RoleService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.core.MultivaluedMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Encapsulating common header and role parsing.
+ */
+abstract class HeaderRoleUtil {
+ private static final Logger LOG = LoggerFactory.getLogger(HeaderRoleUtil.class);
+
+ static Optional headerValue(MultivaluedMap headers, @Nullable String headerName) {
+ if (headerName == null) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(headers.getFirst(headerName.toLowerCase()));
+ }
+
+ static Optional> headerValues(MultivaluedMap headers,
+ @Nullable String headerNamePrefix) {
+ if (headerNamePrefix == null) {
+ return Optional.empty();
+ }
+ Set keys = headers.keySet();
+ List headerValues = keys.stream().filter(key -> key.startsWith(headerNamePrefix.toLowerCase()))
+ .map(key -> headers.getFirst(key)).collect(Collectors.toList());
+
+ return Optional.ofNullable(headerValues);
+ }
+
+ static @NotNull Set csv(@NotNull List values) {
+ Set uniqValues = new HashSet<>();
+ for (String csString : values) {
+ String[] valueArr = csString.split(",");
+
+ for (String value : valueArr) {
+ uniqValues.add(value.trim());
+ }
+ }
+ return uniqValues;
+ }
+
+ static @NotNull Set getRoleIds(RoleService roleService, @NotNull Set roleNames) {
+ Set roleIds = new HashSet<>();
+ for (String roleName : roleNames) {
+ if (roleService.exists(roleName)) {
+ try {
+ Role r = roleService.load(roleName);
+ roleIds.add(r.getId());
+ } catch (NotFoundException e) {
+ LOG.error("Role {} not found, but it existed before", roleName);
+ }
+ }
+ }
+ return roleIds;
+ }
+
+}
diff --git a/src/main/java/org/graylog/plugins/auth/sso/SsoAuthModule.java b/src/main/java/org/graylog/plugins/auth/sso/SsoAuthModule.java
index 6ee4c35..9d0c102 100644
--- a/src/main/java/org/graylog/plugins/auth/sso/SsoAuthModule.java
+++ b/src/main/java/org/graylog/plugins/auth/sso/SsoAuthModule.java
@@ -17,9 +17,12 @@
package org.graylog.plugins.auth.sso;
import com.google.inject.Scopes;
+import com.google.inject.multibindings.Multibinder;
import org.graylog.plugins.auth.sso.audit.SsoAuthAuditEventTypes;
import org.graylog2.plugin.PluginModule;
+import javax.ws.rs.container.DynamicFeature;
+
/**
* Extend the PluginModule abstract class here to add you plugin to the system.
*/
@@ -31,5 +34,8 @@ protected void configure() {
addRestResource(SsoConfigResource.class);
addPermissions(SsoAuthPermissions.class);
addAuditEventTypes(SsoAuthAuditEventTypes.class);
+
+ Multibinder> setBinder = jerseyDynamicFeatureBinder();
+ setBinder.addBinding().toInstance(SsoSecurityBinding.class);
}
}
diff --git a/src/main/java/org/graylog/plugins/auth/sso/SsoAuthRealm.java b/src/main/java/org/graylog/plugins/auth/sso/SsoAuthRealm.java
index 4eb5256..d5874d3 100644
--- a/src/main/java/org/graylog/plugins/auth/sso/SsoAuthRealm.java
+++ b/src/main/java/org/graylog/plugins/auth/sso/SsoAuthRealm.java
@@ -38,17 +38,16 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import javax.ws.rs.core.MultivaluedMap;
import java.net.UnknownHostException;
import java.util.Collections;
-import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
-import java.util.stream.Collectors;
+
+import static org.graylog.plugins.auth.sso.HeaderRoleUtil.*;
public class SsoAuthRealm extends AuthenticatingRealm {
private static final Logger LOG = LoggerFactory.getLogger(SsoAuthRealm.class);
@@ -187,34 +186,13 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
return null;
}
- protected Set csv(List values) {
- Set uniqValues = new HashSet<>();
- for (String csString : values) {
- String[] valueArr = csString.split(",");
-
- for (String value : valueArr) {
- uniqValues.add(value.trim());
- }
- }
- return uniqValues;
- }
-
protected void syncUserRoles(List roleCsv, User user) throws ValidationException {
Set roleNames = csv(roleCsv);
Set existingRoles = user.getRoleIds();
- Set syncedRoles = new HashSet<>();
- for (String roleName : roleNames) {
- if (roleService.exists(roleName)) {
- try {
- Role r = roleService.load(roleName);
- syncedRoles.add(r.getId());
- } catch (NotFoundException e) {
- LOG.error("Role {} not found, but it existed before", roleName);
- }
- }
- }
- if (existingRoles != null && !existingRoles.equals(syncedRoles)) {
+ Set syncedRoles = getRoleIds(roleService, roleNames);
+
+ if (!existingRoles.equals(syncedRoles)) {
user.setRoleIds(syncedRoles);
userService.save(user);
}
@@ -234,22 +212,4 @@ boolean inTrustedSubnets(String remoteAddr) {
});
}
- private Optional headerValue(MultivaluedMap headers, @Nullable String headerName) {
- if (headerName == null) {
- return Optional.empty();
- }
- return Optional.ofNullable(headers.getFirst(headerName.toLowerCase()));
- }
-
- protected Optional> headerValues(MultivaluedMap headers,
- @Nullable String headerNamePrefix) {
- if (headerNamePrefix == null) {
- return Optional.empty();
- }
- Set keys = headers.keySet();
- List headerValues = keys.stream().filter(key -> key.startsWith(headerNamePrefix.toLowerCase()))
- .map(key -> headers.getFirst(key)).collect(Collectors.toList());
-
- return Optional.ofNullable(headerValues);
- }
}
diff --git a/src/main/java/org/graylog/plugins/auth/sso/SsoAuthenticationFilter.java b/src/main/java/org/graylog/plugins/auth/sso/SsoAuthenticationFilter.java
new file mode 100644
index 0000000..f6eb3d9
--- /dev/null
+++ b/src/main/java/org/graylog/plugins/auth/sso/SsoAuthenticationFilter.java
@@ -0,0 +1,136 @@
+/**
+ * This file is part of Graylog Archive.
+ *
+ * Graylog Archive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Graylog Archive is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Graylog Archive. If not, see .
+ */
+package org.graylog.plugins.auth.sso;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.subject.Subject;
+import org.graylog2.plugin.cluster.ClusterConfigService;
+import org.graylog2.plugin.database.users.User;
+import org.graylog2.security.realm.LdapUserAuthenticator;
+import org.graylog2.shared.users.UserService;
+import org.graylog2.users.RoleService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Priority;
+import javax.inject.Inject;
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.Priorities;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.core.Response;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.graylog.plugins.auth.sso.HeaderRoleUtil.*;
+
+/**
+ * Checking the session if it still matches the user and the roles in the HTTP request SSO headers.
+ * This is necessary as {@link SsoAuthRealm} is only called at the creation of the session.
+ *
+ * DESIGN DECISIONS
+ *
+ * #1 This class doesn't honor the additional trust proxies. This leads to closing more session than necessary,
+ * but avoids duplicating the logic here with the danger of failing to call it identically.
+ *
+ * #2 If role syncing with Graylog is activated, the first request of a session checks roles against the user database.
+ * If this matches, the contents of the role header are cached in the session and checked again if the role headers of a request change.
+ * If this doesn't match, the user is logged out and the session is terminated. In a SSO system this will trigger a re-login
+ * and a re-sync of the user roles.
+ *
+ */
+/* This needs to have a lower priority than the {@link org.graylog2.shared.security.ShiroAuthenticationFilter}
+ * so that the {@link Subject} is already populated. */
+@Priority(Priorities.AUTHENTICATION + 1)
+public class SsoAuthenticationFilter implements ContainerRequestFilter {
+ private static final Logger LOG = LoggerFactory.getLogger(SsoAuthenticationFilter.class);
+ private static final String VERIFIED_ROLES = SsoAuthenticationFilter.class.getName() + ".VERIFIED_USERS";
+
+ private final ClusterConfigService clusterConfigService;
+ private final RoleService roleService;
+ private final UserService userService;
+ private final LdapUserAuthenticator ldapAuthenticator;
+
+ @Inject
+ public SsoAuthenticationFilter(ClusterConfigService clusterConfigService, RoleService roleService, UserService userService, LdapUserAuthenticator ldapAuthenticator) {
+ this.clusterConfigService = clusterConfigService;
+ this.roleService = roleService;
+ this.userService = userService;
+ this.ldapAuthenticator = ldapAuthenticator;
+ }
+
+ @Override
+ public void filter(ContainerRequestContext containerRequestContext) {
+
+ final SsoAuthConfig config = clusterConfigService.getOrDefault(
+ SsoAuthConfig.class,
+ SsoAuthConfig.defaultConfig(""));
+
+ Subject subject = SecurityUtils.getSubject();
+ // the subject needs to be inspected only if there is a session.
+ // If there is no session, Shiro has checked the headers on this request already
+ if (subject.getSession(false) != null) {
+ Session session = subject.getSession();
+ final String usernameHeader = config.usernameHeader();
+ final Optional userNameOption = headerValue(containerRequestContext.getHeaders(), usernameHeader);
+ // is the header with the user name is present ...
+ if (userNameOption.isPresent()) {
+ String username = userNameOption.get();
+ if (!username.equals(subject.getPrincipal())) {
+ // terminate the session and return "unauthorized" for this request
+ LOG.warn("terminating session of {} as new user {} appears in the header", subject.getPrincipal(), username);
+ subject.logout();
+ throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build());
+ }
+ if (config.syncRoles()) {
+ Optional> rolesList = headerValues(containerRequestContext.getHeaders(), config.rolesHeader());
+ if (rolesList.isPresent() && !rolesList.get().equals(session.getAttribute(VERIFIED_ROLES))) {
+ User user = null;
+ if (ldapAuthenticator.isEnabled()) {
+ user = ldapAuthenticator.syncLdapUser(username);
+ }
+ if (user == null) {
+ user = userService.load(username);
+ }
+ if (user == null) {
+ LOG.error("user {} not found",
+ subject.getPrincipal());
+ subject.logout();
+ throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build());
+ }
+ Set roleNames = csv(rolesList.get());
+ Set existingRoles = user.getRoleIds();
+
+ Set roleIds = getRoleIds(roleService, roleNames);
+ if (!existingRoles.equals(roleIds)) {
+ // terminate the session and return "unauthorized" for this request
+ LOG.warn("terminating session of user {} as roles in user differ from roles in header ({})",
+ subject.getPrincipal(),
+ roleNames);
+ subject.logout();
+ throw new NotAuthorizedException(Response.status(Response.Status.UNAUTHORIZED).build());
+ }
+ session.setAttribute(VERIFIED_ROLES, rolesList.get());
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/graylog/plugins/auth/sso/SsoSecurityBinding.java b/src/main/java/org/graylog/plugins/auth/sso/SsoSecurityBinding.java
new file mode 100644
index 0000000..ef1fb62
--- /dev/null
+++ b/src/main/java/org/graylog/plugins/auth/sso/SsoSecurityBinding.java
@@ -0,0 +1,55 @@
+/**
+ * This file is part of Graylog.
+ *
+ * Graylog is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Graylog is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Graylog. If not, see .
+ */
+package org.graylog.plugins.auth.sso;
+
+import org.apache.shiro.authz.annotation.RequiresAuthentication;
+import org.apache.shiro.authz.annotation.RequiresGuest;
+import org.graylog2.shared.security.ShiroSecurityBinding;
+import org.graylog2.shared.security.ShiroSecurityContextFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.container.DynamicFeature;
+import javax.ws.rs.container.ResourceInfo;
+import javax.ws.rs.core.FeatureContext;
+import java.lang.reflect.Method;
+
+/**
+ * Adding the {@link SsoAuthenticationFilter} to each resource that requires authentication.
+ * This is mirroring the efforts in {@link ShiroSecurityBinding}
+ */
+public class SsoSecurityBinding implements DynamicFeature {
+ private static final Logger LOG = LoggerFactory.getLogger(SsoSecurityBinding.class);
+
+ @Override
+ public void configure(ResourceInfo resourceInfo, FeatureContext context) {
+ final Class> resourceClass = resourceInfo.getResourceClass();
+ final Method resourceMethod = resourceInfo.getResourceMethod();
+
+ context.register(ShiroSecurityContextFilter.class);
+
+ if (resourceMethod.isAnnotationPresent(RequiresAuthentication.class) || resourceClass.isAnnotationPresent(RequiresAuthentication.class)) {
+ if (resourceMethod.isAnnotationPresent(RequiresGuest.class)) {
+ LOG.debug("Resource method {}#{} is marked as unauthenticated, skipping setting filter.", resourceClass.getCanonicalName(), resourceMethod.getName());
+ } else {
+ LOG.debug("Resource method {}#{} requires an authenticated user.", resourceClass.getCanonicalName(), resourceMethod.getName());
+ context.register(SsoAuthenticationFilter.class);
+ }
+ }
+
+ }
+}
diff --git a/src/test/java/org/graylog/plugins/auth/sso/HeaderRoleUtilTest.java b/src/test/java/org/graylog/plugins/auth/sso/HeaderRoleUtilTest.java
new file mode 100644
index 0000000..a8b6cc4
--- /dev/null
+++ b/src/test/java/org/graylog/plugins/auth/sso/HeaderRoleUtilTest.java
@@ -0,0 +1,51 @@
+/**
+ * This file is part of Graylog Archive.
+ *
+ * Graylog Archive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Graylog Archive is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Graylog Archive. If not, see .
+ */
+package org.graylog.plugins.auth.sso;
+
+import org.junit.Test;
+
+import javax.ws.rs.core.MultivaluedHashMap;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+public class HeaderRoleUtilTest {
+
+ @Test
+ public void testHeaderValuesCsv() {
+ MultivaluedHashMap m = new MultivaluedHashMap<>();
+ m.put("roles", Arrays.asList(new String[]{"role1, role2, role3"}));
+ m.put("roles_1", Arrays.asList(new String[]{"asdf1"}));
+ m.put("roles_2", Arrays.asList(new String[]{"asdf2"}));
+
+ Optional> s = HeaderRoleUtil.headerValues(m, "Roles");
+ Set actual = HeaderRoleUtil.csv(s.get());
+ List expected = Arrays.asList(new String[]{"role1","role2","role3","asdf1","asdf2"});
+
+ assertEquals(actual.size(), expected.size());
+ for ( String role : expected) {
+ if ( !actual.contains(role)) {
+ fail("Role [" + role + "] expected, but not in result: " + actual);
+ }
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/graylog/plugins/auth/sso/SsoAuthRealmTest.java b/src/test/java/org/graylog/plugins/auth/sso/SsoAuthRealmTest.java
index 1675c61..24d22fc 100644
--- a/src/test/java/org/graylog/plugins/auth/sso/SsoAuthRealmTest.java
+++ b/src/test/java/org/graylog/plugins/auth/sso/SsoAuthRealmTest.java
@@ -187,27 +187,6 @@ public void testDefaultDomainNotSet() {
assertThat(user.getEmail()).isEqualTo("horst@localhost");
}
- @Test
- public void testHeaderValuesCsv() {
- MultivaluedHashMap m = new MultivaluedHashMap<>();
- m.put("roles", Arrays.asList(new String[]{"role1, role2, role3"}));
- m.put("roles_1", Arrays.asList(new String[]{"asdf1"}));
- m.put("roles_2", Arrays.asList(new String[]{"asdf2"}));
-
- SsoAuthRealm r = new SsoAuthRealm(null, null, null, null, Collections.emptySet());
- Optional> s = r.headerValues(m, "Roles");
- Set actual = r.csv(s.get());
- List expected = Arrays.asList(new String[]{"role1","role2","role3","asdf1","asdf2"});
-
- assertEquals(actual.size(), expected.size());
- for ( String role : expected) {
- if ( !actual.contains(role)) {
- fail("Role [" + role + "] expected, but not in result: " + actual);
- }
- }
-
- }
-
@Test
public void testSyncRoles() throws Exception {
List rolesCsv = Arrays.asList(new String[]{"role1","role2", "role3, role4"});
diff --git a/src/test/java/org/graylog/plugins/auth/sso/SsoAuthenticationFilterTest.java b/src/test/java/org/graylog/plugins/auth/sso/SsoAuthenticationFilterTest.java
new file mode 100644
index 0000000..0ff2cce
--- /dev/null
+++ b/src/test/java/org/graylog/plugins/auth/sso/SsoAuthenticationFilterTest.java
@@ -0,0 +1,211 @@
+/**
+ * This file is part of Graylog Archive.
+ *
+ * Graylog Archive is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Graylog Archive is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Graylog Archive. If not, see .
+ */
+package org.graylog.plugins.auth.sso;
+
+import org.apache.shiro.SecurityUtils;
+import org.apache.shiro.mgt.SecurityManager;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.subject.Subject;
+import org.glassfish.jersey.message.internal.MediaTypeProvider;
+import org.glassfish.jersey.message.internal.OutboundJaxrsResponse;
+import org.glassfish.jersey.message.internal.OutboundMessageContext;
+import org.graylog2.database.NotFoundException;
+import org.graylog2.plugin.cluster.ClusterConfigService;
+import org.graylog2.plugin.database.users.User;
+import org.graylog2.security.realm.LdapUserAuthenticator;
+import org.graylog2.shared.users.Role;
+import org.graylog2.shared.users.UserService;
+import org.graylog2.users.RoleService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import javax.ws.rs.NotAuthorizedException;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.RuntimeDelegate;
+import java.util.*;
+
+import static org.mockito.Mockito.*;
+
+public class SsoAuthenticationFilterTest {
+
+ private static final String USER_HEADER = "Remote-User";
+ private static final String ROLE_HEADER = "Roles";
+
+ @Rule
+ public ExpectedException exceptionRule = ExpectedException.none();
+
+ private SsoAuthConfig config = SsoAuthConfig.builder()
+ .usernameHeader(USER_HEADER)
+ .autoCreateUser(false)
+ .requireTrustedProxies(false)
+ .syncRoles(true)
+ .rolesHeader(ROLE_HEADER)
+ .autoBuild();
+
+ private SsoAuthenticationFilter filter;
+ private Subject subject;
+ private MultivaluedMap headers;
+ private ContainerRequestContext containerRequest;
+ private UserService userService;
+ private RoleService roleService;
+
+ @Before
+ public void setup() {
+ // ClusterConfigService
+ ClusterConfigService clusterConfigService = mock(ClusterConfigService.class);
+ when(clusterConfigService.getOrDefault(
+ SsoAuthConfig.class,
+ SsoAuthConfig.defaultConfig(""))).thenReturn(config);
+
+ // RoleService
+ roleService = mock(RoleService.class);
+
+ // UserService
+ userService = mock(UserService.class);
+
+ // LdapUserAuthenticator
+ LdapUserAuthenticator ldapAuthenticator = mock(LdapUserAuthenticator.class);
+
+ // SsoAuthenticationFilter = System under Test
+ filter = new SsoAuthenticationFilter(clusterConfigService, roleService, userService, ldapAuthenticator);
+
+ // SecurityManager
+ SecurityManager securityManager = mock(SecurityManager.class);
+ SecurityUtils.setSecurityManager(securityManager);
+
+ // Subject
+ subject = mock(Subject.class);
+ when(securityManager.createSubject(any())).thenReturn(subject);
+
+ // Session
+ Session session = mock(Session.class);
+ when(subject.getSession(anyBoolean())).thenReturn(session);
+ when(subject.getSession()).thenReturn(session);
+ Map