diff --git a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java index ec4966068f..3e891426ee 100644 --- a/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java +++ b/scm-core/src/main/java/sonia/scm/security/SyncingRealmHelper.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.security; import com.google.inject.Inject; @@ -33,10 +33,14 @@ import sonia.scm.NotFoundException; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; import sonia.scm.plugin.Extension; +import sonia.scm.user.ExternalUserConverter; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; +import java.util.Collections; +import java.util.Set; + /** * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * users with the local database. @@ -50,28 +54,41 @@ public final class SyncingRealmHelper { private final AdministrationContext ctx; private final UserManager userManager; private final GroupManager groupManager; + private final Set externalUserConverters; /** * Constructs a new SyncingRealmHelper. * - * @param ctx administration context - * @param userManager user manager - * @param groupManager group manager + * @param ctx administration context + * @param userManager user manager + * @param groupManager group manager + * @param externalUserConverters global scm configuration */ @Inject - public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) { + public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, Set externalUserConverters) { this.ctx = ctx; this.userManager = userManager; this.groupManager = groupManager; + this.externalUserConverters = externalUserConverters; + } + + /** + * Constructs a new SyncingRealmHelper. + * + * @param ctx administration context + * @param userManager user manager + * @param groupManager group manager + */ + @Deprecated + public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) { + this(ctx, userManager, groupManager, Collections.emptySet()); } /** * Create {@link AuthenticationInfo} from user and groups. * - * * @param realm name of the realm - * @param user authenticated user - * + * @param user authenticated user * @return authentication info */ public AuthenticationInfo createAuthenticationInfo(String realm, User user) { @@ -114,10 +131,17 @@ public final class SyncingRealmHelper { public void store(final User user) { ctx.runAsAdmin(() -> { if (userManager.contains(user.getName())) { + User clone = user.clone(); + if (!externalUserConverters.isEmpty()) { + for (ExternalUserConverter converter : externalUserConverters) { + clone = converter.convert(clone); + } + } + try { - userManager.modify(user); + userManager.modify(clone); } catch (NotFoundException e) { - throw new IllegalStateException("got NotFoundException though user " + user.getName() + " could be loaded", e); + throw new IllegalStateException("got NotFoundException though user " + clone.getName() + " could be loaded", e); } } else { try { diff --git a/scm-core/src/main/java/sonia/scm/user/ExternalUserConverter.java b/scm-core/src/main/java/sonia/scm/user/ExternalUserConverter.java new file mode 100644 index 0000000000..c0e7a90d96 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/ExternalUserConverter.java @@ -0,0 +1,39 @@ +/* + * 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.user; + +import sonia.scm.plugin.ExtensionPoint; + +@ExtensionPoint +public interface ExternalUserConverter { + + /** + * Returns the converted user. + * + * @return converted user + * @since 2.8.0 + */ + User convert(User user); +} diff --git a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java index 7dca9c6ada..cd0a1c47c6 100644 --- a/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java +++ b/scm-core/src/test/java/sonia/scm/security/SyncingRealmHelperTest.java @@ -27,6 +27,7 @@ package sonia.scm.security; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableSet; import org.apache.shiro.authc.AuthenticationInfo; import org.junit.Before; import org.junit.Test; @@ -36,6 +37,7 @@ import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.AlreadyExistsException; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; +import sonia.scm.user.ExternalUserConverter; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.security.AdministrationContext; @@ -47,6 +49,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -68,8 +71,13 @@ public class SyncingRealmHelperTest { @Mock private UserManager userManager; + @Mock + private ExternalUserConverter converter; + private SyncingRealmHelper helper; + private SyncingRealmHelper helperWithConverters; + /** * Mock {@link AdministrationContext} and create object under test. */ @@ -94,6 +102,7 @@ public class SyncingRealmHelperTest { }; helper = new SyncingRealmHelper(ctx, userManager, groupManager); + helperWithConverters = new SyncingRealmHelper(ctx, userManager, groupManager, ImmutableSet.of(converter)); } /** @@ -170,6 +179,23 @@ public class SyncingRealmHelperTest { verify(userManager, times(1)).modify(user); } + /** + * Tests {@link SyncingRealmHelper#store(User)} with an existing user. + */ + @Test + public void testConvertUser(){ + User zaphod = new User("zaphod"); + when(converter.convert(any())).thenReturn(zaphod); + when(userManager.contains("tricia")).thenReturn(Boolean.TRUE); + + User user = new User("tricia"); + + helperWithConverters.store(user); + + verify(converter).convert(user); + verify(userManager, times(1)).modify(zaphod); + } + @Test public void builderShouldSetValues() { diff --git a/scm-webapp/src/main/java/sonia/scm/user/InternalToExternalUserConverter.java b/scm-webapp/src/main/java/sonia/scm/user/InternalToExternalUserConverter.java new file mode 100644 index 0000000000..02dfb2de27 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/user/InternalToExternalUserConverter.java @@ -0,0 +1,54 @@ +/* + * 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.user; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; + +import javax.inject.Inject; + +@Extension +public class InternalToExternalUserConverter implements ExternalUserConverter{ + + private final ScmConfiguration scmConfiguration; + + @Inject + public InternalToExternalUserConverter(ScmConfiguration scmConfiguration) { + this.scmConfiguration = scmConfiguration; + } + + public User convert(User user) { + if (shouldNotConvertUser(user)) { + return user; + } + user.setExternal(true); + user.setPassword(null); + return user; + } + + private boolean shouldNotConvertUser(User user) { + return user.isExternal() || !scmConfiguration.isEnabledUserConverter(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/user/InternalToExternalUserConverterTest.java b/scm-webapp/src/test/java/sonia/scm/user/InternalToExternalUserConverterTest.java new file mode 100644 index 0000000000..e4f2ad76e2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/user/InternalToExternalUserConverterTest.java @@ -0,0 +1,79 @@ +/* + * 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.user; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.config.ScmConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class InternalToExternalUserConverterTest { + + @Mock + ScmConfiguration scmConfiguration; + + @InjectMocks + InternalToExternalUserConverter converter; + + @Test + void shouldNotConvertExternalUser() { + User external = new User(); + external.setExternal(true); + + User user = converter.convert(external); + + assertThat(user).isSameAs(external); + } + + @Test + void shouldNotConvertIfConfigDisabled() { + when(scmConfiguration.isEnabledUserConverter()).thenReturn(false); + User external = new User(); + external.setExternal(false); + + User user = converter.convert(external); + + assertThat(user).isSameAs(external); + } + + @Test + void shouldReturnConvertedUser() { + when(scmConfiguration.isEnabledUserConverter()).thenReturn(true); + User internal = new User(); + internal.setExternal(false); + + User external = converter.convert(internal); + + assertThat(external).isInstanceOf(User.class); + assertThat(external.isExternal()).isTrue(); + assertThat(external.getPassword()).isNull(); + } +}