]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13327 Fix SSF-107
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 15 Jun 2020 16:19:02 +0000 (18:19 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 15 Jun 2020 20:05:16 +0000 (20:05 +0000)
* SONAR-13327 Create 'SAML_MESSAGE_IDS' table and DAO
* SONAR-13327 Check SAML Message id not already exist during auth
* SONAR-13327 Clean expired SAML Message ids daily

29 files changed:
server/sonar-auth-saml/build.gradle
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlIdentityProvider.java
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java [new file with mode: 0644]
server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlModule.java
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlIdentityProviderTest.java
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java [new file with mode: 0644]
server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlModuleTest.java
server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdDao.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdMapper.java [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/user/SamlMessageIdMapper.xml [new file with mode: 0644]
server/sonar-db-dao/src/schema/schema-sq.ddl
server/sonar-db-dao/src/test/java/org/sonar/db/user/SamlMessageIdDaoTest.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSamlMessageIdsTable.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/DbVersion84.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSamlMessageIdsTableTest.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/AuthenticationModule.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleaner.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerExecutorService.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerExecutorServiceImpl.java [new file with mode: 0644]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java [deleted file]
server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java [deleted file]
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerTest.java [new file with mode: 0644]
server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java [deleted file]

index 5651516b7b6f64f97f74347d58da7f9dc015a899..dd9d2341cb1919d2ecbbf28ad6ba27dec7616d8c 100644 (file)
@@ -17,10 +17,12 @@ dependencies {
     compileOnly 'com.google.code.findbugs:jsr305'
     compileOnly 'com.squareup.okhttp3:okhttp'
     compileOnly 'javax.servlet:javax.servlet-api'
+    compileOnly project(':server:sonar-db-dao')
     compileOnly project(':sonar-core')
 
     testCompile 'com.tngtech.java:junit-dataprovider'
     testCompile 'junit:junit'
     testCompile 'org.assertj:assertj-core'
     testCompile 'org.mockito:mockito-core'
+    testCompile testFixtures(project(':server:sonar-db-dao'))
 }
index 692e02506693fb798c9d08ad9d417f27240edeb5..f8033bbfc5618dea4753e6c0a9838286d6f8d22a 100644 (file)
@@ -56,9 +56,11 @@ public class SamlIdentityProvider implements OAuth2IdentityProvider {
   private static final String STATE_REQUEST_PARAMETER = "RelayState";
 
   private final SamlSettings samlSettings;
+  private final SamlMessageIdChecker samlMessageIdChecker;
 
-  public SamlIdentityProvider(SamlSettings samlSettings) {
+  public SamlIdentityProvider(SamlSettings samlSettings, SamlMessageIdChecker samlMessageIdChecker) {
     this.samlSettings = samlSettings;
+    this.samlMessageIdChecker = samlMessageIdChecker;
   }
 
   @Override
@@ -107,6 +109,7 @@ public class SamlIdentityProvider implements OAuth2IdentityProvider {
 
     LOGGER.trace("Name ID : {}", auth.getNameId());
     checkAuthentication(auth);
+    samlMessageIdChecker.check(auth);
 
     LOGGER.trace("Attributes received : {}", auth.getAttributes());
     String login = getNonNullFirstAttribute(auth, samlSettings.getUserLogin());
diff --git a/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java b/server/sonar-auth-saml/src/main/java/org/sonar/auth/saml/SamlMessageIdChecker.java
new file mode 100644 (file)
index 0000000..f913b3d
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.auth.saml;
+
+import com.onelogin.saml2.Auth;
+import org.joda.time.Instant;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.SamlMessageIdDto;
+
+import static java.util.Objects.requireNonNull;
+
+@ServerSide
+public class SamlMessageIdChecker {
+
+  private final DbClient dbClient;
+
+  public SamlMessageIdChecker(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  public void check(Auth auth) {
+    String messageId = requireNonNull(auth.getLastMessageId(), "Message ID is missing");
+    Instant lastAssertionNotOnOrAfter = auth.getLastAssertionNotOnOrAfter().stream()
+      .sorted()
+      .findFirst()
+      .orElseThrow(() -> new IllegalArgumentException("Missing NotOnOrAfter element"));
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      dbClient.samlMessageIdDao().selectByMessageId(dbSession, messageId)
+        .ifPresent(m -> {
+          throw new IllegalArgumentException("This message has already been processed");
+        });
+      dbClient.samlMessageIdDao().insert(dbSession, new SamlMessageIdDto()
+        .setMessageId(messageId)
+        .setExpirationDate(lastAssertionNotOnOrAfter.getMillis()));
+      dbSession.commit();
+    }
+  }
+
+}
index 63f9732870d96a36c35ade313755cf75a92f81ec..aecf19200ea659330a96c5a78a43a61f9f55dd09 100644 (file)
@@ -29,6 +29,7 @@ public class SamlModule extends Module {
   protected void configureModule() {
     add(
       SamlIdentityProvider.class,
+      SamlMessageIdChecker.class,
       SamlSettings.class);
     List<PropertyDefinition> definitions = SamlSettings.definitions();
     add(definitions.toArray(new Object[definitions.size()]));
index 58f3e87a0ab6ba1fa5f79372ee153013b6de2d28..3bd7cd2b85ce679fa5f419921608fc4fbb11e028 100644 (file)
@@ -30,15 +30,16 @@ import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.io.IOUtils;
 import org.junit.Rule;
 import org.junit.Test;
-import org.junit.rules.ExpectedException;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.server.authentication.OAuth2IdentityProvider;
 import org.sonar.api.server.authentication.UnauthorizedException;
 import org.sonar.api.server.authentication.UserIdentity;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
@@ -47,11 +48,13 @@ import static org.mockito.Mockito.when;
 public class SamlIdentityProviderTest {
 
   @Rule
-  public ExpectedException expectedException = ExpectedException.none();
+  public DbTester db = DbTester.create();
 
   private MapSettings settings = new MapSettings(new PropertyDefinitions(System2.INSTANCE, SamlSettings.definitions()));
 
-  private SamlIdentityProvider underTest = new SamlIdentityProvider(new SamlSettings(settings.asConfig()));
+  private SamlMessageIdChecker samlMessageIdChecker = mock(SamlMessageIdChecker.class);
+
+  private SamlIdentityProvider underTest = new SamlIdentityProvider(new SamlSettings(settings.asConfig()), new SamlMessageIdChecker(db.getDbClient()));
 
   @Test
   public void check_fields() {
@@ -98,10 +101,9 @@ public class SamlIdentityProviderTest {
     settings.setProperty("sonar.auth.saml.loginUrl", "invalid");
     DumbInitContext context = new DumbInitContext();
 
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Fail to create Auth");
-
-    underTest.init(context);
+    assertThatThrownBy(() -> underTest.init(context))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Fail to create Auth");
   }
 
   @Test
@@ -159,10 +161,10 @@ public class SamlIdentityProviderTest {
     setSettings(true);
     DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_login.txt");
 
-    expectedException.expect(NullPointerException.class);
-    expectedException.expectMessage("login is missing");
+    assertThatThrownBy(() -> underTest.callback(callbackContext))
+      .isInstanceOf(NullPointerException.class)
+      .hasMessage("login is missing");
 
-    underTest.callback(callbackContext);
   }
 
   @Test
@@ -170,10 +172,9 @@ public class SamlIdentityProviderTest {
     setSettings(true);
     DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_response_without_name.txt");
 
-    expectedException.expect(NullPointerException.class);
-    expectedException.expectMessage("name is missing");
-
-    underTest.callback(callbackContext);
+    assertThatThrownBy(() -> underTest.callback(callbackContext))
+      .isInstanceOf(NullPointerException.class)
+      .hasMessage("name is missing");
   }
 
   @Test
@@ -182,10 +183,9 @@ public class SamlIdentityProviderTest {
     settings.setProperty("sonar.auth.saml.certificate.secured", "invalid");
     DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
 
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Fail to create Auth");
-
-    underTest.callback(callbackContext);
+    assertThatThrownBy(() -> underTest.callback(callbackContext))
+      .isInstanceOf(IllegalStateException.class)
+      .hasMessage("Fail to create Auth");
   }
 
   @Test
@@ -218,10 +218,21 @@ public class SamlIdentityProviderTest {
       "-----END CERTIFICATE-----\n");
     DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_full_response.txt");
 
-    expectedException.expect(UnauthorizedException.class);
-    expectedException.expectMessage("Signature validation failed. SAML Response rejected");
+    assertThatThrownBy(() -> underTest.callback(callbackContext))
+      .isInstanceOf(UnauthorizedException.class)
+      .hasMessage("Signature validation failed. SAML Response rejected");
+  }
+
+  @Test
+  public void fail_callback_when_message_was_already_sent() {
+    setSettings(true);
+    DumbCallbackContext callbackContext = new DumbCallbackContext("encoded_minimal_response.txt");
 
     underTest.callback(callbackContext);
+
+    assertThatThrownBy(() -> underTest.callback(callbackContext))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("This message has already been processed");
   }
 
   private void setSettings(boolean enabled) {
diff --git a/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java b/server/sonar-auth-saml/src/test/java/org/sonar/auth/saml/SamlMessageIdCheckerTest.java
new file mode 100644 (file)
index 0000000..f565745
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.auth.saml;
+
+import com.google.common.collect.ImmutableList;
+import com.onelogin.saml2.Auth;
+import java.util.Arrays;
+import org.joda.time.Instant;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.SamlMessageIdDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SamlMessageIdCheckerTest {
+
+  @Rule
+  public DbTester db = DbTester.create();
+
+  private DbSession dbSession = db.getSession();
+
+  private Auth auth = mock(Auth.class);
+
+  private SamlMessageIdChecker underTest = new SamlMessageIdChecker(db.getDbClient());
+
+  @Test
+  public void check_do_not_fail_when_message_id_is_new_and_insert_saml_message_in_db() {
+    db.getDbClient().samlMessageIdDao().insert(dbSession, new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L));
+    db.commit();
+    when(auth.getLastMessageId()).thenReturn("MESSAGE_2");
+    when(auth.getLastAssertionNotOnOrAfter()).thenReturn(ImmutableList.of(Instant.ofEpochMilli(10_000_000_000L)));
+
+    assertThatCode(() -> underTest.check(auth)).doesNotThrowAnyException();
+
+    SamlMessageIdDto result = db.getDbClient().samlMessageIdDao().selectByMessageId(dbSession, "MESSAGE_2").get();
+    assertThat(result.getMessageId()).isEqualTo("MESSAGE_2");
+    assertThat(result.getExpirationDate()).isEqualTo(10_000_000_000L);
+  }
+
+  @Test
+  public void check_fails_when_message_id_already_exist() {
+    db.getDbClient().samlMessageIdDao().insert(dbSession, new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L));
+    db.commit();
+    when(auth.getLastMessageId()).thenReturn("MESSAGE_1");
+    when(auth.getLastAssertionNotOnOrAfter()).thenReturn(ImmutableList.of(Instant.ofEpochMilli(10_000_000_000L)));
+
+    assertThatThrownBy(() -> underTest.check(auth))
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessageContaining("This message has already been processed");
+  }
+
+  @Test
+  public void check_insert_message_id_using_oldest_NotOnOrAfter_value() {
+    db.getDbClient().samlMessageIdDao().insert(dbSession, new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(1_000_000_000L));
+    db.commit();
+    when(auth.getLastMessageId()).thenReturn("MESSAGE_2");
+    when(auth.getLastAssertionNotOnOrAfter())
+      .thenReturn(Arrays.asList(Instant.ofEpochMilli(10_000_000_000L), Instant.ofEpochMilli(30_000_000_000L), Instant.ofEpochMilli(20_000_000_000L)));
+
+    assertThatCode(() -> underTest.check(auth)).doesNotThrowAnyException();
+
+    SamlMessageIdDto result = db.getDbClient().samlMessageIdDao().selectByMessageId(dbSession, "MESSAGE_2").get();
+    assertThat(result.getMessageId()).isEqualTo("MESSAGE_2");
+    assertThat(result.getExpirationDate()).isEqualTo(10_000_000_000L);
+  }
+}
index 2add1d96f882720ed1c57b4eec65c1a61c3c95f5..01aeba26b72e7a8cf6ae8bdbc34d943d92643c2c 100644 (file)
@@ -31,6 +31,6 @@ public class SamlModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new SamlModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 12);
+    assertThat(container.size()).isGreaterThan(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER);
   }
 }
index 5c899412dca83097560875b78473bf92c9d4fd02..b99c49f65362cfef2ca8498ff662202d4dbba36b 100644 (file)
@@ -106,6 +106,7 @@ public final class SqTables {
     "qprofile_edit_users",
     "quality_gates",
     "quality_gate_conditions",
+    "saml_message_ids",
     "rules",
     "rules_metadata",
     "rules_parameters",
index bba7ecd27312963ba891a65d48142fe5c684ea14..aa3ffa7667daa1398700056f7216af882446620a 100644 (file)
@@ -84,6 +84,7 @@ import org.sonar.db.source.FileSourceDao;
 import org.sonar.db.user.GroupDao;
 import org.sonar.db.user.GroupMembershipDao;
 import org.sonar.db.user.RoleDao;
+import org.sonar.db.user.SamlMessageIdDao;
 import org.sonar.db.user.SessionTokensDao;
 import org.sonar.db.user.UserDao;
 import org.sonar.db.user.UserGroupDao;
@@ -155,6 +156,7 @@ public class DaoModule extends Module {
     RoleDao.class,
     RuleDao.class,
     RuleRepositoryDao.class,
+    SamlMessageIdDao.class,
     SnapshotDao.class,
     SchemaMigrationDao.class,
     SessionTokensDao.class,
index 2d3f79aad578efb77ee7a898bfbcf0bcb3edf9ef..24718f69174dfdb563270e455d13aa18c6157a22 100644 (file)
@@ -82,6 +82,7 @@ import org.sonar.db.source.FileSourceDao;
 import org.sonar.db.user.GroupDao;
 import org.sonar.db.user.GroupMembershipDao;
 import org.sonar.db.user.RoleDao;
+import org.sonar.db.user.SamlMessageIdDao;
 import org.sonar.db.user.SessionTokensDao;
 import org.sonar.db.user.UserDao;
 import org.sonar.db.user.UserGroupDao;
@@ -164,6 +165,7 @@ public class DbClient {
   private final NewCodePeriodDao newCodePeriodDao;
   private final ProjectDao projectDao;
   private final SessionTokensDao sessionTokensDao;
+  private final SamlMessageIdDao samlMessageIdDao;
 
   public DbClient(Database database, MyBatis myBatis, DBSessions dbSessions, Dao... daos) {
     this.database = database;
@@ -242,6 +244,7 @@ public class DbClient {
     newCodePeriodDao = getDao(map, NewCodePeriodDao.class);
     projectDao = getDao(map, ProjectDao.class);
     sessionTokensDao = getDao(map, SessionTokensDao.class);
+    samlMessageIdDao = getDao(map, SamlMessageIdDao.class);
   }
 
   public DbSession openSession(boolean batch) {
@@ -534,4 +537,8 @@ public class DbClient {
     return sessionTokensDao;
   }
 
+  public SamlMessageIdDao samlMessageIdDao() {
+    return samlMessageIdDao;
+  }
+
 }
index ad5d5310b2f883571305d47d89ffe74eb1f10b5a..8d31e0f973577d8616831a3c9ea69dbf8a29f856 100644 (file)
@@ -140,6 +140,7 @@ import org.sonar.db.user.GroupMapper;
 import org.sonar.db.user.GroupMembershipDto;
 import org.sonar.db.user.GroupMembershipMapper;
 import org.sonar.db.user.RoleMapper;
+import org.sonar.db.user.SamlMessageIdMapper;
 import org.sonar.db.user.SessionTokenMapper;
 import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserGroupDto;
@@ -286,6 +287,7 @@ public class MyBatis implements Startable {
       RoleMapper.class,
       RuleMapper.class,
       RuleRepositoryMapper.class,
+      SamlMessageIdMapper.class,
       SchemaMigrationMapper.class,
       SessionTokenMapper.class,
       SnapshotMapper.class,
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdDao.java
new file mode 100644 (file)
index 0000000..3cb6729
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import java.util.Optional;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Dao;
+import org.sonar.db.DbSession;
+
+public class SamlMessageIdDao implements Dao {
+
+  private final System2 system2;
+  private final UuidFactory uuidFactory;
+
+  public SamlMessageIdDao(System2 system2, UuidFactory uuidFactory) {
+    this.system2 = system2;
+    this.uuidFactory = uuidFactory;
+  }
+
+  public Optional<SamlMessageIdDto> selectByMessageId(DbSession session, String messageId) {
+    return Optional.ofNullable(mapper(session).selectByMessageId(messageId));
+  }
+
+  public SamlMessageIdDto insert(DbSession session, SamlMessageIdDto dto) {
+    long now = system2.now();
+    mapper(session).insert(dto
+      .setUuid(uuidFactory.create())
+      .setCreatedAt(now));
+    return dto;
+  }
+
+  public int deleteExpired(DbSession dbSession) {
+    return mapper(dbSession).deleteExpired(system2.now());
+  }
+
+  private static SamlMessageIdMapper mapper(DbSession session) {
+    return session.getMapper(SamlMessageIdMapper.class);
+  }
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdDto.java
new file mode 100644 (file)
index 0000000..89b00ab
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+public class SamlMessageIdDto {
+
+  private String uuid;
+
+  /**
+   * Message ID from the SAML response received during authentication.
+   */
+  private String messageId;
+
+  /**
+   * Expiration date is coming from the NotOnOrAfter attribute of the SAML response.
+   *
+   * A row that contained an expired date can be safely deleted from database.
+   */
+  private long expirationDate;
+
+  private long createdAt;
+
+  public String getUuid() {
+    return uuid;
+  }
+
+  SamlMessageIdDto setUuid(String uuid) {
+    this.uuid = uuid;
+    return this;
+  }
+
+  public String getMessageId() {
+    return messageId;
+  }
+
+  public SamlMessageIdDto setMessageId(String messageId) {
+    this.messageId = messageId;
+    return this;
+  }
+
+  public long getExpirationDate() {
+    return expirationDate;
+  }
+
+  public SamlMessageIdDto setExpirationDate(long expirationDate) {
+    this.expirationDate = expirationDate;
+    return this;
+  }
+
+  public long getCreatedAt() {
+    return createdAt;
+  }
+
+  SamlMessageIdDto setCreatedAt(long createdAt) {
+    this.createdAt = createdAt;
+    return this;
+  }
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/SamlMessageIdMapper.java
new file mode 100644 (file)
index 0000000..0cd1a0a
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import javax.annotation.CheckForNull;
+import org.apache.ibatis.annotations.Param;
+
+public interface SamlMessageIdMapper {
+
+  @CheckForNull
+  SamlMessageIdDto selectByMessageId(String messageId);
+
+  void insert(@Param("dto") SamlMessageIdDto dto);
+
+  int deleteExpired(@Param("now") long now);
+
+}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SamlMessageIdMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/SamlMessageIdMapper.xml
new file mode 100644 (file)
index 0000000..5e090f3
--- /dev/null
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.db.user.SamlMessageIdMapper">
+
+  <sql id="columns">
+    smi.uuid as uuid,
+    smi.message_id as "messageId",
+    smi.expiration_date as "expirationDate",
+    smi.created_at as "createdAt"
+  </sql>
+
+  <select id="selectByMessageId" parameterType="String" resultType="org.sonar.db.user.SamlMessageIdDto">
+    select
+    <include refid="columns"/>
+    from saml_message_ids smi
+    where smi.message_id=#{messageId, jdbcType=VARCHAR}
+  </select>
+
+  <insert id="insert" parameterType="Map" useGeneratedKeys="false">
+    insert into saml_message_ids
+    (
+      uuid,
+      message_id,
+      expiration_date,
+      created_at
+    )
+    values (
+      #{dto.uuid, jdbcType=VARCHAR},
+      #{dto.messageId, jdbcType=VARCHAR},
+      #{dto.expirationDate, jdbcType=BIGINT},
+      #{dto.createdAt, jdbcType=BIGINT}
+    )
+  </insert>
+
+  <delete id="deleteExpired" parameterType="Long" >
+    delete from saml_message_ids where expiration_date &lt; #{now, jdbcType=BIGINT}
+  </delete>
+
+</mapper>
index 1bc82047b1ee28de1d3f4199252767e3cede44bb..106647743edcd88967a5249f88e43326ff9e7941 100644 (file)
@@ -871,6 +871,15 @@ CREATE TABLE "RULES_PROFILES"(
 );
 ALTER TABLE "RULES_PROFILES" ADD CONSTRAINT "PK_RULES_PROFILES" PRIMARY KEY("UUID");
 
+CREATE TABLE "SAML_MESSAGE_IDS"(
+    "UUID" VARCHAR(40) NOT NULL,
+    "MESSAGE_ID" VARCHAR(255) NOT NULL,
+    "EXPIRATION_DATE" BIGINT NOT NULL,
+    "CREATED_AT" BIGINT NOT NULL
+);
+ALTER TABLE "SAML_MESSAGE_IDS" ADD CONSTRAINT "PK_SAML_MESSAGE_IDS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "SAML_MESSAGE_IDS_UNIQUE" ON "SAML_MESSAGE_IDS"("MESSAGE_ID");
+
 CREATE TABLE "SESSION_TOKENS"(
     "UUID" VARCHAR(40) NOT NULL,
     "USER_UUID" VARCHAR(255) NOT NULL,
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/user/SamlMessageIdDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/user/SamlMessageIdDaoTest.java
new file mode 100644 (file)
index 0000000..f7dd0b6
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.db.user;
+
+import java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.core.util.SequenceUuidFactory;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SamlMessageIdDaoTest {
+
+  private static final long NOW = 1_000_000_000L;
+
+  private TestSystem2 system2 = new TestSystem2().setNow(NOW);
+  @Rule
+  public DbTester db = DbTester.create(system2);
+
+  private DbSession dbSession = db.getSession();
+  private UuidFactory uuidFactory = new SequenceUuidFactory();
+
+  private SamlMessageIdDao underTest = new SamlMessageIdDao(system2, uuidFactory);
+
+  @Test
+  public void selectByMessageId() {
+    SamlMessageIdDto dto = new SamlMessageIdDto()
+      .setMessageId("ABCD")
+      .setExpirationDate(15_000_000_000L);
+    underTest.insert(dbSession, dto);
+
+    Optional<SamlMessageIdDto> result = underTest.selectByMessageId(dbSession, dto.getMessageId());
+
+    assertThat(result).isPresent();
+    assertThat(result.get().getMessageId()).isEqualTo("ABCD");
+    assertThat(result.get().getExpirationDate()).isEqualTo(15_000_000_000L);
+    assertThat(result.get().getCreatedAt()).isEqualTo(NOW);
+  }
+
+  @Test
+  public void uuid_created_at_and_updated_at_are_ignored_during_insert() {
+    SamlMessageIdDto dto = new SamlMessageIdDto()
+      .setMessageId("ABCD")
+      .setExpirationDate(15_000_000_000L)
+      // Following fields should be ignored
+      .setUuid("SHOULD_NOT_BE_USED")
+      .setCreatedAt(8_000_000_000L);
+    underTest.insert(dbSession, dto);
+
+    Optional<SamlMessageIdDto> result = underTest.selectByMessageId(dbSession, dto.getMessageId());
+
+    assertThat(result).isPresent();
+    assertThat(result.get().getUuid()).isNotEqualTo("SHOULD_NOT_BE_USED");
+    assertThat(result.get().getCreatedAt()).isEqualTo(NOW);
+  }
+
+  @Test
+  public void deleteExpired() {
+    SamlMessageIdDto expiredSamlMessageId1 = underTest.insert(dbSession, new SamlMessageIdDto()
+      .setMessageId("MESSAGE_1")
+      .setExpirationDate(NOW - 2_000_000_000L));
+    SamlMessageIdDto expiredSamlMessageId2 = underTest.insert(dbSession, new SamlMessageIdDto()
+      .setMessageId("MESSAGE_2")
+      .setExpirationDate(NOW - 2_000_000_000L));
+    SamlMessageIdDto validSamlMessageId = underTest.insert(dbSession, new SamlMessageIdDto()
+      .setMessageId("MESSAGE_3")
+      .setExpirationDate(NOW + 1_000_000_000L));
+
+    int result = underTest.deleteExpired(dbSession);
+
+    assertThat(underTest.selectByMessageId(dbSession, expiredSamlMessageId1.getMessageId())).isNotPresent();
+    assertThat(underTest.selectByMessageId(dbSession, expiredSamlMessageId2.getMessageId())).isNotPresent();
+    assertThat(underTest.selectByMessageId(dbSession, validSamlMessageId.getMessageId())).isPresent();
+    assertThat(result).isEqualTo(2);
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSamlMessageIdsTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v84/CreateSamlMessageIdsTable.java
new file mode 100644 (file)
index 0000000..cca7776
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v84;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+
+public class CreateSamlMessageIdsTable extends DdlChange {
+
+  private static final String TABLE_NAME = "saml_message_ids";
+  private static final VarcharColumnDef MESSAGE_ID_COLUMN = VarcharColumnDef.newVarcharColumnDefBuilder()
+    .setColumnName("message_id")
+    .setLimit(255)
+    .setIsNullable(false)
+    .build();
+
+  public CreateSamlMessageIdsTable(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new CreateTableBuilder(getDialect(), TABLE_NAME)
+      .addPkColumn(VarcharColumnDef.newVarcharColumnDefBuilder()
+        .setColumnName("uuid")
+        .setLimit(UUID_SIZE)
+        .setIsNullable(false)
+        .build())
+      .addColumn(MESSAGE_ID_COLUMN)
+      .addColumn(newBigIntegerColumnDefBuilder()
+        .setColumnName("expiration_date")
+        .setIsNullable(false)
+        .build())
+      .addColumn(newBigIntegerColumnDefBuilder()
+        .setColumnName("created_at")
+        .setIsNullable(false)
+        .build())
+      .build());
+
+    context.execute(new CreateIndexBuilder()
+      .setTable(TABLE_NAME)
+      .setName("saml_message_ids_unique")
+      .addColumn(MESSAGE_ID_COLUMN)
+      .setUnique(true)
+      .build());
+  }
+}
index 1d4eac7aed2d5da86d42979b7b167eea84b5126d..811bb596e040944e1ee3fdba308a4e241323da6e 100644 (file)
@@ -782,6 +782,7 @@ public class DbVersion84 implements DbVersion {
 
       .add(3800, "Remove favourites for components with qualifiers 'DIR', 'FIL', 'UTS'", RemoveFilesFavouritesFromProperties.class)
       .add(3801, "Create 'SESSION_TOKENS' table", CreateSessionTokensTable.class)
+      .add(3802, "Create 'SAML_MESSAGE_IDS' table", CreateSamlMessageIdsTable.class)
     ;
   }
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSamlMessageIdsTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v84/CreateSamlMessageIdsTableTest.java
new file mode 100644 (file)
index 0000000..86c5cce
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.platform.db.migration.version.v84;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+
+public class CreateSamlMessageIdsTableTest {
+
+  private static final String TABLE_NAME = "saml_message_ids";
+
+  @Rule
+  public CoreDbTester dbTester = CoreDbTester.createEmpty();
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private CreateSamlMessageIdsTable underTest = new CreateSamlMessageIdsTable(dbTester.database());
+
+  @Test
+  public void table_has_been_created() throws SQLException {
+    underTest.execute();
+
+    dbTester.assertTableExists(TABLE_NAME);
+    dbTester.assertPrimaryKey(TABLE_NAME, "pk_saml_message_ids", "uuid");
+    dbTester.assertUniqueIndex(TABLE_NAME, "saml_message_ids_unique", "message_id");
+
+    dbTester.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, 40, false);
+    dbTester.assertColumnDefinition(TABLE_NAME, "message_id", VARCHAR, 255, false);
+    dbTester.assertColumnDefinition(TABLE_NAME, "expiration_date", BIGINT, 20, false);
+    dbTester.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, 20, false);
+  }
+
+}
index c2619579720cc4e326eee9bff659283db4144cea..18681bee6a05064e6ca70a819d24e154b3f2d8dd 100644 (file)
@@ -21,8 +21,8 @@ package org.sonar.server.authentication;
 
 import org.sonar.core.platform.Module;
 import org.sonar.server.authentication.event.AuthenticationEventImpl;
-import org.sonar.server.authentication.purge.SessionTokensCleaner;
-import org.sonar.server.authentication.purge.SessionTokensCleanerExecutorServiceImpl;
+import org.sonar.server.authentication.purge.ExpiredSessionsCleaner;
+import org.sonar.server.authentication.purge.ExpiredSessionsCleanerExecutorServiceImpl;
 
 public class AuthenticationModule extends Module {
   @Override
@@ -45,8 +45,8 @@ public class AuthenticationModule extends Module {
       OAuth2ContextFactory.class,
       OAuthCsrfVerifier.class,
       RequestAuthenticatorImpl.class,
-      SessionTokensCleaner.class,
-      SessionTokensCleanerExecutorServiceImpl.class,
+      ExpiredSessionsCleaner.class,
+      ExpiredSessionsCleanerExecutorServiceImpl.class,
       UserLastConnectionDatesUpdaterImpl.class,
       UserRegistrarImpl.class,
       UserSessionInitializer.class);
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleaner.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleaner.java
new file mode 100644 (file)
index 0000000..0c4bb61
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.TimeUnit;
+import org.sonar.api.Startable;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.util.GlobalLockManager;
+
+public class ExpiredSessionsCleaner implements Startable {
+
+  private static final Logger LOG = Loggers.get(ExpiredSessionsCleaner.class);
+
+  private static final long PERIOD_IN_SECONDS = 24 * 60 * 60L;
+  private static final String LOCK_NAME = "SessionCleaner";
+
+  private final ExpiredSessionsCleanerExecutorService executorService;
+  private final DbClient dbClient;
+  private final GlobalLockManager lockManager;
+
+  public ExpiredSessionsCleaner(ExpiredSessionsCleanerExecutorService executorService, DbClient dbClient, GlobalLockManager lockManager) {
+    this.executorService = executorService;
+    this.dbClient = dbClient;
+    this.lockManager = lockManager;
+  }
+
+  @Override
+  public void start() {
+    this.executorService.scheduleAtFixedRate(this::executePurge, 0, PERIOD_IN_SECONDS, TimeUnit.SECONDS);
+  }
+
+  private void executePurge() {
+    if (!lockManager.tryLock(LOCK_NAME)) {
+      return;
+    }
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      cleanExpiredSessionTokens(dbSession);
+      cleanExpiredSamlMessageIds(dbSession);
+    }
+  }
+
+  private void cleanExpiredSessionTokens(DbSession dbSession) {
+    LOG.debug("Start of cleaning expired session tokens");
+    int deletedSessionTokens = dbClient.sessionTokensDao().deleteExpired(dbSession);
+    dbSession.commit();
+    LOG.info("Purge of expired session tokens has removed {} elements", deletedSessionTokens);
+  }
+
+  private void cleanExpiredSamlMessageIds(DbSession dbSession) {
+    LOG.debug("Start of cleaning expired SAML message IDs");
+    int deleted = dbClient.samlMessageIdDao().deleteExpired(dbSession);
+    dbSession.commit();
+    LOG.info("Purge of expired SAML message ids has removed {} elements", deleted);
+  }
+
+  @Override
+  public void stop() {
+    // nothing to do
+  }
+
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerExecutorService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerExecutorService.java
new file mode 100644 (file)
index 0000000..3ecd800
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.ScheduledExecutorService;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface ExpiredSessionsCleanerExecutorService extends ScheduledExecutorService {
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerExecutorServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerExecutorServiceImpl.java
new file mode 100644 (file)
index 0000000..b97d66c
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl;
+
+import static java.lang.Thread.MIN_PRIORITY;
+
+public class ExpiredSessionsCleanerExecutorServiceImpl
+  extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService>
+  implements ExpiredSessionsCleanerExecutorService {
+
+  public ExpiredSessionsCleanerExecutorServiceImpl() {
+    super(
+      Executors.newSingleThreadScheduledExecutor(r -> {
+        Thread thread = Executors.defaultThreadFactory().newThread(r);
+        thread.setName("ExpiredSessionsCleaner-%d");
+        thread.setPriority(MIN_PRIORITY);
+        thread.setDaemon(false);
+        return thread;
+      }));
+  }
+}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleaner.java
deleted file mode 100644 (file)
index d5764bf..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-
-package org.sonar.server.authentication.purge;
-
-import java.util.concurrent.TimeUnit;
-import org.sonar.api.Startable;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.server.util.GlobalLockManager;
-
-public class SessionTokensCleaner implements Startable {
-
-  private static final Logger LOG = Loggers.get(SessionTokensCleaner.class);
-
-  private static final String PURGE_DELAY_CONFIGURATION = "sonar.authentication.session.tokens.purge.delay";
-  private static final long DEFAULT_PURGE_DELAY_IN_SECONDS = 24 * 60 * 60L;
-  private static final String LOCK_NAME = "SessionCleaner";
-
-  private final SessionTokensCleanerExecutorService executorService;
-  private final DbClient dbClient;
-  private final Configuration configuration;
-  private final GlobalLockManager lockManager;
-
-  public SessionTokensCleaner(SessionTokensCleanerExecutorService executorService, DbClient dbClient, Configuration configuration, GlobalLockManager lockManager) {
-    this.executorService = executorService;
-    this.dbClient = dbClient;
-    this.configuration = configuration;
-    this.lockManager = lockManager;
-  }
-
-  @Override
-  public void start() {
-    this.executorService.scheduleAtFixedRate(this::executePurge, 0, configuration.getLong(PURGE_DELAY_CONFIGURATION).orElse(DEFAULT_PURGE_DELAY_IN_SECONDS), TimeUnit.SECONDS);
-  }
-
-  private void executePurge() {
-    if (!lockManager.tryLock(LOCK_NAME)) {
-      return;
-    }
-    LOG.debug("Start of cleaning expired session tokens");
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      int deletedSessionTokens = dbClient.sessionTokensDao().deleteExpired(dbSession);
-      dbSession.commit();
-      LOG.info("Purge of expired session tokens has removed {} elements", deletedSessionTokens);
-    }
-  }
-
-  @Override
-  public void stop() {
-    // nothing to do
-  }
-
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorService.java
deleted file mode 100644 (file)
index 551363c..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.authentication.purge;
-
-import java.util.concurrent.ScheduledExecutorService;
-import org.sonar.api.server.ServerSide;
-
-@ServerSide
-public interface SessionTokensCleanerExecutorService extends ScheduledExecutorService {
-}
diff --git a/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java b/server/sonar-webserver-auth/src/main/java/org/sonar/server/authentication/purge/SessionTokensCleanerExecutorServiceImpl.java
deleted file mode 100644 (file)
index 3a0bbad..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.server.authentication.purge;
-
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import org.sonar.server.util.AbstractStoppableScheduledExecutorServiceImpl;
-
-import static java.lang.Thread.MIN_PRIORITY;
-
-public class SessionTokensCleanerExecutorServiceImpl
-  extends AbstractStoppableScheduledExecutorServiceImpl<ScheduledExecutorService>
-  implements SessionTokensCleanerExecutorService {
-
-  public SessionTokensCleanerExecutorServiceImpl() {
-    super(
-      Executors.newSingleThreadScheduledExecutor(r -> {
-        Thread thread = Executors.defaultThreadFactory().newThread(r);
-        thread.setName("SessionTokensCleaner-%d");
-        thread.setPriority(MIN_PRIORITY);
-        thread.setDaemon(false);
-        return thread;
-      }));
-  }
-}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/ExpiredSessionsCleanerTest.java
new file mode 100644 (file)
index 0000000..df04c52
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.authentication.purge;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.api.utils.log.LogAndArguments;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.SamlMessageIdDto;
+import org.sonar.db.user.SessionTokenDto;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.util.AbstractStoppableExecutorService;
+import org.sonar.server.util.GlobalLockManager;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ExpiredSessionsCleanerTest {
+
+  private static final long NOW = 1_000_000_000L;
+
+  private TestSystem2 system2 = new TestSystem2().setNow(NOW);
+  @Rule
+  public DbTester db = DbTester.create(system2);
+  @Rule
+  public LogTester logTester = new LogTester();
+
+  private GlobalLockManager lockManager = mock(GlobalLockManager.class);
+
+  private SyncSessionTokensCleanerExecutorService executorService = new SyncSessionTokensCleanerExecutorService();
+
+  private ExpiredSessionsCleaner underTest = new ExpiredSessionsCleaner(executorService, db.getDbClient(), lockManager);
+
+  @Test
+  public void purge_expired_session_tokens() {
+    when(lockManager.tryLock(anyString())).thenReturn(true);
+    UserDto user = db.users().insertUser();
+    SessionTokenDto validSessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW + 1_000_000L));
+    SessionTokenDto expiredSessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000L));
+    underTest.start();
+
+    executorService.runCommand();
+
+    assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), validSessionToken.getUuid())).isPresent();
+    assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isNotPresent();
+    assertThat(logTester.getLogs(LoggerLevel.INFO))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .contains("Purge of expired session tokens has removed 1 elements");
+  }
+
+  @Test
+  public void purge_expired_saml_message_ids() {
+    when(lockManager.tryLock(anyString())).thenReturn(true);
+    db.getDbClient().samlMessageIdDao().insert(db.getSession(), new SamlMessageIdDto().setMessageId("MESSAGE_1").setExpirationDate(NOW + 1_000_000L));
+    db.getDbClient().samlMessageIdDao().insert(db.getSession(), new SamlMessageIdDto().setMessageId("MESSAGE_2").setExpirationDate(NOW - 1_000_000L));
+    db.commit();
+    underTest.start();
+
+    executorService.runCommand();
+
+    assertThat(db.getDbClient().samlMessageIdDao().selectByMessageId(db.getSession(), "MESSAGE_1")).isPresent();
+    assertThat(db.getDbClient().samlMessageIdDao().selectByMessageId(db.getSession(), "MESSAGE_2")).isNotPresent();
+    assertThat(logTester.getLogs(LoggerLevel.INFO))
+      .extracting(LogAndArguments::getFormattedMsg)
+      .contains("Purge of expired SAML message ids has removed 1 elements");
+  }
+
+  @Test
+  public void do_not_execute_purge_when_fail_to_get_lock() {
+    when(lockManager.tryLock(anyString())).thenReturn(false);
+    SessionTokenDto expiredSessionToken = db.users().insertSessionToken(db.users().insertUser(), st -> st.setExpirationDate(NOW - 1_000_000L));
+    underTest.start();
+
+    executorService.runCommand();
+
+    assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isPresent();
+  }
+
+  private static class SyncSessionTokensCleanerExecutorService extends AbstractStoppableExecutorService<ScheduledExecutorService> implements ExpiredSessionsCleanerExecutorService {
+
+    private Runnable command;
+
+    public SyncSessionTokensCleanerExecutorService() {
+      super(null);
+    }
+
+    public void runCommand() {
+      command.run();
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
+      this.command = command;
+      return null;
+    }
+
+    @Override
+    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
+      return null;
+    }
+
+    @Override
+    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
+      return null;
+    }
+
+    @Override
+    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
+      return null;
+    }
+
+  }
+}
diff --git a/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java b/server/sonar-webserver-auth/src/test/java/org/sonar/server/authentication/purge/SessionTokensCleanerTest.java
deleted file mode 100644 (file)
index 08a28f7..0000000
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2020 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-
-package org.sonar.server.authentication.purge;
-
-import java.util.concurrent.Callable;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.impl.utils.TestSystem2;
-import org.sonar.api.utils.log.LogAndArguments;
-import org.sonar.api.utils.log.LogTester;
-import org.sonar.api.utils.log.LoggerLevel;
-import org.sonar.db.DbTester;
-import org.sonar.db.user.SessionTokenDto;
-import org.sonar.db.user.UserDto;
-import org.sonar.server.util.AbstractStoppableExecutorService;
-import org.sonar.server.util.GlobalLockManager;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class SessionTokensCleanerTest {
-
-  private static final long NOW = 1_000_000_000L;
-
-  private TestSystem2 system2 = new TestSystem2().setNow(NOW);
-  @Rule
-  public DbTester db = DbTester.create(system2);
-  @Rule
-  public LogTester logTester = new LogTester();
-
-  private GlobalLockManager lockManager = mock(GlobalLockManager.class);
-
-  private final MapSettings settings = new MapSettings();
-  private final Configuration configuration = settings.asConfig();
-
-  private SyncSessionTokensCleanerExecutorService executorService = new SyncSessionTokensCleanerExecutorService();
-
-  private SessionTokensCleaner underTest = new SessionTokensCleaner(executorService, db.getDbClient(), configuration, lockManager);
-
-  @Test
-  public void purge_expired_session_tokens() {
-    when(lockManager.tryLock(anyString())).thenReturn(true);
-    UserDto user = db.users().insertUser();
-    SessionTokenDto validSessionToken = db.users().insertSessionToken(user);
-    SessionTokenDto expiredSessionToken = db.users().insertSessionToken(user, st -> st.setExpirationDate(NOW - 1_000_000L));
-    underTest.start();
-
-    executorService.runCommand();
-
-    assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), validSessionToken.getUuid())).isPresent();
-    assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isNotPresent();
-    assertThat(logTester.getLogs(LoggerLevel.INFO))
-      .extracting(LogAndArguments::getFormattedMsg)
-      .containsOnly("Purge of expired session tokens has removed 1 elements");
-  }
-
-  @Test
-  public void do_not_execute_purge_when_fail_to_get_lock() {
-    when(lockManager.tryLock(anyString())).thenReturn(false);
-    SessionTokenDto expiredSessionToken = db.users().insertSessionToken(db.users().insertUser(), st -> st.setExpirationDate(NOW - 1_000_000L));
-    underTest.start();
-
-    executorService.runCommand();
-
-    assertThat(db.getDbClient().sessionTokensDao().selectByUuid(db.getSession(), expiredSessionToken.getUuid())).isPresent();
-  }
-
-  private static class SyncSessionTokensCleanerExecutorService extends AbstractStoppableExecutorService<ScheduledExecutorService> implements SessionTokensCleanerExecutorService {
-
-    private Runnable command;
-
-    public SyncSessionTokensCleanerExecutorService() {
-      super(null);
-    }
-
-    public void runCommand() {
-      command.run();
-    }
-
-    @Override
-    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) {
-      this.command = command;
-      return null;
-    }
-
-    @Override
-    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
-      return null;
-    }
-
-    @Override
-    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
-      return null;
-    }
-
-    @Override
-    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) {
-      return null;
-    }
-
-  }
-}