Skip to content

Commit

Permalink
Command line utility changes (#16)
Browse files Browse the repository at this point in the history
* Print secret on a new line for lookup secret command.

* Added keys command to list all secret keys from a secrets database file.

* Improve usage description for command line utility

* Returning error message when one or more secret keys are not found.

* Added details of how file based plugin should be configured.

* Added suggestion to create secrets database file in CONFIG_DIR.

This is so that it can be part of back up process.
  • Loading branch information
skmistry authored Apr 12, 2019
1 parent cf62df9 commit fec7e29
Show file tree
Hide file tree
Showing 14 changed files with 228 additions and 30 deletions.
74 changes: 72 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ To build the jar, run `./gradlew clean test assemble`
## Usage instructions

1. Download the plugin jar from the [GitHub Releases page](https://github.com/gocd/gocd-file-based-secrets-plugin)
2. Execute the `init` command to initialize the secret database:
2. Execute the `init` command to initialize the secret database. Although it's optional but it is recommended to
store your secret file under CONFIG_DIR. Doing this will make secrets database file part of the backup process.
The CONFIG_DIR is typically /etc/go on Linux and C:\Program Files\Go Server\config on Windows.

```shell
java -jar gocd-file-based-secrets-plugin-$VERSION$.jar init -f secret.db
```
Expand All @@ -25,11 +28,78 @@ To build the jar, run `./gradlew clean test assemble`
```shell
java -jar gocd-file-based-secrets-plugin-$VERSION$.jar show -f secret.db -n my-password
```
5. Remove a secret:
5. Show all secret keys:
```shell
java -jar gocd-file-based-secrets-plugin-$VERSION$.jar keys -f secret.db
```
6. Remove a secret:
```shell
java -jar gocd-file-based-secrets-plugin-$VERSION$.jar remove -f secret.db -n my-password
```

## Configuration

The plugin needs to be configured to use the secrets database file.

The configuration can be added directly to the `config.xml` using the `<secretConfig>` configuration.

* Example Configuration

```xml
<secretConfigs>
<secretConfig id="Env1Secrets" pluginId="cd.go.secrets.file-based-plugin">
<description>All secrets for env1</description>
<configuration>
<property>
<key>SecretsFilePath</key>
<value>/godata/config/secretsDatabase.json</value>
</property>
</configuration>
<rules>
<allow action="refer" type="environment">env_*</allow>
<deny action="refer" type="pipeline_group">my_group</deny>
<allow action="refer" type="pipeline_group">other_group</allow>
</rules>
</secretConfig>
</secretConfigs>
```
`<rules>` tag defines where this secretConfig is allowed/denied to be referred.

* The plugin can also be configured to use multiple secret database files if required:

```xml
<secretConfigs>
<secretConfig id="Env1Secrets" pluginId="cd.go.secrets.file-based-plugin">
<description>All secrets for env1</description>
<configuration>
<property>
<key>SecretsFilePath</key>
<value>/godata/config/secretsDatabase.json</value>
</property>
</configuration>
<rules>
<allow action="refer" type="environment">env_*</allow>
<deny action="refer" type="pipeline_group">my_group</deny>
<allow action="refer" type="pipeline_group">other_group</allow>
</rules>
</secretConfig>
<secretConfig id="Env2Secrets" pluginId="cd.go.secrets.file-based-plugin">
<description>All secrets for env1</description>
<configuration>
<property>
<key>SecretsFilePath</key>
<value>/godata/config/secretsDatabase_env2.json</value>
</property>
</configuration>
<rules>
<allow action="refer" type="environment">env_*</allow>
<deny action="refer" type="pipeline_group">my_group</deny>
<allow action="refer" type="pipeline_group">other_group</allow>
</rules>
</secretConfig>
</secretConfigs>
```

## Troubleshooting

### Verify Connection
Expand Down
7 changes: 7 additions & 0 deletions cli/src/main/java/cd/go/plugin/secret/filebased/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ void run(Consumer<Integer> exitter) {
AddSecretArgs addSecretArgs = new AddSecretArgs();
RemoveSecretArgs removeSecretArgs = new RemoveSecretArgs();
ShowSecretArgs showSecretArgs = new ShowSecretArgs();
ShowAllSecretKeysArgs keysArgs = new ShowAllSecretKeysArgs();

JCommander cmd = JCommander.newBuilder()
.addObject(rootArgs)
.addCommand(initArgs)
.addCommand(addSecretArgs)
.addCommand(removeSecretArgs)
.addCommand(showSecretArgs)
.addCommand(keysArgs)
.build();

String parsedCommand = null;
Expand All @@ -72,6 +74,9 @@ void run(Consumer<Integer> exitter) {
case "show":
showSecretArgs.execute(exitter);
break;
case "keys":
keysArgs.execute(exitter);
break;
default:
throw new UnsupportedOperationException(parsedCommand);
}
Expand All @@ -86,6 +91,8 @@ void run(Consumer<Integer> exitter) {

private static void printUsageAndExit(JCommander cmd, String parsedCommand, int statusCode, Consumer<Integer> exitter) {
StringBuilder out = new StringBuilder();
cmd.setProgramName("java -jar <path.to.plugin.jar.file>");
cmd.setColumnSize(100);
if (parsedCommand == null) {
cmd.usage(out);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import java.security.GeneralSecurityException;
import java.util.function.Consumer;

@Parameters(commandDescription = "Adds a secret", commandNames = "add")
@Parameters(commandDescription = "Adds a secret.", commandNames = "add")
public class AddSecretArgs extends HasNameArgs {

@Parameter(names = {"--value", "-v"}, required = true, description = "The value of the secret.", password = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.function.Consumer;

@Parameters(commandDescription = "Initialize the secret database file", commandNames = "init")
@Parameters(commandDescription = "Initialize the secret database file. Should be run before any other commands as it generates secrets database file used by other commands.", commandNames = "init")
public class InitArgs extends DatabaseFileArgs {
public void execute(Consumer<Integer> exitter) throws NoSuchAlgorithmException, IOException {
FileUtils.write(databaseFile, new SecretsDatabase().toJSON(), StandardCharsets.UTF_8);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import java.security.GeneralSecurityException;
import java.util.function.Consumer;

@Parameters(commandDescription = "Removes given secret", commandNames = "remove")
@Parameters(commandDescription = "Removes given secret.", commandNames = "remove")
public class RemoveSecretArgs extends HasNameArgs {
public void execute(Consumer<Integer> exitter) throws IOException, BadSecretException, GeneralSecurityException {
SecretsDatabase secretsDatabase = SecretsDatabase.readFrom(databaseFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2019 ThoughtWorks, Inc.
*
* 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 cd.go.plugin.secret.filebased.cli.args;

import cd.go.plugin.secret.filebased.db.BadSecretException;
import cd.go.plugin.secret.filebased.db.SecretsDatabase;
import com.beust.jcommander.Parameters;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

@Parameters(commandDescription = "Returns all secret keys.", commandNames = "keys")
public class ShowAllSecretKeysArgs extends DatabaseFileArgs {
public void execute(Consumer<Integer> exitter) throws IOException, BadSecretException, GeneralSecurityException {
Set<String> secretKeys = SecretsDatabase.readFrom(databaseFile).getAllSecretKeys();

if (!secretKeys.isEmpty()) {
System.out.println(secretKeys);
} else {
System.err.println("There are no secrets in the secrets database file.");
exitter.accept(-1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,17 @@

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;

@Parameters(commandDescription = "Returns value for given secret", commandNames = "show")
@Parameters(commandDescription = "Returns value for given secret.", commandNames = "show")
public class ShowSecretArgs extends HasNameArgs {
public void execute(Consumer<Integer> exitter) throws IOException, BadSecretException, GeneralSecurityException {
String secret = SecretsDatabase.readFrom(databaseFile).getSecret(key);

if (secret != null) {
System.out.print(secret);
System.out.println(secret);
} else {
System.err.println("Secret named " + key + " was not found.");
exitter.accept(-1);
Expand Down
39 changes: 36 additions & 3 deletions cli/src/test/java/cd/go/plugin/secret/filebased/cli/MainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class WithoutArguments {
void shouldPrintUsageAndExitWithBadStatusWhenNoOptionIsProvided() throws Exception {
Util.withCapturedSysOut((out, err) -> {
new Main().run(dummyExitter);
assertThat(err.toString()).startsWith("Usage: <main class> [options] [command] [command options]");
assertThat(err.toString()).startsWith("Usage: java -jar <path.to.plugin.jar.file> [options] [command] [command options]");
assertThat(out.toString()).isEmpty();
verify(dummyExitter).accept(1);
});
Expand All @@ -53,7 +53,7 @@ void shouldPrintUsageAndExitWithBadStatusWhenNoOptionIsProvided() throws Excepti
void shouldPrintUsageAndExitWithBadStatusWhenHelpOptionIsProvided() throws Exception {
Util.withCapturedSysOut((out, err) -> {
new Main("-h").run(dummyExitter);
assertThat(err.toString()).startsWith("Usage: <main class> [options] [command] [command options]");
assertThat(err.toString()).startsWith("Usage: java -jar <path.to.plugin.jar.file> [options] [command] [command options]");
assertThat(out.toString()).isEmpty();
verify(dummyExitter).accept(1);
});
Expand Down Expand Up @@ -141,7 +141,7 @@ void shouldLookupSecret(@TempDir Path tempDirectory) throws Exception {

Util.withCapturedSysOut((out, err) -> {
new Main("show", "-f", databaseFile.getAbsolutePath(), "-n", "username").run(dummyExitter);
assertThat(out.toString()).isEqualTo("foo");
assertThat(out.toString()).isEqualToIgnoringNewLines("foo");
assertThat(err.toString()).isEmpty();
verifyNoMoreInteractions(dummyExitter);
});
Expand Down Expand Up @@ -176,6 +176,39 @@ void shouldPrintErrorWhenSecretsFileDoesNotExists(@TempDir Path tempDirectory) t
}
}


@Nested
class LookupAllKeys {
@Test
void shouldLookupSecretKeys(@TempDir Path tempDirectory) throws Exception {
File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
new SecretsDatabase()
.addSecret("username", "foo")
.addSecret("password", "bar")
.saveTo(databaseFile);

Util.withCapturedSysOut((out, err) -> {
new Main("keys", "-f", databaseFile.getAbsolutePath()).run(dummyExitter);
assertThat(out.toString()).isEqualToIgnoringNewLines("[username, password]");
verifyNoMoreInteractions(dummyExitter);
});
}

@Test
void shouldPrintNoKeysMessageWhenSecretsAreNotPresent(@TempDir Path tempDirectory) throws Exception {
File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
new SecretsDatabase()
.saveTo(databaseFile);

Util.withCapturedSysOut((out, err) -> {
new Main("keys", "-f", databaseFile.getAbsolutePath()).run(dummyExitter);
assertThat(out.toString()).isEmpty();
assertThat(err.toString()).isEqualToIgnoringNewLines("There are no secrets in the secrets database file.");
verify(dummyExitter).accept(-1);
});
}
}

@Nested
class DeleteSecret {
@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Set;

import static org.apache.commons.io.FileUtils.readFileToString;

Expand Down Expand Up @@ -66,6 +67,10 @@ public String getSecret(String name) throws BadSecretException, GeneralSecurityE
return null;
}

public Set<String> getAllSecretKeys() {
return secrets.keySet();
}

public SecretsDatabase removeSecret(String name) {
secrets.remove(name);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,27 @@
import cd.go.plugin.secret.filebased.db.BadSecretException;
import cd.go.plugin.secret.filebased.db.SecretsDatabase;
import cd.go.plugin.secret.filebased.model.LookupSecretRequest;
import com.google.gson.Gson;
import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest;
import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse;
import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse;

import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;

import static cd.go.plugin.secret.filebased.FileBasedSecretsPlugin.*;

public class LookupSecretsRequestExecutor {
public static final int NOT_FOUND_ERROR_CODE = 404;

public GoPluginApiResponse execute(GoPluginApiRequest request) {
LookupSecretRequest lookupSecretsRequest = LookupSecretRequest.fromJSON(request.requestBody());
List<Map<String, String>> responseList = new ArrayList<>();

File secretsFile = new File(lookupSecretsRequest.getSecretsFilePath());
List<String> unresolvedKeys = new ArrayList<>();

try {
SecretsDatabase secretsDatabase = SecretsDatabase.readFrom(secretsFile);
Expand All @@ -50,12 +51,19 @@ public GoPluginApiResponse execute(GoPluginApiRequest request) {
response.put("key", key);
response.put("value", secret);
responseList.add(response);
} else {
unresolvedKeys.add(key);
}
}

if (unresolvedKeys.isEmpty()) {
return DefaultGoPluginApiResponse.success(GSON.toJson(responseList));
}

Map<String, String> response = Collections.singletonMap("message", String.format("Secrets with keys %s not found.", unresolvedKeys));
return new DefaultGoPluginApiResponse(NOT_FOUND_ERROR_CODE, GSON.toJson(response));
} catch (IOException | GeneralSecurityException | BadSecretException e) {
return DefaultGoPluginApiResponse.error("Error while looking up secrets: " + e.getMessage());
}
return DefaultGoPluginApiResponse.success(GSON.toJson(responseList));
}
}
Loading

0 comments on commit fec7e29

Please sign in to comment.