#44 - Added infrastructure to prevent passwords from being printed to the console.

Introduced value objects to properly abstract command line arguments and introduced an interface Masked that allows masking a String value when printed to the console.
This commit is contained in:
Oliver Gierke
2017-04-03 17:02:30 +02:00
parent a960ea51a2
commit 0e4f194e81
9 changed files with 489 additions and 70 deletions

View File

@@ -0,0 +1,254 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.data.release.model.Masked;
import org.springframework.util.Assert;
/**
* Value object to represent a Maven command line.
*
* @author Oliver Gierke
*/
@Value
class CommandLine {
@NonNull List<Goal> goals;
@NonNull List<Argument> arguments;
/**
* Creates a new {@link CommandLine} for the given {@link Goal} and {@link Argument}s.
*
* @param goal must not be {@literal null}.
* @param argument must not be {@literal null}.
* @return
*/
public static CommandLine of(Goal goal, Argument... argument) {
return new CommandLine(Collections.singletonList(goal), Arrays.asList(argument));
}
/**
* Creates a new {@link CommandLine} for the given {@link Goal}s and {@link Argument}s.
*
* @param goal must not be {@literal null}.
* @param argument must not be {@literal null}.
* @return
*/
public static CommandLine of(Goal first, Goal second, Argument... argument) {
return new CommandLine(Arrays.asList(first, second), Arrays.asList(argument));
}
/**
* Returns a new {@link CommandLine} with the given {@link Argument} added in case the given {@link BooleanSupplier}
* evaluates to {@literal true}.
*
* @param argument must not be {@literal null}.
* @param condition must not be {@literal null}.
* @return
*/
public CommandLine conditionalAnd(Argument argument, BooleanSupplier condition) {
return condition.getAsBoolean() ? and(argument) : this;
}
/**
* Returns a new {@link CommandLine} with the given {@link Argument} added.
*
* @param argument must not be {@literal null}.
* @return
*/
public CommandLine and(Argument argument) {
Assert.notNull(argument, "Argument must not be null!");
List<Argument> newArguments = new ArrayList<Argument>(arguments.size() + 1);
newArguments.addAll(arguments);
newArguments.add(argument);
return new CommandLine(goals, newArguments);
}
/**
* Renders the current {@link CommandLine} as a plain {@link List} of {@link String}s using the given {@link Function}
* to expand the {@link Goal}s.
*
* @param goalExpansion must not be {@literal null}.
* @return
*/
public List<String> toCommandLine(Function<Goal, String> goalExpansion) {
Stream<String> goalStream = goals.stream().map(goalExpansion);
Stream<String> argumentStream = arguments.stream().map(it -> it.toCommandLineArgument());
return Stream.concat(goalStream, argumentStream).collect(Collectors.toList());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
Stream<String> goalStream = goals.stream().map(it -> it.getGoal());
Stream<String> argumentStream = arguments.stream().map(Object::toString);
return Stream.concat(goalStream, argumentStream).collect(Collectors.joining(" "));
}
/**
* Represents a Maven goal to invoke. Can be a custom one but also one of the predefined instances.
*
* @author Oliver Gierke
*/
@Value(staticConstructor = "goal")
public static class Goal {
public static final Goal CLEAN = Goal.goal("clean");
public static final Goal INSTALL = Goal.goal("install");
public static final Goal DEPLOY = Goal.goal("deploy");
public static final Goal VALIDATE = Goal.goal("validate");
String goal;
}
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public static class Argument {
public static Argument SKIP_TESTS = Argument.arg("skipTests");
@NonNull String name;
@NonNull Optional<ArgumentValue<?>> value;
private Argument(String name, ArgumentValue<?> value) {
this(name, Optional.of(value));
}
static Argument of(String name) {
return new Argument(name, Optional.empty());
}
/**
* Enables the given comma-separated profiles for the {@link CommandLine}.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public static Argument profile(String name, String... others) {
Assert.hasText(name, "Profiles must not be null or empty!");
Assert.notNull(others, "Other profiles must not be null!");
String profiles = Stream.concat(Stream.of(name), Arrays.stream(others)).collect(Collectors.joining(","));
return Argument.of("-P".concat(profiles));
}
public static Argument arg(String name) {
return Argument.of("-D".concat(name));
}
public Argument withValue(Object value) {
return new Argument(name, ArgumentValue.of(value));
}
public Argument withQuotedValue(Object value) {
return new Argument(name, ArgumentValue.of(value, it -> String.format("\"%s\"", it.toString())));
}
public Argument withValue(Masked masked) {
return new Argument(name, ArgumentValue.of(masked));
}
public String toCommandLineArgument() {
return toNameValuePair(value.map(ArgumentValue::toCommandLine));
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return toNameValuePair(value.map(Object::toString));
}
private String toNameValuePair(Optional<String> source) {
return source//
.map(it -> String.format("%s=%s", name, it))//
.orElse(name);
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static class ArgumentValue<T> {
private final @NonNull T value;
private final @NonNull Optional<Function<T, String>> preparer;
private final @NonNull Optional<Function<T, String>> toString;
public static <T> ArgumentValue<T> of(T value) {
return new ArgumentValue<>(value, Optional.empty(), Optional.empty());
}
public static <T> ArgumentValue<T> of(T value, Function<T, String> preparer) {
return new ArgumentValue<>(value, Optional.of(preparer), Optional.empty());
}
/**
* Returns an {@link ArgumentValue} for the given {@link Masked} value.
*
* @param masked must not be {@literal null}.
* @return
*/
public static <T extends Masked> ArgumentValue<T> of(T masked) {
return new ArgumentValue<>(masked, Optional.empty(), Optional.of(it -> it.masked()));
}
/**
* Returns the {@link String} variant of the argument value.
*
* @return
*/
public String toCommandLine() {
return preparer.map(it -> it.apply(value)).orElseGet(() -> value.toString());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return toString.map(it -> it.apply(value)).orElseGet(() -> toCommandLine());
}
}
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.data.release.build;
import static org.springframework.data.release.build.CommandLine.Argument.*;
import static org.springframework.data.release.build.CommandLine.Goal.*;
import static org.springframework.data.release.model.Projects.*;
import lombok.AccessLevel;
@@ -22,11 +24,11 @@ import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.springframework.core.annotation.Order;
import org.springframework.data.release.build.CommandLine.Argument;
import org.springframework.data.release.build.CommandLine.Goal;
import org.springframework.data.release.deployment.DefaultDeploymentInformation;
import org.springframework.data.release.deployment.DeploymentInformation;
import org.springframework.data.release.deployment.DeploymentProperties;
@@ -109,7 +111,8 @@ class MavenBuildSystem implements BuildSystem {
logger.log(project, "Triggering distribution build…");
mvn.execute(project, "clean", "deploy", "-DskipTests", "-Pdistribute", "-B");
mvn.execute(project, CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
SKIP_TESTS, profile("distribute"), Argument.of("-B")));
logger.log(project, "Successfully finished distribution build!");
@@ -186,17 +189,8 @@ class MavenBuildSystem implements BuildSystem {
logger.log(project, "Triggering distribution build…");
ArtifactVersion version = ArtifactVersion.of(module);
String profile = "-Pdistribute";
if (version.isMilestoneVersion()) {
profile = profile.concat(",milestone");
} else if (version.isReleaseVersion()) {
profile = profile.concat(",release");
}
mvn.execute(project, "clean", "deploy", "-DskipTests", profile);
mvn.execute(project,
CommandLine.of(Goal.CLEAN, Goal.DEPLOY, ReleaseVersion.of(module).getDistributionProfiles(), SKIP_TESTS));
logger.log(project, "Successfully finished distribution build!");
});
@@ -212,17 +206,18 @@ class MavenBuildSystem implements BuildSystem {
Project project = module.getProject();
UpdateInformation information = UpdateInformation.of(module.getTrainIteration(), phase);
mvn.execute(project, "versions:set", "versions:commit",
"-DnewVersion=".concat(information.getProjectVersionToSet(project).toString()));
CommandLine goals = CommandLine.of(goal("versions:set"), goal("versions:commit"));
mvn.execute(project, goals.and(arg("newVersion").withValue(information.getProjectVersionToSet(project))));
if (BUILD.equals(project)) {
mvn.execute(project, "versions:set", //
"-DnewVersion=".concat(information.getReleaseTrainVersion()), //
"-DgroupId=org.springframework.data", //
"-DartifactId=spring-data-releasetrain");
mvn.execute(project,
goals.and(arg("newVersion").withValue(information.getReleaseTrainVersion()))//
.and(arg("groupId").withValue("org.springframework.data"))//
.and(arg("artifactId").withValue("spring-data-releasetrain")));
mvn.execute(project, "install");
mvn.execute(project, CommandLine.of(Goal.INSTALL));
}
return module;
@@ -252,13 +247,8 @@ class MavenBuildSystem implements BuildSystem {
@Override
public ModuleIteration triggerBuild(ModuleIteration module) {
List<String> arguments = new ArrayList<>();
arguments.add("clean");
arguments.add("install");
if (module.getProject().skipTests()) {
arguments.add("-DskipTests");
}
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.INSTALL)//
.conditionalAnd(SKIP_TESTS, () -> module.getProject().skipTests());
mvn.execute(module.getProject(), arguments);
@@ -271,7 +261,7 @@ class MavenBuildSystem implements BuildSystem {
*/
public ModuleIteration triggerPreReleaseCheck(ModuleIteration module) {
mvn.execute(module.getProject(), "clean", "validate", "-Ppre-release");
mvn.execute(module.getProject(), CommandLine.of(Goal.CLEAN, Goal.VALIDATE, profile("pre-release")));
return module;
}
@@ -298,18 +288,15 @@ class MavenBuildSystem implements BuildSystem {
logger.log(module, "Deploying artifacts to Spring Artifactory…");
List<String> arguments = new ArrayList<>();
arguments.add("clean");
arguments.add("deploy");
arguments.add("-Pci,release");
arguments.add("-DskipTests");
arguments.add("-Dartifactory.server=".concat(properties.getServer().getUri()));
arguments.add("-Dartifactory.staging-repository=".concat(properties.getStagingRepository()));
arguments.add("-Dartifactory.username=".concat(properties.getUsername()));
arguments.add("-Dartifactory.password=".concat(properties.getPassword()));
arguments.add("-Dartifactory.build-name=\"".concat(information.getBuildName()).concat("\""));
arguments.add("-Dartifactory.build-number=".concat(information.getBuildNumber()));
CommandLine arguments = CommandLine.of(Goal.CLEAN, Goal.DEPLOY, //
profile("ci,release"), //
SKIP_TESTS, //
arg("artifactory.server").withValue(properties.getServer().getUri()),
arg("artifactory.staging-repository").withValue(properties.getStagingRepository()),
arg("artifactory.username").withValue(properties.getUsername()),
arg("artifactory.password").withValue(properties.getPassword()),
arg("artifactory.build-name").withQuotedValue(information.getBuildName()),
arg("artifactory.build-number").withValue(information.getBuildNumber()));
mvn.execute(module.getProject(), arguments);
}
@@ -332,16 +319,13 @@ class MavenBuildSystem implements BuildSystem {
logger.log(module, "Deploying artifacts to Sonatype OSS Nexus…");
List<String> arguments = new ArrayList<>();
arguments.add("deploy");
arguments.add("-Pci,central");
arguments.add("-DskipTests");
Gpg gpg = properties.getGpg();
arguments.add("-Dgpg.executable=".concat(gpg.getExecutable()));
arguments.add("-Dgpg.keyname=".concat(gpg.getKeyname()));
arguments.add("-Dgpg.password=".concat(gpg.getPassword()));
CommandLine arguments = CommandLine.of(Goal.DEPLOY, //
profile("ci,central"), //
SKIP_TESTS, //
arg("gpg.executable").withValue(gpg.getExecutable()), arg("gpg.keyname").withValue(gpg.getKeyname()),
arg("gpg.password").withValue(gpg.getPassword()));
mvn.execute(module.getProject(), arguments);
}
@@ -371,4 +355,42 @@ class MavenBuildSystem implements BuildSystem {
throw new RuntimeException(o_O);
}
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static class ReleaseVersion {
private final ArtifactVersion version;
/**
* Creates a new {@link ReleaseVersion} for the given {@link ModuleIteration}.
*
* @param module must not be {@literal null}.
* @return
*/
public static ReleaseVersion of(ModuleIteration module) {
ArtifactVersion artifactVersion = ArtifactVersion.of(module);
Assert.isTrue(artifactVersion.isMilestoneVersion() || artifactVersion.isReleaseVersion(),
String.format("Given module is not in a fixed version, detected %s!", artifactVersion));
return new ReleaseVersion(ArtifactVersion.of(module));
}
/**
* Returns the Maven profiles to be used during the distribution build.
*
* @return
*/
public Argument getDistributionProfiles() {
if (version.isMilestoneVersion()) {
return profile("distribute", "milestone");
} else if (version.isReleaseVersion()) {
return profile("distribute", "release");
}
throw new IllegalStateException("Should not occur!");
}
}
}

View File

@@ -18,9 +18,6 @@ package org.springframework.data.release.build;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
@@ -75,24 +72,16 @@ class MavenRuntime {
}
}
public void execute(Project project, String... arguments) {
execute(project, Arrays.asList(arguments));
}
public void execute(Project project, List<String> arguments) {
public void execute(Project project, CommandLine arguments) {
DefaultInvocationRequest request = new DefaultInvocationRequest();
request.setJavaHome(os.getJavaHome());
request.setShellEnvironmentInherited(true);
request.setBaseDirectory(workspace.getProjectDirectory(project));
List<String> goals = arguments.stream()//
.map(argument -> properties.getFullyQualifiedPlugin(argument))//
.collect(Collectors.toList());
logger.log(project, "Executing mvn %s", arguments.toString());
request.setGoals(goals);
logger.log(project, "Executing mvn %s", goals.stream().collect(Collectors.joining(" ")));
request.setGoals(arguments.toCommandLine(it -> properties.getFullyQualifiedPlugin(it.getGoal())));
try {

View File

@@ -20,6 +20,7 @@ import lombok.Data;
import java.net.URI;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.model.Password;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@@ -48,7 +49,7 @@ public class DeploymentProperties {
/**
* The deployer's password.
*/
private String password;
private Password password;
/**
* The repository to deploy the artifacts to.
@@ -110,6 +111,7 @@ public class DeploymentProperties {
@Data
public static class Gpg {
private String keyname, password, executable;
private String keyname, executable;
private Password password;
}
}

View File

@@ -24,6 +24,7 @@ import javax.annotation.PostConstruct;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.release.model.Password;
import org.springframework.data.release.utils.HttpBasicCredentials;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
@@ -39,14 +40,14 @@ import org.springframework.util.Assert;
@ConfigurationProperties(prefix = "git")
public class GitProperties {
private @Getter(AccessLevel.PRIVATE) String password;
private @Getter(AccessLevel.PRIVATE) Password password;
private String username, author, email;
@PostConstruct
public void init() {
Assert.hasText(username, "No GitHub username (git.username) configured!");
Assert.hasText(password, "No GitHub password (git.password) configured!");
Assert.notNull(password, "No GitHub password (git.password) configured!");
Assert.hasText(author, "No Git author (git.author) configured!");
Assert.hasText(email, "No Git email (git.email) configured!");
}
@@ -57,7 +58,7 @@ public class GitProperties {
* @return
*/
public CredentialsProvider getCredentials() {
return new UsernamePasswordCredentialsProvider(username, password);
return new UsernamePasswordCredentialsProvider(username, password.toString());
}
public HttpBasicCredentials getHttpCredentials() {

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.model;
/**
* Interface to exposes a method {@link #masked()} to hide an actual value (usually for security reasons).
*
* @author Oliver Gierke
*/
public interface Masked {
/**
* Returns a masked {@link String} as well as information about the type whose String representation was masked.
*
* @return
*/
default String masked() {
return String.format("******** (%s)", getClass().getSimpleName());
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.util.Assert;
/**
* Value object to represent a password.
*
* @author Oliver Gierke
*/
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class Password implements Masked {
@Getter(AccessLevel.NONE) String value;
/**
* Create a new {@link Password} for the given value.
*
* @param password
* @return
*/
public static Password of(String password) {
Assert.hasText(password, "Password must not be null or empty!");
return new Password(password);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return value;
}
}

View File

@@ -20,13 +20,16 @@ import lombok.Value;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.springframework.data.release.model.Password;
/**
* @author Oliver Gierke
*/
@Value
public class HttpBasicCredentials {
private final String username, password;
private final String username;
private final Password password;
/*
* (non-Javadoc)
@@ -34,7 +37,7 @@ public class HttpBasicCredentials {
*/
public String toString() {
String header = username.concat(":").concat(password);
String header = username.concat(":").concat(password.toString());
byte[] encodedAuth = Base64.getEncoder().encode(header.getBytes(StandardCharsets.US_ASCII));
return "Basic ".concat(new String(encodedAuth));

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.build;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.data.release.build.CommandLine.Argument;
import org.springframework.data.release.model.Password;
/**
* Unit tests for {@link Argument}.
*
* @author Oliver Gierke
*/
public class ArgumentUnitTest {
@Test
public void masksMaskedForToString() {
Argument argument = Argument.of("password").withValue(Password.of("password"));
assertThat(argument.toCommandLineArgument(), is("password=password"));
assertThat(argument.toString(), startsWith("password=********"));
}
@Test
public void quotesValue() {
Argument argument = Argument.of("quoted").withQuotedValue("value");
assertThat(argument.toCommandLineArgument(), is("quoted=\"value\""));
}
@Test
public void argCreatesJvmArgument() {
assertThat(Argument.arg("foo").toCommandLineArgument(), is("-Dfoo"));
}
@Test
public void profileCreatesMavenProfile() {
assertThat(Argument.profile("foo").toCommandLineArgument(), is("-Pfoo"));
assertThat(Argument.profile("foo", "bar").toCommandLineArgument(), is("-Pfoo,bar"));
}
}