You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2022/08/18 10:38:17 UTC
[knox] branch master updated: KNOX-2778 - Enforce concurrent session limit in KnoxSSO (#615)
This is an automated email from the ASF dual-hosted git repository.
smolnar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new 78a058900 KNOX-2778 - Enforce concurrent session limit in KnoxSSO (#615)
78a058900 is described below
commit 78a058900f17078ea3278cc41bf8cc9540ed4415
Author: MrtnBalazs <77...@users.noreply.github.com>
AuthorDate: Thu Aug 18 12:38:11 2022 +0200
KNOX-2778 - Enforce concurrent session limit in KnoxSSO (#615)
---
.../org/apache/knox/gateway/GatewayMessages.java | 3 +
.../gateway/services/DefaultGatewayServices.java | 2 +
.../factory/ConcurrentSessionVerifierFactory.java | 46 +++
.../control/InMemoryConcurrentSessionVerifier.java | 172 +++++++++++
...org.apache.knox.gateway.services.ServiceFactory | 3 +-
.../services/AbstractGatewayServicesTest.java | 3 +-
.../InMemoryConcurrentSessionVerifierTest.java | 323 +++++++++++++++++++++
.../gateway/service/knoxsso/WebSSOResource.java | 26 +-
.../service/knoxsso/WebSSOResourceTest.java | 31 +-
.../gateway/service/knoxsso/KnoxSSOutMessages.java | 3 +
.../gateway/service/knoxsso/WebSSOutResource.java | 27 ++
.../service/knoxsso/WebSSOutResourceTest.java | 6 +-
.../session/control/ConcurrentSessionVerifier.java | 106 -------
.../control/ConcurrentSessionVerifierTest.java | 161 ----------
.../apache/knox/gateway/services/ServiceType.java | 3 +-
.../session/control/ConcurrentSessionVerifier.java | 25 +-
16 files changed, 645 insertions(+), 295 deletions(-)
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
index 574ff388b..c88b467e8 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java
@@ -718,4 +718,7 @@ public interface GatewayMessages {
@Message(level = MessageLevel.DEBUG, text = "Jetty's maxFormKeys is set to {0}")
void setMaxFormKeys(int maxFormKeys);
+
+ @Message(level = MessageLevel.ERROR, text = "ConcurrentSessionVerifier got blank username for verification.")
+ void errorVerifyingUserBlankUsername();
}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index 051776334..994025e5e 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -78,6 +78,8 @@ public class DefaultGatewayServices extends AbstractGatewayServices {
addService(ServiceType.SERVICE_DEFINITION_REGISTRY, gatewayServiceFactory.create(this, ServiceType.SERVICE_DEFINITION_REGISTRY, config, options));
addService(ServiceType.METRICS_SERVICE, gatewayServiceFactory.create(this, ServiceType.METRICS_SERVICE, config, options));
+
+ addService(ServiceType.CONCURRENT_SESSION_VERIFIER, gatewayServiceFactory.create(this, ServiceType.CONCURRENT_SESSION_VERIFIER, config, options));
}
@Override
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/ConcurrentSessionVerifierFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/ConcurrentSessionVerifierFactory.java
new file mode 100644
index 000000000..0e84dc2ff
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/ConcurrentSessionVerifierFactory.java
@@ -0,0 +1,46 @@
+/*
+ * 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.knox.gateway.services.factory;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.session.control.InMemoryConcurrentSessionVerifier;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+public class ConcurrentSessionVerifierFactory extends AbstractServiceFactory {
+ @Override
+ protected Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> options, String implementation) throws ServiceLifecycleException {
+ return shouldCreateService(implementation) ? new InMemoryConcurrentSessionVerifier() : null;
+ }
+
+ @Override
+ protected ServiceType getServiceType() {
+ return ServiceType.CONCURRENT_SESSION_VERIFIER;
+ }
+
+ @Override
+ protected Collection<String> getKnownImplementations() {
+ return Collections.singleton(InMemoryConcurrentSessionVerifier.class.getName());
+ }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifier.java b/gateway-server/src/main/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifier.java
new file mode 100644
index 000000000..bf0ec4df4
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifier.java
@@ -0,0 +1,172 @@
+/*
+ * 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.knox.gateway.session.control;
+
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class InMemoryConcurrentSessionVerifier implements ConcurrentSessionVerifier {
+ private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
+
+ private Set<String> privilegedUsers;
+ private Set<String> nonPrivilegedUsers;
+ private int privilegedUserConcurrentSessionLimit;
+ private int nonPrivilegedUserConcurrentSessionLimit;
+ private Map<String, Set<SessionJWT>> concurrentSessionCounter;
+ private final Lock sessionCountModifyLock = new ReentrantLock();
+
+ @Override
+ public boolean verifySessionForUser(String username, JWT jwtToken) {
+ if (StringUtils.isBlank(username)) {
+ LOG.errorVerifyingUserBlankUsername();
+ return false;
+ }
+ if (!privilegedUsers.contains(username) && !nonPrivilegedUsers.contains(username)) {
+ return true;
+ }
+
+ sessionCountModifyLock.lock();
+ try {
+ int validTokenNumber = countValidTokensForUser(username);
+ if (privilegedUserCheckLimitReached(username, validTokenNumber) || nonPrivilegedUserCheckLimitReached(username, validTokenNumber)) {
+ return false;
+ }
+ concurrentSessionCounter.putIfAbsent(username, new HashSet<>());
+ concurrentSessionCounter.compute(username, (key, sessionTokenSet) -> addTokenForUser(sessionTokenSet, jwtToken));
+ } finally {
+ sessionCountModifyLock.unlock();
+ }
+ return true;
+ }
+
+ int countValidTokensForUser(String username) {
+ return (int) concurrentSessionCounter
+ .getOrDefault(username, Collections.emptySet())
+ .stream()
+ .filter(each -> !each.hasExpired())
+ .count();
+ }
+
+ private boolean privilegedUserCheckLimitReached(String username, int validTokenNumber) {
+ if (privilegedUserConcurrentSessionLimit < 0) {
+ return false;
+ }
+ return privilegedUsers.contains(username) && (validTokenNumber >= privilegedUserConcurrentSessionLimit);
+ }
+
+ private boolean nonPrivilegedUserCheckLimitReached(String username, int validTokenNumber) {
+ if (nonPrivilegedUserConcurrentSessionLimit < 0) {
+ return false;
+ }
+ return nonPrivilegedUsers.contains(username) && (validTokenNumber >= nonPrivilegedUserConcurrentSessionLimit);
+ }
+
+ @Override
+ public void sessionEndedForUser(String username, String token) {
+ if (StringUtils.isNotBlank(token) && StringUtils.isNotBlank(username)) {
+ sessionCountModifyLock.lock();
+ try {
+ concurrentSessionCounter.computeIfPresent(username, (key, sessionTokenSet) -> removeTokenFromUser(sessionTokenSet, token));
+ } finally {
+ sessionCountModifyLock.unlock();
+ }
+ }
+ }
+
+ private Set<SessionJWT> removeTokenFromUser(Set<SessionJWT> sessionTokenSet, String token) {
+ sessionTokenSet.removeIf(sessionToken -> sessionToken.getToken().equals(token));
+ if (sessionTokenSet.isEmpty()) {
+ return null;
+ }
+ return sessionTokenSet;
+ }
+
+ private Set<SessionJWT> addTokenForUser(Set<SessionJWT> sessionTokenSet, JWT jwtToken) {
+ sessionTokenSet.add(new SessionJWT(jwtToken));
+ return sessionTokenSet;
+ }
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+ this.privilegedUsers = config.getPrivilegedUsers();
+ this.nonPrivilegedUsers = config.getNonPrivilegedUsers();
+ this.privilegedUserConcurrentSessionLimit = config.getPrivilegedUsersConcurrentSessionLimit();
+ this.nonPrivilegedUserConcurrentSessionLimit = config.getNonPrivilegedUsersConcurrentSessionLimit();
+ this.concurrentSessionCounter = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public void start() throws ServiceLifecycleException {
+
+ }
+
+ @Override
+ public void stop() throws ServiceLifecycleException {
+
+ }
+
+ public static class SessionJWT {
+ private final Date expiry;
+ private final String token;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ SessionJWT that = (SessionJWT) o;
+ return Objects.equals(token, that.token);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(token);
+ }
+
+ public SessionJWT(JWT token) {
+ this.expiry = token.getExpiresDate();
+ this.token = token.toString();
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+ public boolean hasExpired() {
+ return expiry != null && expiry.before(new Date());
+ }
+ }
+}
diff --git a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
index 02275eafb..b8a4fc49d 100644
--- a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
+++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
@@ -30,4 +30,5 @@ org.apache.knox.gateway.services.factory.ServiceRegistryServiceFactory
org.apache.knox.gateway.services.factory.SslServiceFactory
org.apache.knox.gateway.services.factory.TokenServiceFactory
org.apache.knox.gateway.services.factory.TokenStateServiceFactory
-org.apache.knox.gateway.services.factory.TopologyServiceFactory
\ No newline at end of file
+org.apache.knox.gateway.services.factory.TopologyServiceFactory
+org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory
\ No newline at end of file
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
index 1f0a35258..8ecb72684 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
@@ -62,7 +62,8 @@ public class AbstractGatewayServicesTest {
ServiceType.CRYPTO_SERVICE,
ServiceType.HOST_MAPPING_SERVICE,
ServiceType.SERVICE_DEFINITION_REGISTRY,
- ServiceType.SERVICE_REGISTRY_SERVICE
+ ServiceType.SERVICE_REGISTRY_SERVICE,
+ ServiceType.CONCURRENT_SESSION_VERIFIER
};
assertNotEquals(ServiceType.values(), orderedServiceTypes);
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifierTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifierTest.java
new file mode 100644
index 000000000..4cf1f2c75
--- /dev/null
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/session/control/InMemoryConcurrentSessionVerifierTest.java
@@ -0,0 +1,323 @@
+/*
+ * 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.knox.gateway.session.control;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.MasterService;
+import org.apache.knox.gateway.services.security.impl.DefaultKeystoreService;
+import org.apache.knox.gateway.services.security.token.JWTokenAttributes;
+import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
+import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.token.impl.DefaultTokenAuthorityService;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class InMemoryConcurrentSessionVerifierTest {
+ private InMemoryConcurrentSessionVerifier verifier;
+ private Map<String, String> options;
+ private DefaultTokenAuthorityService tokenAuthority;
+ private JWTokenAttributes jwtAttributesForAdmin;
+ private JWTokenAttributes jwtAttributesForTom;
+ private JWT adminToken1;
+ private JWT adminToken2;
+ private JWT adminToken3;
+ private JWT adminToken4;
+ private JWT adminToken5;
+ private JWT adminToken6;
+ private JWT tomToken1;
+ private JWT tomToken2;
+ private JWT tomToken3;
+ private JWT tomToken4;
+ private JWT tomToken5;
+ private JWT tomToken6;
+
+ @Before
+ public void setUp() throws AliasServiceException, IOException, ServiceLifecycleException {
+ verifier = new InMemoryConcurrentSessionVerifier();
+ options = Collections.emptyMap();
+
+ GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+ String basedir = System.getProperty("basedir");
+ if (basedir == null) {
+ basedir = new File(".").getCanonicalPath();
+ }
+ EasyMock.expect(config.getGatewaySecurityDir()).andReturn(basedir + "/target/test-classes").anyTimes();
+ EasyMock.expect(config.getGatewayKeystoreDir()).andReturn(basedir + "/target/test-classes/keystores").anyTimes();
+ EasyMock.expect(config.getSigningKeystoreName()).andReturn("server-keystore.jks").anyTimes();
+ EasyMock.expect(config.getSigningKeystorePath()).andReturn(basedir + "/target/test-classes/keystores/server-keystore.jks").anyTimes();
+ EasyMock.expect(config.getSigningKeystorePasswordAlias()).andReturn(GatewayConfig.DEFAULT_SIGNING_KEYSTORE_PASSWORD_ALIAS).anyTimes();
+ EasyMock.expect(config.getSigningKeyPassphraseAlias()).andReturn(GatewayConfig.DEFAULT_SIGNING_KEY_PASSPHRASE_ALIAS).anyTimes();
+ EasyMock.expect(config.getSigningKeystoreType()).andReturn("jks").anyTimes();
+ EasyMock.expect(config.getSigningKeyAlias()).andReturn("server").anyTimes();
+ EasyMock.expect(config.getCredentialStoreType()).andReturn(GatewayConfig.DEFAULT_CREDENTIAL_STORE_TYPE).anyTimes();
+ EasyMock.expect(config.getCredentialStoreAlgorithm()).andReturn(GatewayConfig.DEFAULT_CREDENTIAL_STORE_ALG).anyTimes();
+
+ MasterService ms = EasyMock.createNiceMock(MasterService.class);
+ EasyMock.expect(ms.getMasterSecret()).andReturn("horton".toCharArray());
+
+ AliasService as = EasyMock.createNiceMock(AliasService.class);
+ EasyMock.expect(as.getSigningKeyPassphrase()).andReturn("horton".toCharArray()).anyTimes();
+
+ EasyMock.replay(config, ms, as);
+
+ DefaultKeystoreService ks = new DefaultKeystoreService();
+ ks.setMasterService(ms);
+ ks.init(config, new HashMap<>());
+
+ tokenAuthority = new DefaultTokenAuthorityService();
+ tokenAuthority.setAliasService(as);
+ tokenAuthority.setKeystoreService(ks);
+ tokenAuthority.init(config, new HashMap<>());
+ tokenAuthority.start();
+
+ jwtAttributesForAdmin = new JWTokenAttributesBuilder()
+ .setIssuer("KNOXSSO")
+ .setUserName("admin")
+ .setAudiences(new ArrayList<>())
+ .setAlgorithm("RS256")
+ .setExpires(-1)
+ .setSigningKeystoreName(null)
+ .setSigningKeystoreAlias(null)
+ .setSigningKeystorePassphrase(null)
+ .build();
+ jwtAttributesForTom = new JWTokenAttributesBuilder()
+ .setIssuer("KNOXSSO")
+ .setUserName("tom")
+ .setAudiences(new ArrayList<>())
+ .setAlgorithm("RS256")
+ .setExpires(-1)
+ .setSigningKeystoreName(null)
+ .setSigningKeystoreAlias(null)
+ .setSigningKeystorePassphrase(null)
+ .build();
+ try {
+ adminToken1 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ adminToken2 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ adminToken3 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ adminToken4 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ adminToken5 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ adminToken6 = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ tomToken1 = tokenAuthority.issueToken(jwtAttributesForTom);
+ tomToken2 = tokenAuthority.issueToken(jwtAttributesForTom);
+ tomToken3 = tokenAuthority.issueToken(jwtAttributesForTom);
+ tomToken4 = tokenAuthority.issueToken(jwtAttributesForTom);
+ tomToken5 = tokenAuthority.issueToken(jwtAttributesForTom);
+ tomToken6 = tokenAuthority.issueToken(jwtAttributesForTom);
+ } catch (TokenServiceException ignored) {
+ }
+ }
+
+ private GatewayConfig mockConfig(Set<String> privilegedUsers, Set<String> nonPrivilegedUsers, int privilegedUsersLimit, int nonPrivilegedUsersLimit) {
+ GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
+ EasyMock.expect(config.getPrivilegedUsers()).andReturn(privilegedUsers);
+ EasyMock.expect(config.getNonPrivilegedUsers()).andReturn(nonPrivilegedUsers);
+ EasyMock.expect(config.getPrivilegedUsersConcurrentSessionLimit()).andReturn(privilegedUsersLimit);
+ EasyMock.expect(config.getNonPrivilegedUsersConcurrentSessionLimit()).andReturn(nonPrivilegedUsersLimit);
+ EasyMock.replay(config);
+ return config;
+ }
+
+ /**
+ * The goal for this test is to prove that if the user is not configured for either of the groups then
+ * neither of the limits apply to him, he can have unlimited sessions.
+ */
+ @Test
+ public void testUserIsInNeitherOfTheGroupsCanBeLoggedInUnlimitedTimes() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("guest")), 3, 2);
+ verifier.init(config, options);
+
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken3));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken4));
+ }
+
+ @Test
+ public void testUserIsInBothOfTheGroupsLowerLimitApplies() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
+ verifier.init(config, options);
+
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+ Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken3));
+
+ config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 4);
+ verifier.init(config, options);
+
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken3));
+ Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken4));
+ }
+
+ @Test
+ public void testUserIsPrivileged() throws ServiceLifecycleException, TokenServiceException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
+ verifier.init(config, options);
+
+ Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken1));
+ Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken2));
+ Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken3));
+ Assert.assertFalse(verifier.verifySessionForUser("admin", adminToken4));
+ verifier.sessionEndedForUser("admin", adminToken1.toString());
+ Assert.assertTrue(verifier.verifySessionForUser("admin", adminToken5));
+ Assert.assertFalse(verifier.verifySessionForUser("admin", adminToken6));
+ }
+
+ @Test
+ public void testUserIsNotPrivileged() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
+ verifier.init(config, options);
+
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken1));
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken2));
+ Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken3));
+ Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken4));
+ verifier.sessionEndedForUser("tom", tomToken1.toString());
+ Assert.assertTrue(verifier.verifySessionForUser("tom", tomToken5));
+ Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken6));
+ }
+
+ @Test
+ public void testPrivilegedLimitIsZero() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 0, 2);
+ verifier.init(config, options);
+
+ Assert.assertFalse(verifier.verifySessionForUser("admin", adminToken1));
+ }
+
+ @Test
+ public void testNonPrivilegedLimitIsZero() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 0);
+ verifier.init(config, options);
+
+ Assert.assertFalse(verifier.verifySessionForUser("tom", tomToken1));
+ }
+
+ @Test
+ public void testSessionsDoNotGoToNegative() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 2, 2);
+ verifier.init(config, options);
+
+ Assert.assertEquals(0, verifier.countValidTokensForUser("admin"));
+ verifier.verifySessionForUser("admin", adminToken1);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+ verifier.sessionEndedForUser("admin", adminToken1.toString());
+ Assert.assertEquals(0, verifier.countValidTokensForUser("admin"));
+ verifier.sessionEndedForUser("admin", adminToken1.toString());
+ Assert.assertEquals(0, verifier.countValidTokensForUser("admin"));
+ verifier.verifySessionForUser("admin", adminToken2);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+
+ Assert.assertEquals(0, verifier.countValidTokensForUser("tom"));
+ verifier.verifySessionForUser("tom", tomToken1);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+ verifier.sessionEndedForUser("tom", tomToken1.toString());
+ Assert.assertEquals(0, verifier.countValidTokensForUser("tom"));
+ verifier.sessionEndedForUser("tom", tomToken1.toString());
+ Assert.assertEquals(0, verifier.countValidTokensForUser("tom"));
+ verifier.verifySessionForUser("tom", tomToken2);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+ }
+
+ @Test
+ public void testNegativeLimitMeansUnlimited() throws ServiceLifecycleException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), -2, -2);
+ verifier.init(config, options);
+
+ for (int i = 0; i < 10; i++) {
+ try {
+ JWT token = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ Assert.assertTrue(verifier.verifySessionForUser("admin", token));
+ token = tokenAuthority.issueToken(jwtAttributesForTom);
+ Assert.assertTrue(verifier.verifySessionForUser("tom", token));
+ } catch (TokenServiceException ignored) {
+ }
+ }
+ }
+
+ @Test
+ public void testExpiredTokensAreNotCounted() throws ServiceLifecycleException, TokenServiceException, InterruptedException {
+ GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 3);
+ verifier.init(config, options);
+
+ JWTokenAttributes expiringJwtAttributesForTom = new JWTokenAttributesBuilder()
+ .setIssuer("KNOXSSO")
+ .setUserName("tom")
+ .setAudiences(new ArrayList<>())
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 1000)
+ .setSigningKeystoreName(null)
+ .setSigningKeystoreAlias(null)
+ .setSigningKeystorePassphrase(null)
+ .build();
+
+ JWT tomToken = tokenAuthority.issueToken(jwtAttributesForTom);
+ verifier.verifySessionForUser("tom", tomToken);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+ tomToken = tokenAuthority.issueToken(expiringJwtAttributesForTom);
+ verifier.verifySessionForUser("tom", tomToken);
+ Assert.assertEquals(2, verifier.countValidTokensForUser("tom"));
+ tomToken = tokenAuthority.issueToken(expiringJwtAttributesForTom);
+ verifier.verifySessionForUser("tom", tomToken);
+ Assert.assertEquals(3, verifier.countValidTokensForUser("tom"));
+ Thread.sleep(1000L);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("tom"));
+
+ JWTokenAttributes expiringJwtAttributesForAdmin = new JWTokenAttributesBuilder()
+ .setIssuer("KNOXSSO")
+ .setUserName("admin")
+ .setAudiences(new ArrayList<>())
+ .setAlgorithm("RS256")
+ .setExpires(System.currentTimeMillis() + 1000)
+ .setSigningKeystoreName(null)
+ .setSigningKeystoreAlias(null)
+ .setSigningKeystorePassphrase(null)
+ .build();
+
+ JWT adminToken = tokenAuthority.issueToken(jwtAttributesForAdmin);
+ verifier.verifySessionForUser("admin", adminToken);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+ adminToken = tokenAuthority.issueToken(expiringJwtAttributesForAdmin);
+ verifier.verifySessionForUser("admin", adminToken);
+ Assert.assertEquals(2, verifier.countValidTokensForUser("admin"));
+ adminToken = tokenAuthority.issueToken(expiringJwtAttributesForAdmin);
+ verifier.verifySessionForUser("admin", adminToken);
+ Assert.assertEquals(3, verifier.countValidTokensForUser("admin"));
+ Thread.sleep(1000L);
+ Assert.assertEquals(1, verifier.countValidTokensForUser("admin"));
+ }
+}
+
+
diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
index e48509a76..59af1c6d3 100644
--- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
+++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
@@ -58,6 +58,7 @@ import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
import org.apache.knox.gateway.services.security.token.TokenServiceException;
import org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.session.control.ConcurrentSessionVerifier;
import org.apache.knox.gateway.util.CookieUtils;
import org.apache.knox.gateway.util.RegExUtils;
import org.apache.knox.gateway.util.Urls;
@@ -215,6 +216,14 @@ public class WebSSOResource {
}
private Response getAuthenticationToken(int statusCode) {
+ if (!enableSession) {
+ // invalidate the session to avoid autologin
+ // Coverity CID 1352857
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ session.invalidate();
+ }
+ }
GatewayServices services =
(GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
boolean removeOriginalUrlCookie = true;
@@ -281,8 +290,12 @@ public class WebSSOResource {
JWT token = tokenAuthority.issueToken(jwtAttributes);
// Coverity CID 1327959
- if( token != null ) {
- addJWTHadoopCookie( original, token );
+ if (token != null) {
+ ConcurrentSessionVerifier verifier = services.getService(ServiceType.CONCURRENT_SESSION_VERIFIER);
+ if (verifier != null && !verifier.verifySessionForUser(p.getName(), token)) {
+ throw new WebApplicationException("Too many sessions for user: " + request.getUserPrincipal().getName(), Response.Status.FORBIDDEN);
+ }
+ addJWTHadoopCookie(original, token);
}
if (removeOriginalUrlCookie) {
@@ -308,14 +321,7 @@ public class WebSSOResource {
// todo log return error response
}
- if (!enableSession) {
- // invalidate the session to avoid autologin
- // Coverity CID 1352857
- HttpSession session = request.getSession(false);
- if( session != null ) {
- session.invalidate();
- }
- }
+
return Response.seeOther(location).entity("{ \"redirectTo\" : " + original + " }").build();
}
diff --git a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
index 407813a21..a3774fd77 100644
--- a/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
+++ b/gateway-service-knoxsso/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOResourceTest.java
@@ -18,6 +18,8 @@
package org.apache.knox.gateway.service.knoxsso;
import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE;
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.anyString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -69,6 +71,7 @@ import org.apache.knox.gateway.services.security.token.TokenServiceException;
import org.apache.knox.gateway.services.security.token.TokenUtils;
import org.apache.knox.gateway.services.security.token.impl.JWT;
import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.session.control.ConcurrentSessionVerifier;
import org.apache.knox.gateway.util.RegExUtils;
import org.easymock.EasyMock;
import org.junit.Assert;
@@ -86,6 +89,7 @@ public class WebSSOResourceTest {
private ServletContext context;
private HttpServletRequest request;
private JWTokenAuthority authority;
+ private ConcurrentSessionVerifier verifier;
CookieResponseWrapper responseWrapper;
@BeforeClass
@@ -142,14 +146,14 @@ public class WebSSOResourceTest {
}
private void configureCommonExpectations(Map<String, String> contextExpectations) throws Exception {
- configureCommonExpectations(contextExpectations, false, false);
+ configureCommonExpectations(contextExpectations, false, false, true);
}
private void configureCommonExpectations(Map<String, String> contextExpectations, boolean sslEnabled) throws Exception {
- configureCommonExpectations(contextExpectations, false, sslEnabled);
+ configureCommonExpectations(contextExpectations, false, sslEnabled, true);
}
- private void configureCommonExpectations(Map<String, String> contextExpectations, boolean useHmac, boolean sslEnabled) throws Exception {
+ private void configureCommonExpectations(Map<String, String> contextExpectations, boolean useHmac, boolean sslEnabled, boolean concurrentSessionVerifyResult) throws Exception {
context = EasyMock.createNiceMock(ServletContext.class);
contextExpectations.forEach((key, value) -> EasyMock.expect(context.getInitParameter(key)).andReturn(value).anyTimes());
@@ -178,7 +182,11 @@ public class WebSSOResourceTest {
ServletOutputStream outputStream = EasyMock.createNiceMock(ServletOutputStream.class);
responseWrapper = new CookieResponseWrapper(response, outputStream);
- EasyMock.replay(principal, services, context, request);
+ verifier = EasyMock.createNiceMock(ConcurrentSessionVerifier.class);
+ EasyMock.expect(verifier.verifySessionForUser(anyString(), anyObject())).andReturn(concurrentSessionVerifyResult).anyTimes();
+ EasyMock.expect(services.getService(ServiceType.CONCURRENT_SESSION_VERIFIER)).andReturn(verifier).anyTimes();
+
+ EasyMock.replay(principal, services, context, request, verifier);
}
@Test
@@ -271,7 +279,7 @@ public class WebSSOResourceTest {
private void testSignatureAlgorithm(boolean useHMAC) throws Exception {
final String algorithm = useHMAC ? "HS256" : "RS512";
- configureCommonExpectations(Collections.singletonMap("knoxsso.token.sigalg", algorithm), useHMAC, false);
+ configureCommonExpectations(Collections.singletonMap("knoxsso.token.sigalg", algorithm), useHMAC, false, true);
WebSSOResource webSSOResponse = new WebSSOResource();
webSSOResponse.request = request;
@@ -683,6 +691,19 @@ public class WebSSOResourceTest {
assertTrue(authority.verifyToken(parsedToken, customPublicKey));
}
+ @Test
+ public void testConcurrentSessionLimitHit() throws Exception {
+ configureCommonExpectations(Collections.emptyMap(), false, false, false);
+
+ WebSSOResource webSSOResponse = new WebSSOResource();
+ webSSOResponse.request = request;
+ webSSOResponse.response = responseWrapper;
+ webSSOResponse.context = context;
+ webSSOResponse.init();
+
+ Assert.assertThrows(WebApplicationException.class, webSSOResponse::doPost);
+ }
+
/**
* A wrapper for HttpServletResponseWrapper to store the cookies
*/
diff --git a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
index 768cf6d70..e94dab897 100644
--- a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
+++ b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
@@ -25,4 +25,7 @@ import org.apache.knox.gateway.i18n.messages.Messages;
public interface KnoxSSOutMessages {
@Message( level = MessageLevel.INFO, text = "There was a problem determining the SSO cookie domain - using default domain.")
void problemWithCookieDomainUsingDefault();
+
+ @Message(level = MessageLevel.WARN, text = "Could not find cookie with the name: {0} in the request to be removed from the concurrent session counter for user: {1}. ")
+ void couldNotFindCookieWithTokenToRemove(String cookieName, String username);
}
\ No newline at end of file
diff --git a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java
index 623274ada..134972c15 100644
--- a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java
+++ b/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResource.java
@@ -18,6 +18,9 @@
package org.apache.knox.gateway.service.knoxsso;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.session.control.ConcurrentSessionVerifier;
import org.apache.knox.gateway.util.Urls;
import javax.annotation.PostConstruct;
@@ -32,6 +35,8 @@ import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import java.net.MalformedURLException;
+import java.util.Arrays;
+import java.util.Optional;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static javax.ws.rs.core.MediaType.APPLICATION_XML;
@@ -106,6 +111,28 @@ public class WebSSOutResource {
}
response.addCookie(c);
+ Optional<Cookie> ssoCookie = findCookie(cookieName);
+ if (ssoCookie.isPresent()) {
+ GatewayServices gwServices =
+ (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+ if (gwServices != null) {
+ ConcurrentSessionVerifier verifier = gwServices.getService(ServiceType.CONCURRENT_SESSION_VERIFIER);
+ if (verifier != null) {
+ verifier.sessionEndedForUser(request.getUserPrincipal().getName(), ssoCookie.get().getValue());
+ }
+ }
+ } else {
+ log.couldNotFindCookieWithTokenToRemove(cookieName, request.getUserPrincipal().getName());
+ }
return rc;
}
+
+ private Optional<Cookie> findCookie(String cookieName) {
+ Cookie[] cookies = request.getCookies();
+ if (cookies != null) {
+ return Arrays.stream(cookies).filter(cookie -> cookie.getName().equals(cookieName)).findFirst();
+ } else {
+ return Optional.empty();
+ }
+ }
}
diff --git a/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java b/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java
index 0e973b161..8d262f315 100644
--- a/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java
+++ b/gateway-service-knoxssout/src/test/java/org/apache/knox/gateway/service/knoxsso/WebSSOutResourceTest.java
@@ -26,6 +26,7 @@ import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
+import java.security.Principal;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@@ -47,12 +48,15 @@ public class WebSSOutResourceTest {
HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
EasyMock.expect(request.getRequestURL()).andReturn(new StringBuffer(""));
+ Principal mockPrincipal = EasyMock.createNiceMock(Principal.class);
+ EasyMock.expect(mockPrincipal.getName()).andReturn("admin").anyTimes();
+ EasyMock.expect(request.getUserPrincipal()).andReturn(mockPrincipal).anyTimes();
HttpServletResponse response = EasyMock.createNiceMock(HttpServletResponse.class);
ServletOutputStream outputStream = EasyMock.createNiceMock(ServletOutputStream.class);
CookieResponseWrapper responseWrapper = new CookieResponseWrapper(response, outputStream);
- EasyMock.replay(context, request);
+ EasyMock.replay(context, request, mockPrincipal);
WebSSOutResource webSSOutResponse = new WebSSOutResource();
webSSOutResponse.request = request;
diff --git a/gateway-spi-common/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java b/gateway-spi-common/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
deleted file mode 100644
index e06633ff6..000000000
--- a/gateway-spi-common/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.knox.gateway.session.control;
-
-
-import org.apache.knox.gateway.config.GatewayConfig;
-
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-public class ConcurrentSessionVerifier {
- public static final ConcurrentSessionVerifier INSTANCE = new ConcurrentSessionVerifier();
- private Set<String> privilegedUsers;
- private Set<String> nonPrivilegedUsers;
- private int privilegedUserConcurrentSessionLimit;
- private int nonPrivilegedUserConcurrentSessionLimit;
- private Map<String, Integer> concurrentSessionCounter;
- private final Lock sessionCountModifyLock = new ReentrantLock();
-
- private ConcurrentSessionVerifier() {
- }
-
- public static ConcurrentSessionVerifier getInstance() {
- return INSTANCE;
- }
-
- public void init(GatewayConfig config) {
- this.privilegedUsers = config.getPrivilegedUsers();
- this.nonPrivilegedUsers = config.getNonPrivilegedUsers();
- this.privilegedUserConcurrentSessionLimit = config.getPrivilegedUsersConcurrentSessionLimit();
- this.nonPrivilegedUserConcurrentSessionLimit = config.getNonPrivilegedUsersConcurrentSessionLimit();
- this.concurrentSessionCounter = new ConcurrentHashMap<>();
- }
-
- public boolean verifySessionForUser(String username) {
- if (!privilegedUsers.contains(username) && !nonPrivilegedUsers.contains(username)) {
- return true;
- }
-
- sessionCountModifyLock.lock();
- try {
- concurrentSessionCounter.putIfAbsent(username, 0);
- if (privilegedUserCheckLimitReached(username) || nonPrivilegedUserCheckLimitReached(username)) {
- return false;
- }
- concurrentSessionCounter.compute(username, (key, value) -> value + 1);
- } finally {
- sessionCountModifyLock.unlock();
- }
- return true;
- }
-
- private boolean privilegedUserCheckLimitReached(String username) {
- if (privilegedUserConcurrentSessionLimit < 0) {
- return false;
- }
- return privilegedUsers.contains(username) && (concurrentSessionCounter.get(username) >= privilegedUserConcurrentSessionLimit);
- }
-
- private boolean nonPrivilegedUserCheckLimitReached(String username) {
- if (nonPrivilegedUserConcurrentSessionLimit < 0) {
- return false;
- }
- return nonPrivilegedUsers.contains(username) && (concurrentSessionCounter.get(username) >= nonPrivilegedUserConcurrentSessionLimit);
- }
-
- public void sessionEndedForUser(String username) {
- sessionCountModifyLock.lock();
- try {
- concurrentSessionCounter.computeIfPresent(username, (key, counter) -> decreaseCounter(counter));
- } finally {
- sessionCountModifyLock.unlock();
- }
- }
-
- private Integer decreaseCounter(Integer counter) {
- counter--;
- if (counter < 1) {
- return null;
- } else {
- return counter;
- }
- }
-
- Integer getUserConcurrentSessionCount(String username) {
- return concurrentSessionCounter.get(username);
- }
-}
diff --git a/gateway-spi-common/src/test/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifierTest.java b/gateway-spi-common/src/test/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifierTest.java
deleted file mode 100644
index d70eca356..000000000
--- a/gateway-spi-common/src/test/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifierTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.knox.gateway.session.control;
-
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.easymock.EasyMock;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-
-public class ConcurrentSessionVerifierTest {
-
- private ConcurrentSessionVerifier verifier;
-
- @Before
- public void setUp() {
- verifier = ConcurrentSessionVerifier.getInstance();
- }
-
- private GatewayConfig mockConfig(Set<String> privilegedUsers, Set<String> nonPrivilegedUsers, int privilegedUsersLimit, int nonPrivilegedUsersLimit) {
- GatewayConfig config = EasyMock.createNiceMock(GatewayConfig.class);
- EasyMock.expect(config.getPrivilegedUsers()).andReturn(privilegedUsers);
- EasyMock.expect(config.getNonPrivilegedUsers()).andReturn(nonPrivilegedUsers);
- EasyMock.expect(config.getPrivilegedUsersConcurrentSessionLimit()).andReturn(privilegedUsersLimit);
- EasyMock.expect(config.getNonPrivilegedUsersConcurrentSessionLimit()).andReturn(nonPrivilegedUsersLimit);
- EasyMock.replay(config);
- return config;
- }
-
-
- @Test
- public void userIsInNeitherOfTheGroups() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
- verifier.init(config);
- for (int i = 0; i < 4; i++) {
- Assert.assertTrue(verifier.verifySessionForUser("sam"));
- }
- }
-
- @Test
- public void userIsInBothOfTheGroups() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
- verifier.init(config);
-
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
-
- config = mockConfig(new HashSet<>(Arrays.asList("admin", "tom")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 4);
- verifier.init(config);
-
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
- }
-
- @Test
- public void userIsPrivileged() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
- verifier.init(config);
-
- Assert.assertTrue(verifier.verifySessionForUser("admin"));
- Assert.assertTrue(verifier.verifySessionForUser("admin"));
- Assert.assertTrue(verifier.verifySessionForUser("admin"));
- Assert.assertFalse(verifier.verifySessionForUser("admin"));
- Assert.assertFalse(verifier.verifySessionForUser("admin"));
- verifier.sessionEndedForUser("admin");
- Assert.assertTrue(verifier.verifySessionForUser("admin"));
- Assert.assertFalse(verifier.verifySessionForUser("admin"));
- Assert.assertFalse(verifier.verifySessionForUser("admin"));
- }
-
- @Test
- public void userIsNotPrivileged() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 2);
- verifier.init(config);
-
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
- verifier.sessionEndedForUser("tom");
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
- }
-
- @Test
- public void privilegedLimitIsZero() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 0, 2);
- verifier.init(config);
-
- Assert.assertFalse(verifier.verifySessionForUser("admin"));
- }
-
- @Test
- public void nonPrivilegedLimitIsZero() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 3, 0);
- verifier.init(config);
-
- Assert.assertFalse(verifier.verifySessionForUser("tom"));
- }
-
- @Test
- public void sessionsDoNotGoToNegative() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), 2, 2);
- verifier.init(config);
-
- Assert.assertNull(verifier.getUserConcurrentSessionCount("admin"));
- verifier.verifySessionForUser("admin");
- Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("admin").intValue());
- verifier.sessionEndedForUser("admin");
- Assert.assertNull(verifier.getUserConcurrentSessionCount("admin"));
- verifier.sessionEndedForUser("admin");
- Assert.assertNull(verifier.getUserConcurrentSessionCount("admin"));
- verifier.verifySessionForUser("admin");
- Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("admin").intValue());
-
- Assert.assertNull(verifier.getUserConcurrentSessionCount("tom"));
- verifier.verifySessionForUser("tom");
- Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("tom").intValue());
- verifier.sessionEndedForUser("tom");
- Assert.assertNull(verifier.getUserConcurrentSessionCount("tom"));
- verifier.sessionEndedForUser("tom");
- Assert.assertNull(verifier.getUserConcurrentSessionCount("tom"));
- verifier.verifySessionForUser("tom");
- Assert.assertEquals(1, verifier.getUserConcurrentSessionCount("tom").intValue());
- }
-
- @Test
- public void negativeLimitMeansUnlimited() {
- GatewayConfig config = mockConfig(new HashSet<>(Arrays.asList("admin")), new HashSet<>(Arrays.asList("tom", "guest")), -2, -2);
- verifier.init(config);
-
- for (int i = 0; i < 10; i++) {
- Assert.assertTrue(verifier.verifySessionForUser("admin"));
- Assert.assertTrue(verifier.verifySessionForUser("tom"));
- }
- }
-}
-
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
index 38767f86e..14a1da5d8 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
@@ -35,7 +35,8 @@ public enum ServiceType {
SSL_SERVICE("SSLService"),
TOKEN_SERVICE("TokenService"),
TOKEN_STATE_SERVICE("TokenStateService"),
- TOPOLOGY_SERVICE("TopologyService");
+ TOPOLOGY_SERVICE("TopologyService"),
+ CONCURRENT_SESSION_VERIFIER("ConcurrentSessionCounter");
private final String serviceTypeName;
private final String shortName;
diff --git a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java b/gateway-spi/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
similarity index 53%
copy from gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
copy to gateway-spi/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
index 768cf6d70..f421f9c2c 100644
--- a/gateway-service-knoxssout/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOutMessages.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/session/control/ConcurrentSessionVerifier.java
@@ -15,14 +15,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.knox.gateway.service.knoxsso;
+package org.apache.knox.gateway.session.control;
-import org.apache.knox.gateway.i18n.messages.Message;
-import org.apache.knox.gateway.i18n.messages.MessageLevel;
-import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
-@Messages(logger="org.apache.knox.gateway.service.knoxsso")
-public interface KnoxSSOutMessages {
- @Message( level = MessageLevel.INFO, text = "There was a problem determining the SSO cookie domain - using default domain.")
- void problemWithCookieDomainUsingDefault();
-}
\ No newline at end of file
+public interface ConcurrentSessionVerifier extends Service {
+ /**
+ * Verifies whether the given user is permitted to have a[nother] session or not.
+ *
+ * @param username the user who needs verification
+ * @param JWToken the token which the user will use in the session
+ * @return true if the user is allowed to have a[nother] session, false if the user is not allowed to have a[nother] session
+ */
+ boolean verifySessionForUser(String username, JWT JWToken);
+
+ void sessionEndedForUser(String username, String token);
+
+}