Add cli commands to modify repository permissions (#2090)

Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
This commit is contained in:
René Pfeuffer
2022-07-20 09:17:14 +02:00
committed by GitHub
parent fc28da90b3
commit 3b4b1a1767
52 changed files with 2289 additions and 44 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -33,7 +33,7 @@ import java.util.Map;
import java.util.stream.Collectors;
@Singleton
public class CommandRegistry {
class CommandRegistry {
private final RegisteredCommandCollector commandCollector;

View File

@@ -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;

View File

@@ -27,4 +27,4 @@ package sonia.scm.cli;
import picocli.CommandLine;
@CommandLine.Command(name = "logout")
public class LogoutCommand {}
class LogoutCommand {}

View File

@@ -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);
}

View File

@@ -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<String> 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<String> getVerbDescriptionFromI18nBundle(String verb) {
Optional<JsonNode> 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());
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<RegisteredCommandNode> children = new ArrayList<>();

View File

@@ -27,4 +27,4 @@ package sonia.scm.cli;
import picocli.CommandLine;
@CommandLine.Command(name = "scm")
public class ScmManagerCommand {}
class ScmManagerCommand {}

View File

@@ -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}}");

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<Repository> 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<String> getPermissionsAsModifiableSet(Repository repository, String name, boolean forGroup) {
return this.getExistingPermissions(repository, name, forGroup)
.map(this::getVerbs)
.map(HashSet::new)
.orElseGet(HashSet::new);
}
private Optional<RepositoryPermission> getExistingPermissions(Repository repo, String name, boolean forGroup) {
if (!forGroup) {
return repo.findUserPermission(name);
} else {
return repo.findGroupPermission(name);
}
}
private Collection<String> 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();
}
}

View File

@@ -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<String> verbs;
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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<VerbBean> getVerbBeans() {
return repositoryPermissionProvider.availableVerbs().stream().map(this::createBean).collect(toList());
}
private List<RoleBean> 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());
}
}

View File

@@ -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;
}
}

View File

@@ -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<RepositoryPermissionBean> 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<String> 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<String> getDescriptions(Collection<String> effectiveVerbs) {
return effectiveVerbs.stream().map(this::getDescription).collect(Collectors.toList());
}
private String getDescription(String verb) {
return permissionDescriptionResolver.getDescription(verb).orElse(verb);
}
}

View File

@@ -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<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<RepositoryPermissionBean> 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<RepositoryPermissionBean> 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<VerbBean> 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<RoleBean> 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<RoleBean> roles, Collection<VerbBean> verbs) {
renderRoles(roles);
renderToStdout("\n", emptyMap());
renderVerbs(verbs);
}
}

View File

@@ -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;

View File

@@ -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<String> verbs;
}

View File

@@ -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;
}

View File

@@ -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) {