From 3b4b1a17678055013b31fcc7ef0c08167dac4faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Wed, 20 Jul 2022 09:17:14 +0200 Subject: [PATCH] Add cli commands to modify repository permissions (#2090) Co-authored-by: Konstantin Schaper --- .../cli_repository_permission_commands.yaml | 2 + .../cli/GroupCommand.java | 2 +- .../scm/repository/NamespaceAndName.java | 13 +- .../java/sonia/scm/repository/Repository.java | 23 ++ .../scm/cli/CliExecutionExceptionHandler.java | 2 +- .../scm/cli/CliParameterExceptionHandler.java | 2 +- .../sonia/scm/cli/CombinedResourceBundle.java | 2 +- .../java/sonia/scm/cli/CommandFactory.java | 10 +- .../java/sonia/scm/cli/CommandRegistry.java | 2 +- .../main/java/sonia/scm/cli/HelpMixin.java | 2 +- .../java/sonia/scm/cli/LogoutCommand.java | 2 +- .../NonExistingParentCommandException.java | 2 +- .../cli/PermissionDescriptionResolver.java | 84 +++++++ .../PermissionDescriptionResolverFactory.java | 44 ++++ .../main/java/sonia/scm/cli/PingCommand.java | 2 +- .../scm/cli/RegisteredCommandCollector.java | 2 +- .../sonia/scm/cli/RegisteredCommandNode.java | 2 +- .../java/sonia/scm/cli/ScmManagerCommand.java | 2 +- .../java/sonia/scm/cli/VersionCommand.java | 2 +- .../scm/group/cli/GroupAddMemberCommand.java | 1 - .../scm/group/cli/GroupCreateCommand.java | 1 - .../scm/group/cli/GroupDeleteCommand.java | 1 - .../sonia/scm/group/cli/GroupGetCommand.java | 1 - .../sonia/scm/group/cli/GroupListCommand.java | 1 - .../scm/group/cli/GroupModifyCommand.java | 1 - .../group/cli/GroupRemoveMemberCommand.java | 1 - .../cli/RepositoryPermissionBaseCommand.java | 116 +++++++++ .../cli/RepositoryPermissionBean.java | 38 +++ .../cli/RepositoryPermissionsAddCommand.java | 109 +++++++++ ...RepositoryPermissionsAvailableCommand.java | 99 ++++++++ .../RepositoryPermissionsClearCommand.java | 76 ++++++ .../cli/RepositoryPermissionsListCommand.java | 129 ++++++++++ .../RepositoryPermissionsRemoveCommand.java | 92 +++++++ .../RepositoryPermissionsSetRoleCommand.java | 91 +++++++ .../cli/RepositoryTemplateRenderer.java | 94 +++++++- ...epositoryToRepositoryCommandDtoMapper.java | 2 +- .../sonia/scm/repository/cli/RoleBean.java | 36 +++ .../sonia/scm/repository/cli/VerbBean.java | 33 +++ .../java/sonia/scm/web/i18n/I18nServlet.java | 2 +- .../main/resources/locales/de/plugins.json | 6 +- .../resources/sonia/scm/cli/i18n.properties | 46 ++++ .../sonia/scm/cli/i18n_de.properties | 46 ++++ .../java/sonia/scm/cli/CliProcessorTest.java | 22 +- .../cli/RepositoryDeleteCommandTest.java | 1 - .../cli/RepositoryGetCommandTest.java | 3 - .../cli/RepositoryListCommandTest.java | 3 - .../RepositoryPermissionsAddCommandTest.java | 226 ++++++++++++++++++ ...sitoryPermissionsAvailableCommandTest.java | 153 ++++++++++++ ...RepositoryPermissionsClearCommandTest.java | 128 ++++++++++ .../RepositoryPermissionsListCommandTest.java | 195 +++++++++++++++ ...epositoryPermissionsRemoveCommandTest.java | 182 ++++++++++++++ ...positoryPermissionsSetRoleCommandTest.java | 196 +++++++++++++++ 52 files changed, 2289 insertions(+), 44 deletions(-) create mode 100644 gradle/changelog/cli_repository_permission_commands.yaml rename scm-core/src/main/java/sonia/scm/{repository => group}/cli/GroupCommand.java (97%) create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolver.java create mode 100644 scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolverFactory.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBaseCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBean.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsListCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommand.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/RoleBean.java create mode 100644 scm-webapp/src/main/java/sonia/scm/repository/cli/VerbBean.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsListCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommandTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommandTest.java diff --git a/gradle/changelog/cli_repository_permission_commands.yaml b/gradle/changelog/cli_repository_permission_commands.yaml new file mode 100644 index 0000000000..52a431b5be --- /dev/null +++ b/gradle/changelog/cli_repository_permission_commands.yaml @@ -0,0 +1,2 @@ +- type: added + description: Cli commands to modify repository permissions ([#2090](https://github.com/scm-manager/scm-manager/pull/2090)) diff --git a/scm-core/src/main/java/sonia/scm/repository/cli/GroupCommand.java b/scm-core/src/main/java/sonia/scm/group/cli/GroupCommand.java similarity index 97% rename from scm-core/src/main/java/sonia/scm/repository/cli/GroupCommand.java rename to scm-core/src/main/java/sonia/scm/group/cli/GroupCommand.java index 300a8cf00a..6b4e17a035 100644 --- a/scm-core/src/main/java/sonia/scm/repository/cli/GroupCommand.java +++ b/scm-core/src/main/java/sonia/scm/group/cli/GroupCommand.java @@ -22,7 +22,7 @@ * SOFTWARE. */ -package sonia.scm.repository.cli; +package sonia.scm.group.cli; import picocli.CommandLine; diff --git a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java index be75f31a94..4d49cd6ccb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.java +++ b/scm-core/src/main/java/sonia/scm/repository/NamespaceAndName.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.repository; import com.google.common.base.Preconditions; @@ -41,6 +41,17 @@ public class NamespaceAndName implements Comparable { this.name = name; } + /** + * @since 2.38.0 + */ + public static NamespaceAndName fromString(String namespaceAndName) { + String[] parts = namespaceAndName.split("/"); + if (parts.length != 2) { + throw new IllegalArgumentException("namespace and name must be divided by a slash (/)"); + } + return new NamespaceAndName(parts[0], parts[1]); + } + public String getNamespace() { return namespace; } diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index c0ae338670..958e26cde5 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -46,6 +46,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; /** @@ -209,6 +210,28 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return Collections.unmodifiableCollection(permissions); } + /** + * Returns the permission for the given user, if present, or an empty {@link Optional} otherwise. + * + * @since 2.38.0 + */ + public Optional findUserPermission(String userId) { + return findPermission(userId, false); + } + + /** + * Returns the permission for the given group, if present, or an empty {@link Optional} otherwise. + * + * @since 2.38.0 + */ + public Optional findGroupPermission(String groupId) { + return findPermission(groupId, true); + } + + private Optional findPermission(String x, boolean isGroup) { + return getPermissions().stream().filter(p -> p.isGroupPermission() == isGroup && p.getName().equals(x)).findFirst(); + } + /** * Returns the type (hg, git, svn ...) of the {@link Repository}. * diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CliExecutionExceptionHandler.java b/scm-webapp/src/main/java/sonia/scm/cli/CliExecutionExceptionHandler.java index 2c1160d977..4820dbc88f 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/CliExecutionExceptionHandler.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/CliExecutionExceptionHandler.java @@ -41,7 +41,7 @@ import static java.util.Optional.empty; import static java.util.Optional.of; @Slf4j -public class CliExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { +class CliExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { private final I18nCollector i18nCollector; private final String languageCode; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CliParameterExceptionHandler.java b/scm-webapp/src/main/java/sonia/scm/cli/CliParameterExceptionHandler.java index 543d978de2..47caae8bcf 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/CliParameterExceptionHandler.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/CliParameterExceptionHandler.java @@ -32,7 +32,7 @@ import java.util.Locale; import java.util.ResourceBundle; import java.util.stream.Collectors; -public class CliParameterExceptionHandler implements IParameterExceptionHandler { +class CliParameterExceptionHandler implements IParameterExceptionHandler { private final String languageCode; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java b/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java index 725519ecc7..6445392618 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/CombinedResourceBundle.java @@ -30,7 +30,7 @@ import java.util.ListResourceBundle; import java.util.ResourceBundle; import java.util.stream.Collectors; -public class CombinedResourceBundle extends ListResourceBundle { +class CombinedResourceBundle extends ListResourceBundle { private final Object[][] contents; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java b/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java index 9c2eda306e..79a5252f6f 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/CommandFactory.java @@ -28,12 +28,13 @@ import com.google.inject.AbstractModule; import com.google.inject.Injector; import picocli.CommandLine; -public class CommandFactory implements CommandLine.IFactory { +class CommandFactory implements CommandLine.IFactory { private final Injector injector; public CommandFactory(Injector injector, CliContext context) { - this.injector = injector.createChildInjector(new CliContextModule(context)); + PermissionDescriptionResolverFactory permissionDescriptionResolverFactory = injector.getInstance(PermissionDescriptionResolverFactory.class); + this.injector = injector.createChildInjector(new CliContextModule(permissionDescriptionResolverFactory, context)); } @Override @@ -44,14 +45,17 @@ public class CommandFactory implements CommandLine.IFactory { static class CliContextModule extends AbstractModule { private final CliContext context; + private final PermissionDescriptionResolver permissionDescriptionResolver; - private CliContextModule(CliContext context) { + private CliContextModule(PermissionDescriptionResolverFactory permissionDescriptionResolverFactory, CliContext context) { this.context = context; + permissionDescriptionResolver = permissionDescriptionResolverFactory.createResolver(context.getLocale()); } @Override protected void configure() { bind(CliContext.class).toInstance(context); + bind(PermissionDescriptionResolver.class).toInstance(permissionDescriptionResolver); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java b/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java index d05b59856f..315c54394b 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/CommandRegistry.java @@ -33,7 +33,7 @@ import java.util.Map; import java.util.stream.Collectors; @Singleton -public class CommandRegistry { +class CommandRegistry { private final RegisteredCommandCollector commandCollector; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java b/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java index 01d444365e..d794afa4ad 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/HelpMixin.java @@ -26,7 +26,7 @@ package sonia.scm.cli; import picocli.CommandLine; -public class HelpMixin { +class HelpMixin { @CommandLine.Option(names = {"--help", "-h"}, usageHelp = true, descriptionKey = "scm.help.usage.description.0") private boolean usageHelp; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java index 8055dd0def..98faa97b90 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/LogoutCommand.java @@ -27,4 +27,4 @@ package sonia.scm.cli; import picocli.CommandLine; @CommandLine.Command(name = "logout") -public class LogoutCommand {} +class LogoutCommand {} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java b/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java index 735c50b08e..9001fc5a81 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/NonExistingParentCommandException.java @@ -28,7 +28,7 @@ package sonia.scm.cli; * Exception is thrown if a command is registered with parent which does not exist. * @since 2.33.0 */ -public class NonExistingParentCommandException extends CliException { +class NonExistingParentCommandException extends CliException { public NonExistingParentCommandException(String message) { super(message); } diff --git a/scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolver.java b/scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolver.java new file mode 100644 index 0000000000..953d8659e9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolver.java @@ -0,0 +1,84 @@ +/* + * 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.cli; + +import com.fasterxml.jackson.databind.JsonNode; +import sonia.scm.i18n.I18nCollector; + +import java.io.IOException; +import java.util.Locale; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +public class PermissionDescriptionResolver { + private final I18nCollector i18nCollector; + private final Locale locale; + + PermissionDescriptionResolver(I18nCollector i18nCollector, Locale locale) { + this.i18nCollector = i18nCollector; + this.locale = locale; + } + + public Optional getDescription(String verb) { + try { + i18nCollector.findJson(locale.getLanguage()); + } catch (IOException e) { + throw new RuntimeException("failed to load i18n package", e); + } + return getVerbDescriptionFromI18nBundle(verb); + } + + private Optional getVerbDescriptionFromI18nBundle(String verb) { + Optional jsonNode; + try { + jsonNode = i18nCollector.findJson(locale.getLanguage()); + } catch (IOException e) { + return empty(); + } + if (jsonNode.isEmpty()) { + return empty(); + } + JsonNode verbsNode = jsonNode.get().get("verbs"); + if (verbsNode == null) { + return empty(); + } + JsonNode repositoryNode = verbsNode.get("repository"); + if (repositoryNode == null) { + return empty(); + } + JsonNode permissionNode = repositoryNode.get(verb); + if (permissionNode == null) { + return empty(); + } + JsonNode displayNameNode = permissionNode.get("displayName"); + if (displayNameNode == null) { + return empty(); + } + + return of(displayNameNode.asText()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolverFactory.java b/scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolverFactory.java new file mode 100644 index 0000000000..dbb7f70c1d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/cli/PermissionDescriptionResolverFactory.java @@ -0,0 +1,44 @@ +/* + * 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.cli; + +import sonia.scm.i18n.I18nCollector; + +import javax.inject.Inject; +import java.util.Locale; + +class PermissionDescriptionResolverFactory { + + private final I18nCollector i18nCollector; + + @Inject + PermissionDescriptionResolverFactory(I18nCollector i18nCollector) { + this.i18nCollector = i18nCollector; + } + + PermissionDescriptionResolver createResolver(Locale locale) { + return new PermissionDescriptionResolver(i18nCollector, locale); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java index 0b09af21eb..d3690a46d2 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/PingCommand.java @@ -29,7 +29,7 @@ import picocli.CommandLine; import javax.inject.Inject; @CommandLine.Command(name = "ping", hidden = true) -public class PingCommand implements Runnable { +class PingCommand implements Runnable { private final CliContext context; diff --git a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java index acc19c9c53..2c63cecfd4 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandCollector.java @@ -36,7 +36,7 @@ import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; -public class RegisteredCommandCollector { +class RegisteredCommandCollector { private static final Logger LOG = LoggerFactory.getLogger(RegisteredCommandCollector.class); diff --git a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java index 264fa9c259..6a4b1da296 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/RegisteredCommandNode.java @@ -30,7 +30,7 @@ import java.util.ArrayList; import java.util.List; @Getter -public class RegisteredCommandNode { +class RegisteredCommandNode { private final String name; private final Class command; private final List children = new ArrayList<>(); diff --git a/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java index d19b9c45eb..049cb3b473 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/ScmManagerCommand.java @@ -27,4 +27,4 @@ package sonia.scm.cli; import picocli.CommandLine; @CommandLine.Command(name = "scm") -public class ScmManagerCommand {} +class ScmManagerCommand {} diff --git a/scm-webapp/src/main/java/sonia/scm/cli/VersionCommand.java b/scm-webapp/src/main/java/sonia/scm/cli/VersionCommand.java index e1187cd3c9..2d010edee4 100644 --- a/scm-webapp/src/main/java/sonia/scm/cli/VersionCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/cli/VersionCommand.java @@ -32,7 +32,7 @@ import sonia.scm.SCMContextProvider; import javax.inject.Inject; @CommandLine.Command(name = "version") -public class VersionCommand implements Runnable{ +class VersionCommand implements Runnable{ private static final String TEMPLATE = String.join("\n","Client Version: {{client.version}}", "Server Version: {{server.version}}"); diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupAddMemberCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupAddMemberCommand.java index 61f44f3953..9a6903fcb6 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupAddMemberCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupAddMemberCommand.java @@ -29,7 +29,6 @@ import picocli.CommandLine; import sonia.scm.cli.ParentCommand; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; import java.util.Arrays; diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupCreateCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupCreateCommand.java index 436877e138..7d19779de5 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupCreateCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupCreateCommand.java @@ -29,7 +29,6 @@ import picocli.CommandLine; import sonia.scm.cli.ParentCommand; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupDeleteCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupDeleteCommand.java index d7effb5229..2f2dce31d3 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupDeleteCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupDeleteCommand.java @@ -29,7 +29,6 @@ import picocli.CommandLine; import sonia.scm.cli.ParentCommand; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; import java.util.Collections; diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupGetCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupGetCommand.java index 88f8fd8938..018e08a600 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupGetCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupGetCommand.java @@ -29,7 +29,6 @@ import picocli.CommandLine; import sonia.scm.cli.ParentCommand; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupListCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupListCommand.java index f21081897e..0a302ef47c 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupListCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupListCommand.java @@ -31,7 +31,6 @@ import sonia.scm.cli.ParentCommand; import sonia.scm.cli.Table; import sonia.scm.cli.TemplateRenderer; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; import java.util.Collection; diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupModifyCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupModifyCommand.java index 70c2fb49f0..bef4c4ad18 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupModifyCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupModifyCommand.java @@ -29,7 +29,6 @@ import picocli.CommandLine; import sonia.scm.cli.ParentCommand; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; diff --git a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupRemoveMemberCommand.java b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupRemoveMemberCommand.java index 1e063a6c29..36166ee568 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/cli/GroupRemoveMemberCommand.java +++ b/scm-webapp/src/main/java/sonia/scm/group/cli/GroupRemoveMemberCommand.java @@ -29,7 +29,6 @@ import picocli.CommandLine; import sonia.scm.cli.ParentCommand; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; -import sonia.scm.repository.cli.GroupCommand; import javax.inject.Inject; import java.util.Arrays; diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBaseCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBaseCommand.java new file mode 100644 index 0000000000..3ec5365261 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBaseCommand.java @@ -0,0 +1,116 @@ +/* + * 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.repository.cli; + +import picocli.CommandLine; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRoleManager; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; +import java.util.function.Predicate; + +class RepositoryPermissionBaseCommand { + + private final RepositoryManager repositoryManager; + private final RepositoryRoleManager roleManager; + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + + @Inject + RepositoryPermissionBaseCommand(RepositoryManager repositoryManager, RepositoryRoleManager roleManager, RepositoryTemplateRenderer templateRenderer) { + this.repositoryManager = repositoryManager; + this.roleManager = roleManager; + this.templateRenderer = templateRenderer; + } + + void modifyRepository(String repositoryName, Predicate modifier) { + NamespaceAndName namespaceAndName; + try { + namespaceAndName = NamespaceAndName.fromString(repositoryName); + } catch (IllegalArgumentException e) { + templateRenderer.renderInvalidInputError(); + return; + } + + Repository repository = repositoryManager.get(namespaceAndName); + if (repository != null) { + if (modifier.test(repository)) { + repositoryManager.modify(repository); + } + } else { + templateRenderer.renderNotFoundError(); + } + } + + void replacePermission(Repository repository, RepositoryPermission permission) { + this.removeExistingPermission(repository, permission.getName(), permission.isGroupPermission()); + repository.addPermission(permission); + } + + void removeExistingPermission(Repository repository, String name, boolean forGroup) { + if (!forGroup) { + repository.findUserPermission(name).ifPresent(repository::removePermission); + } else { + repository.findGroupPermission(name).ifPresent(repository::removePermission); + } + } + + HashSet getPermissionsAsModifiableSet(Repository repository, String name, boolean forGroup) { + return this.getExistingPermissions(repository, name, forGroup) + .map(this::getVerbs) + .map(HashSet::new) + .orElseGet(HashSet::new); + } + + private Optional getExistingPermissions(Repository repo, String name, boolean forGroup) { + if (!forGroup) { + return repo.findUserPermission(name); + } else { + return repo.findGroupPermission(name); + } + } + + private Collection getVerbs(RepositoryPermission permission) { + if (permission.getRole() == null) { + return permission.getVerbs(); + } else { + return roleManager.get(permission.getRole()).getVerbs(); + } + } + + void renderRoleNotFoundError() { + templateRenderer.renderRoleNotFoundError(); + } + + void renderVerbNotFoundError() { + templateRenderer.renderVerbNotFoundError(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBean.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBean.java new file mode 100644 index 0000000000..53cc5fc4a0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionBean.java @@ -0,0 +1,38 @@ +/* + * 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.repository.cli; + +import lombok.Value; + +import java.util.Collection; + +@Value +class RepositoryPermissionBean { + + boolean groupPermission; + String name; + String role; + Collection verbs; +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommand.java new file mode 100644 index 0000000000..e78626071b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommand.java @@ -0,0 +1,109 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.cli.PermissionDescriptionResolver; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRoleManager; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Set; + +import static java.util.Arrays.asList; + +@CommandLine.Command(name = "add-permissions") +@ParentCommand(value = RepositoryCommand.class) +class RepositoryPermissionsAddCommand extends RepositoryPermissionBaseCommand implements Runnable { + + private final PermissionDescriptionResolver permissionDescriptionResolver; + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.add-permissions.repository") + private String repositoryName; + @CommandLine.Parameters(paramLabel = "name", index = "1", descriptionKey = "scm.repo.add-permissions.name") + private String name; + @CommandLine.Parameters(paramLabel = "verbs", index = "2..", arity = "1..", descriptionKey = "scm.repo.add-permissions.verbs") + private String[] verbs = new String[0]; + + @CommandLine.Option(names = {"--group", "-g"}, descriptionKey = "scm.repo.add-permissions.forGroup") + private boolean forGroup; + + @Inject + RepositoryPermissionsAddCommand(RepositoryManager repositoryManager, RepositoryPermissionBaseCommand permissionCommandManager, RepositoryRoleManager roleManager, PermissionDescriptionResolver permissionDescriptionResolver, RepositoryTemplateRenderer templateRenderer) { + super(repositoryManager, roleManager, templateRenderer); + this.permissionDescriptionResolver = permissionDescriptionResolver; + } + + @Override + public void run() { + modifyRepository( + repositoryName, + repository -> { + if (!Arrays.stream(verbs).allMatch(this::verifyVerbExists)) { + return false; + } + Set resultingVerbs = + getPermissionsAsModifiableSet(repository, name, forGroup); + if (resultingVerbs.containsAll(asList(this.verbs))) { + return false; + } + resultingVerbs.addAll(asList(this.verbs)); + replacePermission(repository, new RepositoryPermission(name, resultingVerbs, forGroup)); + return true; + } + ); + } + + private boolean verifyVerbExists(String verb) { + if (permissionDescriptionResolver.getDescription(verb).isEmpty()) { + renderVerbNotFoundError(); + return false; + } + return true; + } + + @VisibleForTesting + void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + @VisibleForTesting + void setName(String name) { + this.name = name; + } + + @VisibleForTesting + void setVerbs(String... verbs) { + this.verbs = verbs; + } + + public void setForGroup(boolean forGroup) { + this.forGroup = forGroup; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommand.java new file mode 100644 index 0000000000..a36d18ae0b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommand.java @@ -0,0 +1,99 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.cli.PermissionDescriptionResolver; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.security.RepositoryPermissionProvider; + +import javax.inject.Inject; +import java.util.List; + +import static java.util.stream.Collectors.toList; + +@CommandLine.Command(name = "available-permissions") +@ParentCommand(value = RepositoryCommand.class) +class RepositoryPermissionsAvailableCommand implements Runnable { + + @CommandLine.Option(names = {"--roles", "-r"}, descriptionKey = "scm.repo.available-permissions.roles-only") + private boolean roles; + @CommandLine.Option(names = {"--verbs", "-v"}, descriptionKey = "scm.repo.available-permissions.verbs-only") + private boolean verbs; + + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + private final RepositoryPermissionProvider repositoryPermissionProvider; + private final PermissionDescriptionResolver permissionDescriptionResolver; + private final RepositoryRoleManager repositoryRoleManager; + + @Inject + public RepositoryPermissionsAvailableCommand(RepositoryTemplateRenderer templateRenderer, RepositoryPermissionProvider repositoryPermissionProvider, PermissionDescriptionResolver permissionDescriptionResolver, RepositoryRoleManager repositoryRoleManager) { + this.templateRenderer = templateRenderer; + this.repositoryPermissionProvider = repositoryPermissionProvider; + this.permissionDescriptionResolver = permissionDescriptionResolver; + this.repositoryRoleManager = repositoryRoleManager; + } + + @VisibleForTesting + void setRoles(boolean roles) { + this.roles = roles; + } + + @VisibleForTesting + void setVerbs(boolean verbs) { + this.verbs = verbs; + } + + @Override + public void run() { + if (roles) { + templateRenderer.renderRoles(getRoleBeans()); + } else if (verbs) { + templateRenderer.renderVerbs(getVerbBeans()); + } else { + templateRenderer.render(getRoleBeans(), getVerbBeans()); + } + } + + private List getVerbBeans() { + return repositoryPermissionProvider.availableVerbs().stream().map(this::createBean).collect(toList()); + } + + private List getRoleBeans() { + return repositoryRoleManager.getAll().stream().map(this::createBean).collect(toList()); + } + + private VerbBean createBean(String verb) { + return new VerbBean(verb, permissionDescriptionResolver.getDescription(verb).orElse(verb)); + } + + private RoleBean createBean(RepositoryRole role) { + return new RoleBean(role.getName(), role.getVerbs()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommand.java new file mode 100644 index 0000000000..c27d26ecc4 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommand.java @@ -0,0 +1,76 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryRoleManager; + +import javax.inject.Inject; + +@CommandLine.Command(name = "clear-permissions") +@ParentCommand(value = RepositoryCommand.class) +class RepositoryPermissionsClearCommand extends RepositoryPermissionBaseCommand implements Runnable { + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.clear-permissions.repository") + private String repositoryName; + @CommandLine.Parameters(paramLabel = "name", index = "1", descriptionKey = "scm.repo.clear-permissions.name") + private String name; + + @CommandLine.Option(names = {"--group", "-g"}, descriptionKey = "scm.repo.clear-permissions.forGroup") + private boolean forGroup; + + @Inject + public RepositoryPermissionsClearCommand(RepositoryManager repositoryManager, RepositoryRoleManager roleManager, RepositoryTemplateRenderer templateRenderer) { + super(repositoryManager, roleManager, templateRenderer); + } + + @Override + public void run() { + modifyRepository( + repositoryName, + repo -> { + removeExistingPermission(repo, name, forGroup); + return true; + } + ); + } + + @VisibleForTesting + void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + @VisibleForTesting + void setName(String name) { + this.name = name; + } + + public void setForGroup(boolean forGroup) { + this.forGroup = forGroup; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsListCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsListCommand.java new file mode 100644 index 0000000000..26771872a5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsListCommand.java @@ -0,0 +1,129 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.CommandValidator; +import sonia.scm.cli.ParentCommand; +import sonia.scm.cli.PermissionDescriptionResolver; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRoleManager; + +import javax.inject.Inject; +import java.util.Collection; +import java.util.stream.Collectors; + +@CommandLine.Command(name = "list-permissions") +@ParentCommand(value = RepositoryCommand.class) +class RepositoryPermissionsListCommand implements Runnable { + + @CommandLine.Mixin + private final RepositoryTemplateRenderer templateRenderer; + @CommandLine.Mixin + private final CommandValidator validator; + private final RepositoryManager manager; + private final RepositoryRoleManager roleManager; + private final PermissionDescriptionResolver permissionDescriptionResolver; + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.list-permissions.repository") + private String repository; + @CommandLine.Option(names = {"--verbose", "-v"}, descriptionKey = "scm.repo.list-permissions.verbose") + private boolean verbose; + @CommandLine.Option(names = {"--keys", "-k"}, descriptionKey = "scm.repo.list-permissions.keys") + private boolean keys; + + @Inject + public RepositoryPermissionsListCommand(RepositoryTemplateRenderer templateRenderer, CommandValidator validator, RepositoryManager manager, RepositoryRoleManager roleManager, PermissionDescriptionResolver permissionDescriptionResolver) { + this.templateRenderer = templateRenderer; + this.validator = validator; + this.manager = manager; + this.roleManager = roleManager; + this.permissionDescriptionResolver = permissionDescriptionResolver; + } + + @VisibleForTesting + void setRepository(String repository) { + this.repository = repository; + } + + @VisibleForTesting + void setVerbose(boolean verbose) { + this.verbose = verbose; + } + + public void setKeys(boolean keys) { + this.keys = keys; + } + + @Override + public void run() { + validator.validate(); + String[] splitRepo = repository.split("/"); + if (splitRepo.length == 2) { + Repository repo = manager.get(new NamespaceAndName(splitRepo[0], splitRepo[1])); + + if (repo != null) { + Collection permissions = + repo.getPermissions().stream().map(this::createPermissionBean).collect(Collectors.toList()); + if (verbose) { + templateRenderer.renderVerbose(permissions); + } else { + templateRenderer.render(permissions); + } + } else { + templateRenderer.renderNotFoundError(); + } + } else { + templateRenderer.renderInvalidInputError(); + } + } + + private RepositoryPermissionBean createPermissionBean(RepositoryPermission permission) { + Collection effectiveVerbs; + if (permission.getRole() == null) { + effectiveVerbs = permission.getVerbs(); + } else { + effectiveVerbs = roleManager.get(permission.getRole()).getVerbs(); + } + return new RepositoryPermissionBean( + permission.isGroupPermission(), + permission.getName(), + permission.getRole() == null? "CUSTOM": permission.getRole(), + keys? effectiveVerbs: getDescriptions(effectiveVerbs) + ); + } + + private Collection getDescriptions(Collection effectiveVerbs) { + return effectiveVerbs.stream().map(this::getDescription).collect(Collectors.toList()); + } + + private String getDescription(String verb) { + return permissionDescriptionResolver.getDescription(verb).orElse(verb); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommand.java new file mode 100644 index 0000000000..0bb68f6b66 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommand.java @@ -0,0 +1,92 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRoleManager; + +import javax.inject.Inject; +import java.util.Set; + +import static java.util.Arrays.asList; + +@CommandLine.Command(name = "remove-permissions") +@ParentCommand(value = RepositoryCommand.class) +class RepositoryPermissionsRemoveCommand extends RepositoryPermissionBaseCommand implements Runnable { + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.remove-permissions.repository") + private String repositoryName; + @CommandLine.Parameters(paramLabel = "name", index = "1", descriptionKey = "scm.repo.remove-permissions.name") + private String name; + @CommandLine.Parameters(paramLabel = "verbs", index = "2..", arity = "1..", descriptionKey = "scm.repo.remove-permissions.verbs") + private String[] verbs = new String[0]; + + @CommandLine.Option(names = {"--group", "-g"}, descriptionKey = "scm.repo.remove-permissions.forGroup") + private boolean forGroup; + + @Inject + public RepositoryPermissionsRemoveCommand(RepositoryManager repositoryManager, RepositoryRoleManager roleManager, RepositoryTemplateRenderer templateRenderer) { + super(repositoryManager, roleManager, templateRenderer); + } + + @Override + public void run() { + modifyRepository( + repositoryName, + repository -> { + Set resultingVerbs = + getPermissionsAsModifiableSet(repository, name, forGroup); + if (resultingVerbs.stream().noneMatch(verb -> asList(verbs).contains(verb))) { + return false; + } + resultingVerbs.removeAll(asList(this.verbs)); + replacePermission(repository, new RepositoryPermission(name, resultingVerbs, forGroup)); + return true; + } + ); + } + + @VisibleForTesting + void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + @VisibleForTesting + void setName(String name) { + this.name = name; + } + + public void setVerbs(String... verbs) { + this.verbs = verbs; + } + + public void setForGroup(boolean forGroup) { + this.forGroup = forGroup; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommand.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommand.java new file mode 100644 index 0000000000..2b4ae8e7cf --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommand.java @@ -0,0 +1,91 @@ +/* + * 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.repository.cli; + +import com.google.common.annotations.VisibleForTesting; +import picocli.CommandLine; +import sonia.scm.cli.ParentCommand; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRoleManager; + +import javax.inject.Inject; + +@CommandLine.Command(name = "set-role") +@ParentCommand(value = RepositoryCommand.class) +class RepositoryPermissionsSetRoleCommand extends RepositoryPermissionBaseCommand implements Runnable { + + private final RepositoryRoleManager roleManager; + + @CommandLine.Parameters(paramLabel = "namespace/name", index = "0", descriptionKey = "scm.repo.set-role.repository") + private String repositoryName; + @CommandLine.Parameters(paramLabel = "name", index = "1", descriptionKey = "scm.repo.set-role.name") + private String name; + @CommandLine.Parameters(paramLabel = "role", index = "2", descriptionKey = "scm.repo.set-role.role") + private String role; + + @CommandLine.Option(names = {"--group", "-g"}, descriptionKey = "scm.repo.set-role.forGroup") + private boolean forGroup; + + @Inject + public RepositoryPermissionsSetRoleCommand(RepositoryManager repositoryManager, RepositoryRoleManager roleManager, RepositoryTemplateRenderer templateRenderer) { + super(repositoryManager, roleManager, templateRenderer); + this.roleManager = roleManager; + } + + @Override + public void run() { + modifyRepository( + repositoryName, + repository -> { + if (roleManager.get(role) == null) { + renderRoleNotFoundError(); + return false; + } + replacePermission(repository, new RepositoryPermission(name, role, forGroup)); + return true; + } + ); + } + + @VisibleForTesting + void setRepositoryName(String repositoryName) { + this.repositoryName = repositoryName; + } + + @VisibleForTesting + void setName(String name) { + this.name = name; + } + + @VisibleForTesting + void setRole(String role) { + this.role = role; + } + + public void setForGroup(boolean forGroup) { + this.forGroup = forGroup; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java index aa6e3cfc38..23e0f47c3d 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryTemplateRenderer.java @@ -24,7 +24,6 @@ package sonia.scm.repository.cli; -import com.google.common.collect.ImmutableMap; import sonia.scm.cli.CliContext; import sonia.scm.cli.ExitCode; import sonia.scm.cli.Table; @@ -33,7 +32,11 @@ import sonia.scm.repository.Repository; import sonia.scm.template.TemplateEngineFactory; import javax.inject.Inject; -import java.util.Collections; +import java.util.Collection; +import java.util.Map; + +import static java.util.Collections.emptyMap; +import static java.util.Map.entry; class RepositoryTemplateRenderer extends TemplateRenderer { @@ -42,9 +45,19 @@ class RepositoryTemplateRenderer extends TemplateRenderer { "{{#cols}}{{value}}{{/cols}}", "{{/rows}}" ); + private static final String TABLE_TEMPLATE = String.join("\n", + "{{#rows}}", + "{{#cols}}{{#row.first}}{{#upper}}{{value}}{{/upper}}{{/row.first}}{{^row.first}}{{value}}{{/row.first}}{{^last}} {{/last}}{{/cols}}", + "{{/rows}}" + ); private static final String INVALID_INPUT_TEMPLATE = "{{i18n.repoInvalidInput}}"; private static final String NOT_FOUND_TEMPLATE = "{{i18n.repoNotFound}}"; + private static final String TYPE_HEADER_KEY = "scm.repo.permissions.type"; + private static final String NAME_HEADER_KEY = "scm.repo.permissions.name"; + private static final String ROLE_HEADER_KEY = "scm.repo.permissions.role"; + private static final String VERBS_HEADER_KEY = "scm.repo.permissions.verbs"; + private final CliContext context; private final RepositoryToRepositoryCommandDtoMapper mapper; @@ -66,17 +79,60 @@ class RepositoryTemplateRenderer extends TemplateRenderer { table.addLabelValueRow("repoLastModified", bean.getLastModified()); table.addLabelValueRow("repoUrl", bean.getUrl()); table.addLabelValueRow("repoDescription", bean.getDescription()); - renderToStdout(DETAILS_TABLE_TEMPLATE, ImmutableMap.of("rows", table, "repo", bean)); + renderToStdout(DETAILS_TABLE_TEMPLATE, Map.of("rows", table, "repo", bean)); + } + + public void render(Collection permissions) { + Table table = createTable(); + table.addHeader(TYPE_HEADER_KEY, NAME_HEADER_KEY, ROLE_HEADER_KEY); + permissions.forEach(permission -> addPermissionToTable(table, permission)); + renderToStdout(TABLE_TEMPLATE, Map.ofEntries(entry("rows", table), entry("permissions", permissions))); + } + + public void renderVerbose(Collection permissions) { + Table table = createTable(); + table.addHeader(TYPE_HEADER_KEY, NAME_HEADER_KEY, ROLE_HEADER_KEY, VERBS_HEADER_KEY); + permissions.forEach(permission -> addVerbosePermissionToTable(table, permission)); + renderToStdout(TABLE_TEMPLATE, Map.ofEntries(entry("rows", table), entry("permissions", permissions))); + } + + private void addPermissionToTable(Table table, RepositoryPermissionBean permission) { + table.addRow( + getBundle().getString(permission.isGroupPermission()? "scm.repo.permissions.isGroup": "scm.repo.permissions.isUser"), + permission.getName(), + permission.getRole() + ); + } + + private void addVerbosePermissionToTable(Table table, RepositoryPermissionBean permission) { + table.addRow( + getBundle().getString(permission.isGroupPermission()? "scm.repo.permissions.isGroup": "scm.repo.permissions.isUser"), + permission.getName(), + permission.getRole(), + String.join(", ", permission.getVerbs()) + ); } public void renderInvalidInputError() { - renderToStderr(INVALID_INPUT_TEMPLATE, Collections.emptyMap()); + renderToStderr(INVALID_INPUT_TEMPLATE, emptyMap()); context.getStderr().println(); context.exit(ExitCode.USAGE); } public void renderNotFoundError() { - renderToStderr(NOT_FOUND_TEMPLATE, Collections.emptyMap()); + renderToStderr(NOT_FOUND_TEMPLATE, emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.NOT_FOUND); + } + + void renderRoleNotFoundError() { + renderToStderr("{{i18n.roleNotFound}}", emptyMap()); + context.getStderr().println(); + context.exit(ExitCode.NOT_FOUND); + } + + void renderVerbNotFoundError() { + renderToStderr("{{i18n.verbNotFound}}", emptyMap()); context.getStderr().println(); context.exit(ExitCode.NOT_FOUND); } @@ -85,4 +141,32 @@ class RepositoryTemplateRenderer extends TemplateRenderer { renderDefaultError(exception); context.exit(ExitCode.SERVER_ERROR); } + + public void renderVerbs(Collection verbs) { + Table table = createTable(); + table.addHeader("scm.repo.permissions.verb", "scm.repo.permissions.description"); + verbs.forEach(verb -> addVerbToTable(table, verb)); + renderToStdout(TABLE_TEMPLATE, Map.ofEntries(entry("rows", table), entry("verbs", verbs))); + } + + private void addVerbToTable(Table table, VerbBean verb) { + table.addRow(verb.getVerb(), verb.getDescription()); + } + + public void renderRoles(Collection roles) { + Table table = createTable(); + table.addHeader(ROLE_HEADER_KEY, VERBS_HEADER_KEY); + roles.forEach(role -> addRoleToTable(table, role)); + renderToStdout(TABLE_TEMPLATE, Map.ofEntries(entry("rows", table), entry("roles", roles))); + } + + private void addRoleToTable(Table table, RoleBean role) { + table.addRow(role.getName(), String.join(", ", role.getVerbs())); + } + + public void render(Collection roles, Collection verbs) { + renderRoles(roles); + renderToStdout("\n", emptyMap()); + renderVerbs(verbs); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java index 927d967169..697624f780 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RepositoryToRepositoryCommandDtoMapper.java @@ -39,7 +39,7 @@ import java.time.format.DateTimeFormatter; import java.util.Optional; @Mapper -public abstract class RepositoryToRepositoryCommandDtoMapper { +abstract class RepositoryToRepositoryCommandDtoMapper { @Inject private RepositoryServiceFactory serviceFactory; diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/RoleBean.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/RoleBean.java new file mode 100644 index 0000000000..e68d627a4d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/RoleBean.java @@ -0,0 +1,36 @@ +/* + * 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.repository.cli; + +import lombok.Value; + +import java.util.Collection; + +@Value +class RoleBean { + + String name; + Collection verbs; +} diff --git a/scm-webapp/src/main/java/sonia/scm/repository/cli/VerbBean.java b/scm-webapp/src/main/java/sonia/scm/repository/cli/VerbBean.java new file mode 100644 index 0000000000..0761fc27e0 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/repository/cli/VerbBean.java @@ -0,0 +1,33 @@ +/* + * 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.repository.cli; + +import lombok.Value; + +@Value +class VerbBean { + String verb; + String description; +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java index ceb1611b35..d2731495b4 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/i18n/I18nServlet.java @@ -74,7 +74,7 @@ public class I18nServlet extends HttpServlet { if (json.isPresent()) { write(response, json.get()); } else { - LOG.debug("could not find translation for lanugage {}", languageCode); + LOG.debug("could not find translation for language {}", languageCode); response.setStatus(HttpServletResponse.SC_NOT_FOUND); } } catch (IOException ex) { diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 1e78dd2855..f43be776bd 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -136,15 +136,15 @@ "verbs": { "repository": { "read": { - "displayName": "Repository Lesen", + "displayName": "Repository lesen", "description": "Darf das Repository im SCM-Manager sehen." }, "modify": { - "displayName": "Repository Modifizieren", + "displayName": "Repository modifizieren", "description": "Darf die Eigenschaften des Repository verändern." }, "delete": { - "displayName": "Repository Löschen", + "displayName": "Repository löschen", "description": "Darf das Repository löschen." }, "rename": { diff --git a/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties b/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties index 7e90be4835..8b31869b60 100644 --- a/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties +++ b/scm-webapp/src/main/resources/sonia/scm/cli/i18n.properties @@ -91,6 +91,52 @@ repoDeletePrompt= If you really want to delete this repository please pass --yes scm.repo.modify.usage.description.0 = Modify repository on server scm.repo.modify.repository = Repository namespace/name +### List repository permissions +scm.repo.list-permissions.usage.description.0 = List permissions for repository +scm.repo.list-permissions.repository = Repository namespace/name +scm.repo.list-permissions.verbose = Show single detailed permissions +scm.repo.list-permissions.keys = Use keys instead of descriptive permission + +scm.repo.available-permissions.usage.description.0 = List available permissions for repository +scm.repo.available-permissions.roles-only = List roles only +scm.repo.available-permissions.verbs-only = List verbs only + +scm.repo.set-role.usage.description.0 = Set role permission for user or group +scm.repo.set-role.repository = Repository namespace/name +scm.repo.set-role.name = User or group name to set role for +scm.repo.set-role.role = Permission role to grant to user or group +scm.repo.set-role.forGroup = Set role for a group, not for a user + +scm.repo.add-permissions.usage.description.0 = Add single permission for user or group +scm.repo.add-permissions.repository = Repository namespace/name +scm.repo.add-permissions.name = User or group name to grant permission to +scm.repo.add-permissions.verbs = Single permissions to grant to user or group +scm.repo.add-permissions.forGroup = Add permission for a group, not for a user + +scm.repo.remove-permissions.usage.description.0 = Revoke single permission from user or group +scm.repo.remove-permissions.repository = Repository namespace/name +scm.repo.remove-permissions.name = User or group name to revoke permission from +scm.repo.remove-permissions.verbs = Single permissions to revoke from user or group +scm.repo.remove-permissions.forGroup = Revoke permission from a group, not for a user + +scm.repo.clear-permission.usage.description.0 = Revoke all permissions from user or group +scm.repo.clear-permission.repository = Repository namespace/name +scm.repo.clear-permission.name = User or group name to revoke permissions from +scm.repo.clear-permission.forGroup = Revoke permissions from a group, not for a user + +scm.repo.permissions.type = Type +scm.repo.permissions.isUser = user +scm.repo.permissions.isGroup = group +scm.repo.permissions.name = Name +scm.repo.permissions.role = Role +scm.repo.permissions.verbs = Verbs +scm.repo.permissions.custom = custom +scm.repo.permissions.verb = Verb +scm.repo.permissions.description = Description + +roleNotFound= Could not find permission role +verbNotFound= Could not find single permission + ## User scm.user.username = Username scm.user.displayName = Display Name diff --git a/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties b/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties index 85a60c99d8..a90d8cc7a5 100644 --- a/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties +++ b/scm-webapp/src/main/resources/sonia/scm/cli/i18n_de.properties @@ -101,6 +101,52 @@ repoDeletePrompt= Wenn dieses Repository endg scm.repo.modify.usage.description.0 = Aktualisiert ein Repository auf dem Server scm.repo.modify.repository = Repository Namespace/Name +### List repository permissions +scm.repo.list-permissions.usage.description.0 = Listet die Berechtigungen für das Repository +scm.repo.list-permissions.repository = Repository Namespace/Name +scm.repo.list-permissions.verbose = Zeigt detaillierte einzelne Berechtigungen +scm.repo.list-permissions.keys = Nutze Schlüssel anstelle der Beschreibungen + +scm.repo.available-permissions.usage.description.0 = Listet die verfügbaren Berechtigungen +scm.repo.available-permissions.roles-only = Zeigt nur die Rollen +scm.repo.available-permissions.verbs-only = Zeigt nur die Einzelberechtigungen + +scm.repo.set-role.usage.description.0 = Setze eine Rolle für einen Benutzer oder eine Gruppe +scm.repo.set-role.repository = Repository Namespace/Name +scm.repo.set-role.name = Name des Benutzers oder der Gruppe für die Berechtigung +scm.repo.set-role.role = Rolle, die für den Benutzer oder die Gruppe gesetzt werden soll +scm.repo.set-role.forGroup = Setzt die Berechtigung für eine Gruppe, nicht für einen Benutzer + +scm.repo.add-permissions.usage.description.0 = Fügt eine einzelne Berechtigung für einen Benutzer oder eine Gruppe hinzu +scm.repo.add-permissions.repository = Repository Namespace/Name +scm.repo.add-permissions.name = Name des Benutzers oder der Gruppe für die Berechtigung +scm.repo.add-permissions.verbs = Einzelne Berechtigungen, die für den Benutzer oder die Gruppe hinzugefügt werden sollen +scm.repo.add-permissions.forGroup = Setzt die Berechtigung für eine Gruppe, nicht für einen Benutzer + +scm.repo.remove-permissions.usage.description.0 = Entzieht einem Benutzer oder einer Gruppe eine einzelne Berechtigung +scm.repo.remove-permissions.repository = Repository Namespace/Name +scm.repo.remove-permissions.name = Name des Benutzers oder der Gruppe, dem/der die Berechtigung entzogen werden soll +scm.repo.remove-permissions.verbs = Einzelne Berechtigungen, die dem Benutzer oder der Gruppe entzogen werden sollen +scm.repo.remove-permissions.forGroup = Entziehe die Berechtigung einer Gruppe, nicht einem Benutzer + +scm.repo.clear-permission.usage.description.0 = Entzieht einem Benutzer oder einer Gruppe alle Berechtigungen +scm.repo.clear-permission.repository = Repository Namespace/Name +scm.repo.clear-permission.name = Name des Benutzers oder der Gruppe, dem/der die Berechtigung entzogen werden soll +scm.repo.clear-permission.forGroup = Entziehe die Berechtigung einer Gruppe, nicht einem Benutzer + +scm.repo.permissions.type = Typ +scm.repo.permissions.isUser = Benutzer +scm.repo.permissions.isGroup = Gruppe +scm.repo.permissions.name = Name +scm.repo.permissions.role = Rolle +scm.repo.permissions.verbs = Verben +scm.repo.permissions.custom = individuell +scm.repo.permissions.verb = Verb +scm.repo.permissions.description = Beschreibung + +roleNotFound= Berechtigungsrolle konnte nicht gefunden werden +verbNotFound= konnte nicht gefunden werden + ## User scm.user.username = Benutzername scm.user.displayName = Anzeigename diff --git a/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java index 0694efbd5c..8913387efd 100644 --- a/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/cli/CliProcessorTest.java @@ -25,8 +25,10 @@ package sonia.scm.cli; import com.google.common.collect.ImmutableList; +import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; +import com.google.inject.Module; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -35,6 +37,7 @@ import org.mockito.Answers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import picocli.CommandLine; +import sonia.scm.i18n.I18nCollector; import sonia.scm.plugin.PluginLoader; import javax.annotation.Nonnull; @@ -44,6 +47,7 @@ import java.util.Locale; import static java.lang.String.format; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -64,6 +68,8 @@ class CliProcessorTest { private CliExecutionExceptionHandler executionExceptionHandler; @Mock private CliParameterExceptionHandler parameterExceptionHandler; + @Mock + private PermissionDescriptionResolverFactory permissionDescriptionResolverFactory; @BeforeEach void mockPluginLoader() { @@ -83,7 +89,7 @@ class CliProcessorTest { @Test void shouldExecutePingCommand() { when(registry.createCommandTree()).thenReturn(ImmutableList.of(new RegisteredCommandNode("ping", PingCommand.class))); - Injector injector = Guice.createInjector(); + Injector injector = Guice.createInjector(new MockedModule()); CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory, pluginLoader); cliProcessor.execute(context, "ping"); @@ -94,7 +100,7 @@ class CliProcessorTest { @Test void shouldExecutePingCommandWithExitCode0() { when(registry.createCommandTree()).thenReturn(ImmutableList.of(new RegisteredCommandNode("ping", PingCommand.class))); - Injector injector = Guice.createInjector(); + Injector injector = Guice.createInjector(new MockedModule()); CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory, pluginLoader); int exitCode = cliProcessor.execute(context, "ping"); @@ -177,7 +183,7 @@ class CliProcessorTest { ByteArrayOutputStream baos = new ByteArrayOutputStream(); when(context.getStdout()).thenReturn(new PrintWriter(baos)); - Injector injector = Guice.createInjector(); + Injector injector = Guice.createInjector(new MockedModule()); CliProcessor cliProcessor = new CliProcessor(registry, injector, exceptionHandlerFactory, pluginLoader); cliProcessor.execute(context, args); @@ -210,4 +216,14 @@ class CliProcessorTest { } } + + static class MockedModule implements Module { + + @Override + public void configure(Binder binder) { + I18nCollector i18nCollector = mock(I18nCollector.class); + binder.bind(PermissionDescriptionResolverFactory.class) + .toInstance(new PermissionDescriptionResolverFactory(i18nCollector)); + } + } } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java index ebbff19f2d..555f6a3ac6 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryDeleteCommandTest.java @@ -29,7 +29,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import sonia.scm.cli.CliContext; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java index 07d12059cc..4f2a42b305 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryGetCommandTest.java @@ -33,10 +33,7 @@ import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryTestData; -import sonia.scm.repository.cli.RepositoryGetCommand; -import sonia.scm.repository.cli.RepositoryTemplateRenderer; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java index 33481e0cec..45117539ba 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryListCommandTest.java @@ -24,7 +24,6 @@ package sonia.scm.repository.cli; -import com.google.common.collect.ImmutableMap; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; @@ -41,8 +40,6 @@ import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommandTest.java new file mode 100644 index 0000000000..ddf945befe --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAddCommandTest.java @@ -0,0 +1,226 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.cli.PermissionDescriptionResolver; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.repository.RepositoryTestData; + +import java.util.List; +import java.util.Set; + +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +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 RepositoryPermissionsAddCommandTest { + + @Mock + private RepositoryManager repositoryManager; + @Mock + private RepositoryRoleManager roleManager; + @Mock + private PermissionDescriptionResolver permissionDescriptionResolver; + @Mock + private RepositoryTemplateRenderer templateRenderer; + + @InjectMocks + private RepositoryPermissionsAddCommand command; + + private final Repository repository = RepositoryTestData.createHeartOfGold(); + + @Nested + class ForExistingRepository { + + @BeforeEach + void mockRepository() { + when(repositoryManager.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))) + .thenReturn(repository); + } + + @Nested + class ForExistingVerbs { + + @BeforeEach + void mockVerbs() { + when(permissionDescriptionResolver.getDescription(anyString())) + .thenAnswer(invocation -> of(invocation.getArgument(0, String.class))); + } + + @Test + void shouldSetMultipleVerbsForNewUser() { + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setVerbs("read", "pull", "push"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("trillian", Set.of("read", "pull", "push"), false)); + return true; + })); + } + + @Test + void shouldAddNewVerbToExistingVerbsForUser() { + repository.setPermissions( + List.of( + new RepositoryPermission("trillian", List.of("read"), false) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setVerbs("write"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("trillian", Set.of("read", "write"), false)); + return true; + })); + } + + @Test + void shouldAddNewVerbToExistingVerbsForGroup() { + repository.setPermissions( + List.of( + new RepositoryPermission("hog", List.of("read"), true) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("hog"); + command.setVerbs("write"); + command.setForGroup(true); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("hog", Set.of("read", "write"), true)); + return true; + })); + } + + @Test + void shouldAddNewVerbToRoleAndReplaceRoleWithCustomPermissionsForUser() { + repository.setPermissions( + List.of( + new RepositoryPermission("trillian", "READ", false) + ) + ); + when(roleManager.get("READ")) + .thenReturn(new RepositoryRole("READ", List.of("read", "pull"), "")); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setVerbs("write"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("trillian", Set.of("pull", "read", "write"), false)); + return true; + })); + } + + @Test + void shouldNotModifyRoleIfNewVerbIsPartOfRole() { + repository.setPermissions( + List.of( + new RepositoryPermission("trillian", "READ", false) + ) + ); + when(roleManager.get("READ")) + .thenReturn(new RepositoryRole("READ", List.of("read", "pull"), "")); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setVerbs("read"); + + command.run(); + + verify(repositoryManager, never()).modify(any()); + } + } + + @Test + void shouldHandleMissingVerb() { + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setVerbs("make-party"); + + command.run(); + + verify(templateRenderer).renderVerbNotFoundError(); + } + } + + @Test + void shouldHandleIllegalNamespaceNameParameter() { + command.setRepositoryName("illegal name"); + command.setName("trillian"); + command.setVerbs("write"); + + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + } + + @Test + void shouldHandleNotExistingRepository() { + command.setRepositoryName("no/repository"); + command.setName("trillian"); + command.setVerbs("write"); + + command.run(); + + verify(templateRenderer).renderNotFoundError(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommandTest.java new file mode 100644 index 0000000000..1cf30dc173 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsAvailableCommandTest.java @@ -0,0 +1,153 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.PermissionDescriptionResolver; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.security.RepositoryPermissionProvider; + +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.emptyList; +import static java.util.Optional.empty; +import static java.util.Optional.of; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryPermissionsAvailableCommandTest { + + @Mock + private RepositoryTemplateRenderer templateRenderer; + @Mock + private RepositoryPermissionProvider repositoryPermissionProvider; + @Mock + private PermissionDescriptionResolver permissionDescriptionResolver; + @Mock + private RepositoryRoleManager repositoryRoleManager; + + @InjectMocks + private RepositoryPermissionsAvailableCommand command; + + @Captor + private ArgumentCaptor> verbsCaptor; + @Captor + private ArgumentCaptor> rolesCaptor; + + @Test + void shouldListVerbs() { + doNothing().when(templateRenderer).render(any(), verbsCaptor.capture()); + when(repositoryPermissionProvider.availableVerbs()) + .thenReturn(List.of("read", "write")); + when(permissionDescriptionResolver.getDescription("read")) + .thenReturn(of("read repository")); + when(permissionDescriptionResolver.getDescription("write")) + .thenReturn(of("write repository")); + + command.run(); + + Collection capturedVerbs = verbsCaptor.getValue(); + + assertThat(capturedVerbs). + extracting("verb") + .containsExactly("read", "write"); + assertThat(capturedVerbs). + extracting("description") + .containsExactly("read repository", "write repository"); + } + + @Test + void shouldHandleMissingDescription() { + doNothing().when(templateRenderer).render(any(), verbsCaptor.capture()); + when(repositoryPermissionProvider.availableVerbs()) + .thenReturn(List.of("unknown")); + when(permissionDescriptionResolver.getDescription("unknown")) + .thenReturn(empty()); + + command.run(); + + Collection capturedVerbs = verbsCaptor.getValue(); + + assertThat(capturedVerbs). + extracting("verb") + .containsExactly("unknown"); + assertThat(capturedVerbs). + extracting("description") + .containsExactly("unknown"); + } + + @Test + void shouldRenderRoles() { + doNothing().when(templateRenderer).render(rolesCaptor.capture(), any()); + when(repositoryRoleManager.getAll()) + .thenReturn(List.of(new RepositoryRole("READ", List.of("read", "pull"), null))); + + command.run(); + + Collection capturedRoles = rolesCaptor.getValue(); + + assertThat(capturedRoles) + .extracting("name") + .containsExactly("READ"); + assertThat(capturedRoles) + .extracting("verbs") + .map(c -> ((Collection) c).stream().collect(toList())) // to satisfy equal in the comparison, we have to use this form + .containsExactly(List.of("read", "pull")); + } + + @Test + void shouldRenderRolesOnlyWithFlag() { + command.setRoles(true); + + command.run(); + + verify(templateRenderer).renderRoles(emptyList()); + verify(templateRenderer, never()).renderVerbs(any()); + } + + @Test + void shouldRenderVerbsOnlyWithFlag() { + command.setVerbs(true); + + command.run(); + + verify(templateRenderer).renderVerbs(emptyList()); + verify(templateRenderer, never()).renderRoles(any()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommandTest.java new file mode 100644 index 0000000000..8ca98a6512 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsClearCommandTest.java @@ -0,0 +1,128 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryTestData; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryPermissionsClearCommandTest { + + @Mock + private RepositoryManager repositoryManager; + @Mock + private RepositoryTemplateRenderer templateRenderer; + + @InjectMocks + private RepositoryPermissionsClearCommand command; + + private final Repository repository = RepositoryTestData.createHeartOfGold(); + + @Nested + class ForExistingRepository { + + @BeforeEach + void mockRepository() { + when(repositoryManager.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))) + .thenReturn(repository); + } + + @Test + void shouldClearPermissionsForUser() { + repository.setPermissions( + List.of( + new RepositoryPermission("trillian", List.of("read"), false) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).isEmpty(); + return true; + })); + } + + @Test + void shouldClearPermissionsForGroup() { + repository.setPermissions( + List.of( + new RepositoryPermission("hog", "READ", true) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("hog"); + command.setForGroup(true); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).isEmpty(); + return true; + })); + } + } + + @Test + void shouldHandleIllegalNamespaceNameParameter() { + command.setRepositoryName("illegal name"); + command.setName("trillian"); + + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + } + + @Test + void shouldHandleNotExistingRepository() { + command.setRepositoryName("no/repository"); + command.setName("trillian"); + + command.run(); + + verify(templateRenderer).renderNotFoundError(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsListCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsListCommandTest.java new file mode 100644 index 0000000000..6ec43f8bde --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsListCommandTest.java @@ -0,0 +1,195 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.cli.CommandValidator; +import sonia.scm.cli.PermissionDescriptionResolver; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.repository.RepositoryTestData; + +import java.util.Collection; +import java.util.List; + +import static java.util.Optional.of; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class RepositoryPermissionsListCommandTest { + + @Mock + private RepositoryTemplateRenderer templateRenderer; + @Mock + private CommandValidator validator; + @Mock + private RepositoryManager manager; + @Mock + private RepositoryRoleManager roleManager; + @Mock + private PermissionDescriptionResolver permissionDescriptionResolver; + + @InjectMocks + private RepositoryPermissionsListCommand command; + + @Test + void shouldPrintNotFoundErrorForUnknownRepository() { + command.setRepository("hg2g/hog"); + + command.run(); + + verify(templateRenderer).renderNotFoundError(); + } + + @Nested + class ForExistingRepository { + + @Captor + private ArgumentCaptor> permissionsCaptor; + + private final Repository repository = RepositoryTestData.createHeartOfGold(); + + @BeforeEach + void mockRepository() { + when(manager.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))) + .thenReturn(repository); + command.setRepository("hitchhiker/HeartOfGold"); + } + + @Nested + class WithoutVerboseFlag { + + @BeforeEach + void setUpRenderer() { + doNothing().when(templateRenderer).render(permissionsCaptor.capture()); + } + + @Test + void shouldRenderEmptyTableWithoutPermissions() { + command.run(); + + Collection beans = permissionsCaptor.getValue(); + assertThat(beans).isEmpty(); + } + + @Test + void shouldListCustomUserPermission() { + RepositoryPermission permission = new RepositoryPermission("trillian", List.of("read", "write"), false); + repository.setPermissions(List.of(permission)); + when(permissionDescriptionResolver.getDescription("read")) + .thenReturn(of("read repository")); + when(permissionDescriptionResolver.getDescription("write")) + .thenReturn(of("write repository")); + + command.run(); + + Collection beans = permissionsCaptor.getValue(); + assertThat(beans).extracting("groupPermission", "name", "role") + .containsExactly(tuple(false, "trillian", "CUSTOM")); + } + } + + @Nested + class WithVerboseFlag { + + @BeforeEach + void setUpRenderer() { + doNothing().when(templateRenderer).renderVerbose(permissionsCaptor.capture()); + } + + @BeforeEach + void setVerbose() { + command.setVerbose(true); + } + + @Test + void shouldListUserPermissionWithVerbs() { + RepositoryPermission permission = new RepositoryPermission("trillian", List.of("read", "write"), false); + repository.setPermissions(List.of(permission)); + when(permissionDescriptionResolver.getDescription("read")) + .thenReturn(of("read repository")); + when(permissionDescriptionResolver.getDescription("write")) + .thenReturn(of("write repository")); + + command.run(); + + Collection beans = permissionsCaptor.getValue(); + assertThat(beans).extracting("groupPermission", "name", "role", "verbs") + .containsExactly(tuple(false, "trillian", "CUSTOM", List.of("read repository", "write repository"))); + } + + @Test + void shouldListUserPermissionWithRole() { + RepositoryPermission permission = new RepositoryPermission("trillian", "READ", false); + repository.setPermissions(List.of(permission)); + when(roleManager.get("READ")) + .thenReturn(new RepositoryRole("READ", List.of("read", "pull"), "")); + when(permissionDescriptionResolver.getDescription("read")) + .thenReturn(of("read repository")); + when(permissionDescriptionResolver.getDescription("pull")) + .thenReturn(of("clone/checkout repository")); + + command.run(); + + Collection beans = permissionsCaptor.getValue(); + assertThat(beans).extracting("groupPermission", "name", "verbs") + .containsExactly(tuple(false, "trillian", List.of("read repository", "clone/checkout repository"))); + } + + @Test + void shouldListUserPermissionWithVerbsAsKeys() { + RepositoryPermission permission = new RepositoryPermission("trillian", List.of("read", "write"), false); + repository.setPermissions(List.of(permission)); + + command.setKeys(true); + command.run(); + + Collection beans = permissionsCaptor.getValue(); + assertThat(beans).extracting("groupPermission", "name", "role") + .containsExactly(tuple(false, "trillian", "CUSTOM")); + assertThat(beans).extracting("verbs") + .map(c -> ((Collection) c).stream().collect(toList())) // to satisfy equal in the comparison, we have to use this form + .containsExactly(List.of("read", "write")); + } + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommandTest.java new file mode 100644 index 0000000000..cbed86d911 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsRemoveCommandTest.java @@ -0,0 +1,182 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.repository.RepositoryTestData; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +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 RepositoryPermissionsRemoveCommandTest { + + @Mock + private RepositoryManager repositoryManager; + @Mock + private RepositoryRoleManager roleManager; + @Mock + private RepositoryTemplateRenderer templateRenderer; + + @InjectMocks + private RepositoryPermissionsRemoveCommand command; + + private final Repository repository = RepositoryTestData.createHeartOfGold(); + + @Nested + class ForExistingRepository { + + @BeforeEach + void mockRepository() { + when(repositoryManager.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))) + .thenReturn(repository); + } + + @Test + void shouldRemoveMultipleVerbsFromExistingVerbsForUser() { + repository.setPermissions( + List.of( + new RepositoryPermission("dent", List.of("read", "write", "push", "pull"), false) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("dent"); + command.setVerbs("write", "push", "pull"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("dent", Set.of("read"), false)); + return true; + })); + } + + @Test + void shouldRemoveNewVerbFromExistingVerbsForGroup() { + repository.setPermissions( + List.of( + new RepositoryPermission("hog", List.of("read", "write"), true) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("hog"); + command.setVerbs("write"); + command.setForGroup(true); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("hog", Set.of("read"), true)); + return true; + })); + } + + @Test + void shouldRemoveNewVerbToRoleAndReplaceRoleWithCustomPermissionsForUser() { + repository.setPermissions( + List.of( + new RepositoryPermission("dent", "READ", false) + ) + ); + when(roleManager.get("READ")) + .thenReturn(new RepositoryRole("READ", List.of("read", "pull"), "")); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("dent"); + command.setVerbs("pull"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "verbs", "groupPermission") + .containsExactly(tuple("dent", Set.of("read"), false)); + return true; + })); + } + + @Test + void shouldNotModifyRepositoryIfVerbsAreNotSet() { + repository.setPermissions( + List.of( + new RepositoryPermission("dent", List.of("read", "write"), false) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("dent"); + command.setVerbs("push", "pull"); + + command.run(); + + verify(repositoryManager, never()).modify(any()); + } + } + + @Test + void shouldHandleIllegalNamespaceNameParameter() { + command.setRepositoryName("illegal name"); + command.setName("trillian"); + command.setVerbs("write"); + + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + } + + @Test + void shouldHandleNotExistingRepository() { + command.setRepositoryName("no/repository"); + command.setName("trillian"); + command.setVerbs("write"); + + command.run(); + + verify(templateRenderer).renderNotFoundError(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommandTest.java b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommandTest.java new file mode 100644 index 0000000000..a35fd93682 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/cli/RepositoryPermissionsSetRoleCommandTest.java @@ -0,0 +1,196 @@ +/* + * 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.repository.cli; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +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.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryPermission; +import sonia.scm.repository.RepositoryRole; +import sonia.scm.repository.RepositoryRoleManager; +import sonia.scm.repository.RepositoryTestData; + +import java.util.List; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +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 RepositoryPermissionsSetRoleCommandTest { + + @Mock + private RepositoryManager repositoryManager; + @Mock + private RepositoryRoleManager roleManager; + @Mock + private RepositoryTemplateRenderer templateRenderer; + + @InjectMocks + private RepositoryPermissionsSetRoleCommand command; + + private final Repository repository = RepositoryTestData.createHeartOfGold(); + + @Nested + class ForExistingRepository { + + @BeforeEach + void mockRepository() { + when(repositoryManager.get(new NamespaceAndName("hitchhiker", "HeartOfGold"))) + .thenReturn(repository); + } + + @Nested + class ForExistingRole { + + @BeforeEach + void mockRole() { + when(roleManager.get(any())).thenAnswer( + invocation -> new RepositoryRole(invocation.getArgument(0, String.class), emptyList(), null)); + } + + @Test + void shouldSetRoleForUser() { + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setRole("OWNER"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "role", "groupPermission") + .containsExactly(tuple("trillian", "OWNER", false)); + return true; + })); + } + + @Test + void shouldSetRoleForGroup() { + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("crew"); + command.setRole("READ"); + command.setForGroup(true); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "role", "groupPermission") + .containsExactly(tuple("crew", "READ", true)); + return true; + })); + } + + @Test + void shouldReplaceRepositoryPermissionForUser() { + repository.setPermissions( + List.of( + new RepositoryPermission("trillian", List.of("read"), false) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setRole("OWNER"); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "role", "groupPermission") + .containsExactly(tuple("trillian", "OWNER", false)); + return true; + })); + } + + @Test + void shouldReplaceRepositoryPermissionForGroup() { + repository.setPermissions( + List.of( + new RepositoryPermission("trillian", List.of("read"), true) + ) + ); + + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setRole("OWNER"); + command.setForGroup(true); + + command.run(); + + verify(repositoryManager).modify(argThat(argument -> { + assertThat(argument.getPermissions()).extracting("name", "role", "groupPermission") + .containsExactly(tuple("trillian", "OWNER", true)); + return true; + })); + } + } + + @Test + void shouldHandleMissingRole() { + command.setRepositoryName("hitchhiker/HeartOfGold"); + command.setName("trillian"); + command.setRole("FUNNY"); + + command.run(); + + verify(templateRenderer).renderRoleNotFoundError(); + } + } + + @Test + void shouldHandleIllegalNamespaceNameParameter() { + command.setRepositoryName("illegal name"); + command.setName("trillian"); + command.setRole("READ"); + + command.run(); + + verify(templateRenderer).renderInvalidInputError(); + verify(repositoryManager, never()).modify(any()); + } + + @Test + void shouldHandleNotExistingRepository() { + command.setRepositoryName("no/repository"); + command.setName("trillian"); + command.setRole("READ"); + + command.run(); + + verify(templateRenderer).renderNotFoundError(); + verify(repositoryManager, never()).modify(any()); + } +}