From 07fa753f80ea525ba6c400f2e477f20d15e26d11 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 19 Jan 2022 09:26:01 +0100 Subject: [PATCH] Encrypt myCloudogu refresh token on file system (#1923) Encrypt myCloudogu refresh token on file system and update current stored tokens using an update step. --- gradle/changelog/encrypt_myc_token.yaml | 2 + .../scm/plugin/PluginCenterAuthenticator.java | 5 +- .../PluginCenterAuthenticationUpdateStep.java | 78 +++++++++++++++ ...uginCenterAuthentiationUpdateStepTest.java | 94 +++++++++++++++++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 gradle/changelog/encrypt_myc_token.yaml create mode 100644 scm-webapp/src/main/java/sonia/scm/update/plugin/PluginCenterAuthenticationUpdateStep.java create mode 100644 scm-webapp/src/test/java/sonia/scm/update/plugin/PluginCenterAuthentiationUpdateStepTest.java diff --git a/gradle/changelog/encrypt_myc_token.yaml b/gradle/changelog/encrypt_myc_token.yaml new file mode 100644 index 0000000000..3d3f3b18f1 --- /dev/null +++ b/gradle/changelog/encrypt_myc_token.yaml @@ -0,0 +1,2 @@ +- type: fixed + description: Encrypt myCloudogu refresh token on file system ([#1923](https://github.com/scm-manager/scm-manager/pull/1923)) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterAuthenticator.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterAuthenticator.java index 44a7c7213c..c8b426a546 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterAuthenticator.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterAuthenticator.java @@ -41,6 +41,7 @@ import sonia.scm.net.ahc.AdvancedHttpResponse; import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.util.HttpUtil; +import sonia.scm.xml.XmlEncryptionAdapter; import sonia.scm.xml.XmlInstantAdapter; import javax.inject.Inject; @@ -151,13 +152,13 @@ public class PluginCenterAuthenticator { @Data @XmlRootElement - @VisibleForTesting @AllArgsConstructor @NoArgsConstructor @XmlAccessorType(XmlAccessType.FIELD) - static class Authentication implements AuthenticationInfo { + public static class Authentication implements AuthenticationInfo { private String principal; private String pluginCenterSubject; + @XmlJavaTypeAdapter(XmlEncryptionAdapter.class) private String refreshToken; @XmlJavaTypeAdapter(XmlInstantAdapter.class) private Instant date; diff --git a/scm-webapp/src/main/java/sonia/scm/update/plugin/PluginCenterAuthenticationUpdateStep.java b/scm-webapp/src/main/java/sonia/scm/update/plugin/PluginCenterAuthenticationUpdateStep.java new file mode 100644 index 0000000000..3a2c715d59 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/update/plugin/PluginCenterAuthenticationUpdateStep.java @@ -0,0 +1,78 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.update.plugin; + +import com.google.common.base.Strings; +import sonia.scm.migration.UpdateStep; +import sonia.scm.plugin.Extension; +import sonia.scm.plugin.PluginCenterAuthenticator; +import sonia.scm.security.CipherUtil; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.version.Version; + +import javax.inject.Inject; + +import static sonia.scm.version.Version.parse; + +@Extension +public class PluginCenterAuthenticationUpdateStep implements UpdateStep { + + private final ConfigurationStoreFactory configurationStoreFactory; + + @Inject + public PluginCenterAuthenticationUpdateStep(ConfigurationStoreFactory configurationStoreFactory) { + this.configurationStoreFactory = configurationStoreFactory; + } + + @Override + public void doUpdate() throws Exception { + ConfigurationStore configurationStore = configurationStoreFactory + .withType(PluginCenterAuthenticator.Authentication.class) + .withName("plugin-center-auth") + .build(); + configurationStore.getOptional() + .ifPresent(config -> { + String token = config.getRefreshToken(); + CipherUtil cipher = CipherUtil.getInstance(); + if (Strings.isNullOrEmpty(token) || !token.startsWith("{enc}")) { + token = "{enc}".concat(cipher.encode(token)); + config.setRefreshToken(token); + configurationStore.set(config); + } + }); + } + + + @Override + public Version getTargetVersion() { + return parse("2.30.0"); + } + + @Override + public String getAffectedDataType() { + return "sonia.scm.plugin-center.authentication"; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/update/plugin/PluginCenterAuthentiationUpdateStepTest.java b/scm-webapp/src/test/java/sonia/scm/update/plugin/PluginCenterAuthentiationUpdateStepTest.java new file mode 100644 index 0000000000..d9ca9452f2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/update/plugin/PluginCenterAuthentiationUpdateStepTest.java @@ -0,0 +1,94 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.update.plugin; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.PluginCenterAuthenticator; +import sonia.scm.store.ConfigurationStore; +import sonia.scm.store.ConfigurationStoreFactory; + +import java.time.Instant; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginCenterAuthenticationUpdateStepTest { + + private PluginCenterAuthenticationUpdateStep updateStep; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ConfigurationStoreFactory configurationStoreFactory; + @Mock + private ConfigurationStore configurationStore; + + @BeforeEach + void initUpdateStep() { + when(configurationStoreFactory.withType(PluginCenterAuthenticator.Authentication.class).withName("plugin-center-auth").build()) + .thenReturn(configurationStore); + updateStep = new PluginCenterAuthenticationUpdateStep(configurationStoreFactory); + } + + @Test + void shouldNotUpdateIfConfigFileNotAvailable() throws Exception { + when(configurationStore.getOptional()).thenReturn(Optional.empty()); + + updateStep.doUpdate(); + + verify(configurationStore, never()).set(any()); + } + + @Test + void shouldUpdateIfRefreshTokenNotEncrypted() throws Exception { + when(configurationStore.getOptional()) + .thenReturn(Optional.of(new PluginCenterAuthenticator.Authentication("trillian", "trillian", "some_not_encrypted_token", Instant.now()))); + + updateStep.doUpdate(); + + verify(configurationStore).set(argThat(config -> { + assertThat(config.getRefreshToken()).startsWith("{enc}"); + return true; + })); + } + + @Test + void shouldNotUpdateIfRefreshTokenIsAlreadyEncrypted() throws Exception { + when(configurationStore.getOptional()) + .thenReturn(Optional.of(new PluginCenterAuthenticator.Authentication("trillian", "trillian", "{enc}my_encrypted_token", Instant.now()))); + + updateStep.doUpdate(); + + verify(configurationStore, never()).set(any()); + } +}