{
22 | void accept(ByteArrayOutputStream t, ByteArrayOutputStream r);
23 | }
24 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/HelpArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import com.beust.jcommander.Parameter;
20 |
21 | public class HelpArgs {
22 | @Parameter(names = {"--help", "-h"}, help = true, description = "Prints this help.")
23 | public boolean help;
24 | }
25 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/HasNameArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import com.beust.jcommander.Parameter;
20 |
21 | public abstract class HasNameArgs extends DatabaseFileArgs {
22 | @Parameter(names = {"--name", "-n"}, required = true, description = "The name of the secret to remove.")
23 | public String key;
24 | }
25 |
--------------------------------------------------------------------------------
/db/src/main/java/cd/go/plugin/secret/filebased/db/BadSecretException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.db;
18 |
19 | public class BadSecretException extends Exception {
20 |
21 | public BadSecretException(String message) {
22 | super(message);
23 | }
24 |
25 | public BadSecretException(String message, Throwable cause) {
26 | super(message, cause);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/DatabaseFileArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import com.beust.jcommander.Parameter;
20 |
21 | import java.io.File;
22 |
23 | public class DatabaseFileArgs {
24 | @Parameter(names = {"--file", "-f"}, required = true, description = "The path to the secret database file.")
25 | public File databaseFile;
26 | }
27 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/resources/secrets.template.html:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 | Secrets File Path:*
19 |
20 | {{ GOINPUTNAME[SecretsFilePath].$error.server }}
21 |
22 |
23 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/resource-templates/plugin.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
19 | ${name}
20 | ${version}
21 | ${goCdVersion}
22 | ${description}
23 |
24 | ${vendorName}
25 | ${vendorUrl}
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/cli/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | dependencies {
18 | compile project(':db')
19 | compile group: 'com.beust', name: 'jcommander', version: '1.72'
20 |
21 | testCompile group: 'org.assertj', name: 'assertj-core', version: '3.15.0'
22 | testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.6.0'
23 | testRuntime group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.6.0'
24 | testCompile group: 'org.mockito', name: 'mockito-core', version: '3.2.4'
25 | }
26 |
--------------------------------------------------------------------------------
/db/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | dependencies {
18 | compile group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
19 | compile group: 'commons-io', name: 'commons-io', version: '2.6'
20 |
21 | testCompile group: 'org.assertj', name: 'assertj-core', version: '3.15.0'
22 | testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.6.0'
23 | testRuntime group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.6.0'
24 | }
25 |
26 | test {
27 | useJUnitPlatform()
28 | }
29 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/util/LRUCache.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.util;
18 |
19 | import java.util.LinkedHashMap;
20 | import java.util.Map;
21 |
22 | public class LRUCache extends LinkedHashMap {
23 |
24 | private int cacheSize;
25 |
26 | public LRUCache(int cacheSize) {
27 | super(16, 0.75f, true);
28 | this.cacheSize = cacheSize;
29 | }
30 |
31 | protected boolean removeEldestEntry(Map.Entry eldest) {
32 | return size() >= cacheSize;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by .ignore support plugin (hsz.mobi)
2 | ### Gradle template
3 | .gradle
4 |
5 | # Ignore Gradle GUI config
6 | gradle-app.setting
7 |
8 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
9 | !gradle-wrapper.jar
10 |
11 | # Cache of project
12 | .gradletasknamecache
13 |
14 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
15 | # gradle/wrapper/gradle-wrapper.properties
16 |
17 | # User-specific stuff
18 | .idea/**/workspace.xml
19 | .idea/**/tasks.xml
20 | .idea/**/usage.statistics.xml
21 | .idea/**/dictionaries
22 | .idea/**/shelf
23 |
24 | # Sensitive or high-churn files
25 | .idea/**/dataSources/
26 | .idea/**/dataSources.ids
27 | .idea/**/dataSources.local.xml
28 | .idea/**/sqlDataSources.xml
29 | .idea/**/dynamic.xml
30 | .idea/**/uiDesigner.xml
31 | .idea/**/dbnavigator.xml
32 |
33 | # Gradle
34 | .idea/**/gradle.xml
35 | .idea/**/libraries
36 |
37 | # Gradle and Maven with auto-import
38 | # When using Gradle or Maven with auto-import, you should exclude module files,
39 | # since they will be recreated, and may cause churn. Uncomment if using
40 | # auto-import.
41 | .idea/modules.xml
42 | .idea/*.iml
43 | .idea/modules
44 | .idea
45 |
46 | # project specific ignores
47 |
48 | /build/
49 | /out/
50 | cli/build/
51 | cli/out/
52 | db/build/
53 | db/out/
54 | jar-class-loader/build/
55 | jar-class-loader/out/
56 | gocd-file-based-secrets-plugin/build/
57 | gocd-file-based-secrets-plugin/out/
58 | gocd-file-based-secrets-plugin/src/main/resources-generated
59 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/InitArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import com.beust.jcommander.Parameters;
21 | import org.apache.commons.io.FileUtils;
22 |
23 | import java.io.IOException;
24 | import java.nio.charset.StandardCharsets;
25 | import java.security.NoSuchAlgorithmException;
26 | import java.util.function.Consumer;
27 |
28 | @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")
29 | public class InitArgs extends DatabaseFileArgs {
30 | public void execute(Consumer exitter) throws NoSuchAlgorithmException, IOException {
31 | FileUtils.write(databaseFile, new SecretsDatabase().toJSON(), StandardCharsets.UTF_8);
32 | System.err.println("Initialized secret database file in " + databaseFile);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/AddSecretArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import com.beust.jcommander.Parameter;
21 | import com.beust.jcommander.Parameters;
22 |
23 | import java.io.IOException;
24 | import java.security.GeneralSecurityException;
25 | import java.util.function.Consumer;
26 |
27 | @Parameters(commandDescription = "Adds a secret.", commandNames = "add")
28 | public class AddSecretArgs extends HasNameArgs {
29 |
30 | @Parameter(names = {"--value", "-v"}, required = true, description = "The value of the secret.", password = true)
31 | public String secret;
32 |
33 | public void execute(Consumer exitter) throws IOException, GeneralSecurityException {
34 | SecretsDatabase.readFrom(databaseFile)
35 | .addSecret(key, secret)
36 | .saveTo(databaseFile);
37 |
38 | System.err.println("Added secret named " + key + ".");
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/ShowSecretArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import cd.go.plugin.secret.filebased.db.BadSecretException;
20 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
21 | import com.beust.jcommander.Parameters;
22 |
23 | import java.io.IOException;
24 | import java.security.GeneralSecurityException;
25 | import java.util.HashSet;
26 | import java.util.Set;
27 | import java.util.function.Consumer;
28 |
29 | @Parameters(commandDescription = "Returns value for given secret.", commandNames = "show")
30 | public class ShowSecretArgs extends HasNameArgs {
31 | public void execute(Consumer exitter) throws IOException, BadSecretException, GeneralSecurityException {
32 | String secret = SecretsDatabase.readFrom(databaseFile).getSecret(key);
33 |
34 | if (secret != null) {
35 | System.out.println(secret);
36 | } else {
37 | System.err.println("Secret named " + key + " was not found.");
38 | exitter.accept(-1);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/ShowAllSecretKeysArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import cd.go.plugin.secret.filebased.db.BadSecretException;
20 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
21 | import com.beust.jcommander.Parameters;
22 |
23 | import java.io.IOException;
24 | import java.security.GeneralSecurityException;
25 | import java.util.HashSet;
26 | import java.util.Set;
27 | import java.util.function.Consumer;
28 |
29 | @Parameters(commandDescription = "Returns all secret keys.", commandNames = "keys")
30 | public class ShowAllSecretKeysArgs extends DatabaseFileArgs {
31 | public void execute(Consumer exitter) throws IOException, BadSecretException, GeneralSecurityException {
32 | Set secretKeys = SecretsDatabase.readFrom(databaseFile).getAllSecretKeys();
33 |
34 | if (!secretKeys.isEmpty()) {
35 | System.out.println(secretKeys);
36 | } else {
37 | System.err.println("There are no secrets in the secrets database file.");
38 | exitter.accept(-1);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/RemoveSecretArgs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli.args;
18 |
19 | import cd.go.plugin.secret.filebased.db.BadSecretException;
20 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
21 | import com.beust.jcommander.Parameters;
22 |
23 | import java.io.IOException;
24 | import java.security.GeneralSecurityException;
25 | import java.util.function.Consumer;
26 |
27 | @Parameters(commandDescription = "Removes given secret.", commandNames = "remove")
28 | public class RemoveSecretArgs extends HasNameArgs {
29 | public void execute(Consumer exitter) throws IOException, BadSecretException, GeneralSecurityException {
30 | SecretsDatabase secretsDatabase = SecretsDatabase.readFrom(databaseFile);
31 |
32 | if (secretsDatabase.getSecret(key) != null) {
33 | secretsDatabase
34 | .removeSecret(key)
35 | .saveTo(databaseFile);
36 | System.err.println("Removed secret named " + key + ".");
37 | } else {
38 | System.err.println("Secret named " + key + " was not found.");
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/model/SecretsConfiguration.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.model;
18 |
19 | import cd.go.plugin.base.annotations.Property;
20 | import com.google.gson.Gson;
21 | import com.google.gson.GsonBuilder;
22 | import com.google.gson.annotations.Expose;
23 | import com.google.gson.annotations.SerializedName;
24 |
25 | public class SecretsConfiguration {
26 |
27 | public static final String SECRETS_FILE_PATH_PROPERTY = "SecretsFilePath";
28 |
29 | private static final Gson GSON = new GsonBuilder().serializeNulls().create();
30 |
31 | @Expose
32 | @SerializedName(SECRETS_FILE_PATH_PROPERTY)
33 | @Property(name = SECRETS_FILE_PATH_PROPERTY, required = true)
34 | private String secretsFilePath;
35 |
36 | public SecretsConfiguration() {
37 | }
38 |
39 | SecretsConfiguration(String secretsFilePath) {
40 | this.secretsFilePath = secretsFilePath;
41 | }
42 |
43 | public static SecretsConfiguration fromJSON(String requestBody) {
44 | return GSON.fromJson(requestBody, SecretsConfiguration.class);
45 | }
46 |
47 | public String getSecretsFilePath() {
48 | return secretsFilePath;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/executors/CacheEntry.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.executors;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import cd.go.plugin.secret.filebased.util.FileStat;
21 |
22 | import java.io.File;
23 | import java.io.IOException;
24 |
25 | class CacheEntry {
26 |
27 | private final FileStat fileStat;
28 |
29 | private volatile SecretsDatabase secretsDatabase;
30 |
31 | CacheEntry(File file) {
32 | this.fileStat = new FileStat(file);
33 | }
34 |
35 | void refresh() throws IOException {
36 | if (this.fileStat.changed(5000)) {
37 | this.secretsDatabase = null;
38 | }
39 | }
40 |
41 | // double checked locks
42 | public SecretsDatabase getSecretsDatabase() throws IOException {
43 | SecretsDatabase localRef = secretsDatabase;
44 | if (localRef == null) {
45 | synchronized (this) {
46 | localRef = secretsDatabase;
47 | if (localRef == null) {
48 | secretsDatabase = localRef = SecretsDatabase.readFrom(this.fileStat.getFile());
49 | }
50 | }
51 | }
52 | return localRef;
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/model/LookupSecretRequest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.model;
18 |
19 | import com.google.gson.Gson;
20 | import com.google.gson.GsonBuilder;
21 | import com.google.gson.annotations.Expose;
22 | import com.google.gson.annotations.SerializedName;
23 |
24 | import java.util.List;
25 |
26 | public class LookupSecretRequest {
27 | private static final Gson GSON = new GsonBuilder().serializeNulls().create();
28 |
29 | @Expose
30 | @SerializedName("configuration")
31 | private SecretsConfiguration configuration;
32 |
33 | @Expose
34 | @SerializedName("keys")
35 | private List keys;
36 |
37 | public LookupSecretRequest() {}
38 |
39 | public LookupSecretRequest(String secretsFilePath, List keys) {
40 | this.configuration = new SecretsConfiguration(secretsFilePath);
41 | this.keys = keys;
42 | }
43 |
44 | public static LookupSecretRequest fromJSON(String requestBody) {
45 | return GSON.fromJson(requestBody, LookupSecretRequest.class);
46 | }
47 |
48 | public String getSecretsFilePath() {
49 | return configuration.getSecretsFilePath();
50 | }
51 |
52 | public List getKeys() {
53 | return keys;
54 | }
55 |
56 | public String toJSON() {
57 | return GSON.toJson(this);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/model/Metadata.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.model;
18 |
19 | import com.google.gson.annotations.Expose;
20 | import com.google.gson.annotations.SerializedName;
21 |
22 | class Metadata {
23 | @Expose
24 | @SerializedName("required")
25 | private boolean required;
26 |
27 | @Expose
28 | @SerializedName("secure")
29 | private boolean secure;
30 |
31 | @Expose
32 | @SerializedName("display_name")
33 | private String displayName;
34 |
35 | public Metadata() {}
36 |
37 | public Metadata(boolean required, boolean secure, String displayName) {
38 | this.required = required;
39 | this.secure = secure;
40 | this.displayName = displayName;
41 | }
42 |
43 | public boolean isRequired() {
44 | return required;
45 | }
46 |
47 | public boolean isSecure() {
48 | return secure;
49 | }
50 |
51 | public String getDisplayName() {
52 | return displayName;
53 | }
54 |
55 | @Override
56 | public boolean equals(Object o) {
57 | if (this == o) return true;
58 | if (o == null || getClass() != o.getClass()) return false;
59 |
60 | Metadata metadata = (Metadata) o;
61 |
62 | if (required != metadata.required) return false;
63 | if (secure != metadata.secure) return false;
64 | return displayName != null ? displayName.equals(metadata.displayName) : metadata.displayName == null;
65 | }
66 |
67 | @Override
68 | public int hashCode() {
69 | int result = (required ? 1 : 0);
70 | result = 31 * result + (secure ? 1 : 0);
71 | result = 31 * result + (displayName != null ? displayName.hashCode() : 0);
72 | return result;
73 | }
74 | }
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | plugins {
18 | id "me.champeau.gradle.jmh" version "0.5.0"
19 | }
20 |
21 | configurations {
22 | extractedAtTopLevel
23 | }
24 |
25 | dependencies {
26 | compileOnly group: 'cd.go.plugin', name: 'go-plugin-api', version: '18.9.0'
27 | compile group: 'cd.go.plugin.base', name: 'gocd-plugin-base', version: '0.0.1'
28 | compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
29 |
30 | compile project(':db')
31 | compile project(':cli')
32 |
33 | compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
34 | annotationProcessor group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
35 |
36 | extractedAtTopLevel project(':jar-class-loader')
37 |
38 | testCompile group: 'org.assertj', name: 'assertj-core', version: '3.15.0'
39 | testCompile group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.6.0'
40 | testRuntime group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.6.0'
41 | testCompile group: 'org.mockito', name: 'mockito-core', version: '3.2.4'
42 | testCompile group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.0'
43 | }
44 |
45 | jar {
46 | dependsOn project.configurations.extractedAtTopLevel
47 |
48 | manifest {
49 | attributes 'Main-Class': 'com.thoughtworks.gocd.Boot'
50 | attributes 'GoCD-Main-Class': 'cd.go.plugin.secret.filebased.cli.Main'
51 | }
52 |
53 | from(configurations.compile) {
54 | into "lib/"
55 | }
56 |
57 | // This is useful for debugging
58 | from(sourceSets.main.java) {
59 | into "/"
60 | }
61 |
62 | from(project.configurations.extractedAtTopLevel.collect { it.isDirectory() ? it : zipTree(it) }) {
63 | into("/")
64 | }
65 | }
66 |
67 | test {
68 | useJUnitPlatform()
69 | }
70 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/SecretFilePathValidator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased;
18 |
19 | import cd.go.plugin.base.validation.ValidationResult;
20 | import cd.go.plugin.base.validation.Validator;
21 | import org.apache.commons.lang3.StringUtils;
22 |
23 | import java.io.File;
24 | import java.util.Map;
25 |
26 | import static cd.go.plugin.secret.filebased.model.SecretsConfiguration.SECRETS_FILE_PATH_PROPERTY;
27 |
28 | public class SecretFilePathValidator implements Validator {
29 |
30 | @Override
31 | public ValidationResult validate(Map requestBody) {
32 | ValidationResult validationResult = new ValidationResult();
33 | String filePath = requestBody.get(SECRETS_FILE_PATH_PROPERTY);
34 | if (StringUtils.isBlank(filePath)) {
35 | return addErrorAndReturn(validationResult, "SecretsFilePath must not be blank.");
36 | }
37 |
38 | File secretFile = new File(filePath);
39 |
40 | if (!secretFile.exists()) {
41 | return addErrorAndReturn(validationResult, String.format("No secret config file at path '%s'.", filePath));
42 | }
43 |
44 | if (secretFile.isDirectory()) {
45 | return addErrorAndReturn(validationResult, String.format("Secret config file path '%s' is not a normal file.", filePath));
46 | }
47 |
48 | if (!secretFile.canRead()) {
49 | return addErrorAndReturn(validationResult, String.format("Unable to read secret config file '%s', check permissions.", filePath));
50 | }
51 |
52 | return validationResult;
53 | }
54 |
55 | private ValidationResult addErrorAndReturn(ValidationResult validationResult, String message) {
56 | validationResult.add(SECRETS_FILE_PATH_PROPERTY, message);
57 | return validationResult;
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/FileBasedSecretsPlugin.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased;
18 |
19 | import cd.go.plugin.base.dispatcher.BaseBuilder;
20 | import cd.go.plugin.base.dispatcher.RequestDispatcher;
21 | import cd.go.plugin.secret.filebased.executors.LookupSecretsRequestExecutor;
22 | import cd.go.plugin.secret.filebased.model.SecretsConfiguration;
23 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor;
24 | import com.thoughtworks.go.plugin.api.GoPlugin;
25 | import com.thoughtworks.go.plugin.api.GoPluginIdentifier;
26 | import com.thoughtworks.go.plugin.api.annotation.Extension;
27 | import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException;
28 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest;
29 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse;
30 |
31 | import java.util.Arrays;
32 |
33 | @Extension
34 | public class FileBasedSecretsPlugin implements GoPlugin {
35 |
36 | private RequestDispatcher requestDispatcher;
37 |
38 | @Override
39 | public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationAccessor) {
40 | requestDispatcher = BaseBuilder
41 | .forSecrets()
42 | .v1()
43 | .icon("/plugin-icon.svg", "image/svg+xml")
44 | .configMetadata(SecretsConfiguration.class, false)
45 | .configView("/secrets.template.html")
46 | .validateSecretConfig(new SecretFilePathValidator())
47 | .lookup(new LookupSecretsRequestExecutor())
48 | .build();
49 | }
50 |
51 | @Override
52 | public GoPluginApiResponse handle(GoPluginApiRequest request) throws UnhandledRequestTypeException {
53 | return requestDispatcher.dispatch(request);
54 | }
55 |
56 | @Override
57 | public GoPluginIdentifier pluginIdentifier() {
58 | return new GoPluginIdentifier("secrets", Arrays.asList("1.0"));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/db/src/main/java/cd/go/plugin/secret/filebased/db/Util.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.db;
18 |
19 | import org.apache.commons.io.IOUtils;
20 |
21 | import java.io.*;
22 | import java.nio.charset.StandardCharsets;
23 |
24 | public class Util {
25 | public static String readResourceAsString(String resourceFile) {
26 | try (InputStreamReader reader = new InputStreamReader(Util.class.getResourceAsStream(resourceFile), StandardCharsets.UTF_8)) {
27 | return IOUtils.toString(reader);
28 | } catch (IOException e) {
29 | throw new RuntimeException("Could not find resource " + resourceFile, e);
30 | }
31 | }
32 |
33 | public static byte[] readResourceBytes(String resourceFile) {
34 | try (InputStream in = Util.class.getResourceAsStream(resourceFile)) {
35 | return IOUtils.toByteArray(in);
36 | } catch (IOException e) {
37 | throw new RuntimeException("Could not find resource " + resourceFile, e);
38 | }
39 | }
40 |
41 | public static boolean isBlank(String text) {
42 | return text == null || text.trim().length() == 0;
43 | }
44 |
45 | public static void withCapturedSysOut(ThrowingBiConsumer throwingBiConsumer) throws Exception {
46 | PrintStream originalSystemOutStream = System.out;
47 | PrintStream originalSystemErrorStream = System.err;
48 |
49 | try (
50 | ByteArrayOutputStream stdoutBuffer = new ByteArrayOutputStream();
51 | PrintStream stdOutStream = new PrintStream(stdoutBuffer, true);
52 |
53 | ByteArrayOutputStream stdErrBuffer = new ByteArrayOutputStream();
54 | PrintStream stdErrStream = new PrintStream(stdErrBuffer, true)
55 | ) {
56 | System.setOut(stdOutStream);
57 | System.setErr(stdErrStream);
58 | throwingBiConsumer.accept(stdoutBuffer, stdErrBuffer);
59 | } finally {
60 | System.setErr(originalSystemErrorStream);
61 | System.setOut(originalSystemOutStream);
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/jar-class-loader/src/main/java/com/thoughtworks/gocd/onejar/Handler.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.thoughtworks.gocd.onejar;
18 |
19 | import com.thoughtworks.gocd.Boot;
20 |
21 | import java.io.IOException;
22 | import java.io.InputStream;
23 | import java.net.MalformedURLException;
24 | import java.net.URL;
25 | import java.net.URLConnection;
26 | import java.net.URLStreamHandler;
27 |
28 | public class Handler extends URLStreamHandler {
29 |
30 | private static final String PROTOCOL = "onejar";
31 |
32 | private static final String JAVA_PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
33 |
34 | private static boolean initialized = false;
35 |
36 | public static synchronized void init() {
37 | if (initialized) {
38 | return;
39 | }
40 | // Add our 'onejar:' protocol handler, but leave open the
41 | // possibility of a subsequent class taking over the
42 | // factory.
43 | String handlerPackage = System.getProperty(JAVA_PROTOCOL_HANDLER);
44 | if (handlerPackage == null || handlerPackage.trim().isEmpty()) {
45 | handlerPackage = "";
46 | }
47 | if (handlerPackage.length() > 0) {
48 | handlerPackage = "|" + handlerPackage;
49 | }
50 | handlerPackage = Boot.class.getPackage().getName() + handlerPackage;
51 | System.setProperty(JAVA_PROTOCOL_HANDLER, handlerPackage);
52 | initialized = true;
53 | }
54 |
55 | @Override
56 | protected URLConnection openConnection(URL u) throws IOException {
57 | URLConnection urlConnection = new URLConnection(u) {
58 |
59 | @Override
60 | public void connect() throws IOException {
61 |
62 | }
63 |
64 | @Override
65 | public InputStream getInputStream() throws IOException {
66 | String file = u.getFile();
67 | return Boot.class.getResourceAsStream("/" + file);
68 | }
69 | };
70 | urlConnection.setUseCaches(false);
71 | return urlConnection;
72 | }
73 |
74 | public static URL toOneJarUrl(String name) {
75 | try {
76 | return new URL(PROTOCOL, null, -1, name);
77 | } catch (MalformedURLException e) {
78 | throw new RuntimeException(e);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/test/java/cd/go/plugin/secret/filebased/util/FileStatTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.util;
18 |
19 | import org.apache.commons.io.FileUtils;
20 | import org.junit.jupiter.api.Nested;
21 | import org.junit.jupiter.api.Test;
22 | import org.junit.jupiter.api.io.TempDir;
23 |
24 | import java.io.File;
25 | import java.io.IOException;
26 | import java.util.UUID;
27 |
28 | import static java.nio.charset.StandardCharsets.UTF_8;
29 | import static org.assertj.core.api.Assertions.assertThat;
30 |
31 | class FileStatTest {
32 |
33 | @Nested
34 | class Changed_method {
35 |
36 | @Test
37 | void shouldStatANonExistantFile(@TempDir File tempDir) {
38 | File file = new File(tempDir, UUID.randomUUID().toString());
39 |
40 | FileStat fileStat = new FileStat(file);
41 | assertThat(fileStat.changed(0)).isTrue();
42 | assertThat(fileStat.changed(0)).isFalse();
43 | }
44 |
45 | @Test
46 | void shouldReturnTrueIfFileHasBeenModifiedWithinAnInterval(@TempDir File tempDir) throws InterruptedException, IOException {
47 | File file = new File(tempDir, UUID.randomUUID().toString());
48 | assertThat(file.createNewFile()).isTrue();
49 |
50 | FileStat fileStat = new FileStat(file);
51 | fileStat.refresh();
52 |
53 | assertThat(fileStat.changed(0)).isFalse();
54 | Thread.sleep(1500);
55 |
56 | assertThat(file.setLastModified(System.currentTimeMillis())).isTrue();
57 |
58 | assertThat(fileStat.changed(2000)).isFalse();
59 |
60 | assertThat(fileStat.changed(1000)).isTrue();
61 | }
62 |
63 | @Test
64 | void shouldReturnTrueIfFileContentsChanged(@TempDir File tempDir) throws IOException, InterruptedException {
65 | File file = new File(tempDir, UUID.randomUUID().toString());
66 | assertThat(file.createNewFile()).isTrue();
67 |
68 | FileStat fileStat = new FileStat(file);
69 | fileStat.refresh();
70 |
71 | assertThat(fileStat.changed(0)).isFalse();
72 | FileUtils.writeStringToFile(file, "foo", UTF_8);
73 | Thread.sleep(100);
74 | assertThat(fileStat.changed(110)).isFalse();
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/jmh/java/cd/go/plugin/secret/filebased/executors/SecretsDatabasePerformanceBenchmark.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.executors;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import cd.go.plugin.secret.filebased.util.LRUCache;
21 | import org.openjdk.jmh.annotations.*;
22 | import org.openjdk.jmh.infra.Blackhole;
23 |
24 | import java.io.File;
25 | import java.io.IOException;
26 | import java.security.GeneralSecurityException;
27 | import java.security.NoSuchAlgorithmException;
28 | import java.util.Map;
29 | import java.util.concurrent.TimeUnit;
30 |
31 | import static java.util.Collections.synchronizedMap;
32 |
33 | @BenchmarkMode(Mode.Throughput)
34 | @OutputTimeUnit(TimeUnit.MILLISECONDS)
35 | @State(Scope.Benchmark)
36 | @Fork(value = 1, jvmArgs = {"-Xms512m", "-Xmx512m"})
37 | @Warmup(iterations = 2, time = 10, timeUnit = TimeUnit.SECONDS)
38 | @Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS)
39 | public class SecretsDatabasePerformanceBenchmark {
40 |
41 | private final Map fileStatCache = synchronizedMap(new LRUCache<>(3));
42 |
43 | private SecretsDatabase secretsDatabase;
44 |
45 | private File tempFile;
46 |
47 | @Setup
48 | public void setup() throws GeneralSecurityException, IOException {
49 | tempFile = new File("tmp.secrets.db");
50 | tempFile.deleteOnExit();
51 |
52 | secretsDatabase = new SecretsDatabase();
53 | secretsDatabase.addSecret("foo", "bar");
54 |
55 | secretsDatabase.saveTo(tempFile);
56 | }
57 |
58 | @Benchmark
59 | public void readDatabaseEachTime(Blackhole bh) throws NoSuchAlgorithmException, IOException {
60 | SecretsDatabase secretsDatabase = SecretsDatabase.readFrom(tempFile);
61 | bh.consume(secretsDatabase.getSecret("foo"));
62 | }
63 |
64 | @Benchmark
65 | public void readDatabaseFromCache(Blackhole bh) {
66 | bh.consume(secretsDatabase.getSecret("foo"));
67 | }
68 |
69 | @Benchmark
70 | public void readDatabaseThroughFileStatCache(Blackhole bh) throws IOException {
71 | bh.consume(fileStatCache
72 | .compute(tempFile, LookupSecretsRequestExecutor.FileCacheEntryCacheEntryBiFunction.INSTANCE)
73 | .getSecretsDatabase()
74 | .getSecret("foo"));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/db/src/test/java/cd/go/plugin/secret/filebased/db/CipherTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.db;
18 |
19 | import org.junit.jupiter.api.Nested;
20 | import org.junit.jupiter.api.Test;
21 |
22 | import java.security.GeneralSecurityException;
23 | import java.security.NoSuchAlgorithmException;
24 | import java.util.Base64;
25 |
26 | import static org.assertj.core.api.Assertions.assertThat;
27 | import static org.assertj.core.api.Assertions.assertThatCode;
28 |
29 | class CipherTest {
30 |
31 | @Nested
32 | class GenerateKey {
33 |
34 | @Test
35 | void shouldGenerate128BitKey() throws NoSuchAlgorithmException {
36 | assertThat(Cipher.generateKey())
37 | .hasSize(128 / 8);
38 | }
39 |
40 | @Test
41 | void shouldGenerateRandomKeyEveryTime() throws NoSuchAlgorithmException {
42 | assertThat(Cipher.generateKey())
43 | .isNotEqualTo(Cipher.generateKey());
44 | }
45 | }
46 |
47 | @Nested
48 | class Encrypt {
49 |
50 | @Test
51 | void shouldEncrypt() throws GeneralSecurityException {
52 | String clearText = "foo";
53 |
54 | String key = Base64.getEncoder().encodeToString(Cipher.generateKey());
55 |
56 | assertThat(Cipher.encrypt(key, clearText))
57 | .doesNotContain(clearText);
58 | }
59 |
60 | @Test
61 | void shouldEncryptToDifferentValueEveryTime() throws GeneralSecurityException {
62 | String clearText = "foo";
63 |
64 | String key = Base64.getEncoder().encodeToString(Cipher.generateKey());
65 |
66 | assertThat(Cipher.encrypt(key, clearText))
67 | .isNotEqualTo(Cipher.encrypt(key, clearText));
68 | }
69 | }
70 |
71 | @Nested
72 | class Decrypt {
73 |
74 | @Test
75 | void shouldDecrypt() throws GeneralSecurityException, BadSecretException {
76 | String clearText = "foo";
77 |
78 | String key = Base64.getEncoder().encodeToString(Cipher.generateKey());
79 |
80 | String encryptedValue = Cipher.encrypt(key, clearText);
81 |
82 | assertThat(Cipher.decrypt(key, encryptedValue)).isEqualTo(clearText);
83 | }
84 |
85 | @Test
86 | void shouldFailIfEncryptedValueIsTampered() throws GeneralSecurityException {
87 | String key = Base64.getEncoder().encodeToString(Cipher.generateKey());
88 |
89 | assertThatCode(() -> Cipher.decrypt(key, "junk")).hasMessage("Bad cipher text")
90 | .isInstanceOf(BadSecretException.class);
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
34 |
35 | @rem Find java.exe
36 | if defined JAVA_HOME goto findJavaFromJavaHome
37 |
38 | set JAVA_EXE=java.exe
39 | %JAVA_EXE% -version >NUL 2>&1
40 | if "%ERRORLEVEL%" == "0" goto init
41 |
42 | echo.
43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
44 | echo.
45 | echo Please set the JAVA_HOME variable in your environment to match the
46 | echo location of your Java installation.
47 |
48 | goto fail
49 |
50 | :findJavaFromJavaHome
51 | set JAVA_HOME=%JAVA_HOME:"=%
52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
53 |
54 | if exist "%JAVA_EXE%" goto init
55 |
56 | echo.
57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
58 | echo.
59 | echo Please set the JAVA_HOME variable in your environment to match the
60 | echo location of your Java installation.
61 |
62 | goto fail
63 |
64 | :init
65 | @rem Get command-line arguments, handling Windows variants
66 |
67 | if not "%OS%" == "Windows_NT" goto win9xME_args
68 |
69 | :win9xME_args
70 | @rem Slurp the command line arguments.
71 | set CMD_LINE_ARGS=
72 | set _SKIP=2
73 |
74 | :win9xME_args_slurp
75 | if "x%~1" == "x" goto execute
76 |
77 | set CMD_LINE_ARGS=%*
78 |
79 | :execute
80 | @rem Setup the command line
81 |
82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
83 |
84 | @rem Execute Gradle
85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
86 |
87 | :end
88 | @rem End local scope for the variables with windows NT shell
89 | if "%ERRORLEVEL%"=="0" goto mainEnd
90 |
91 | :fail
92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
93 | rem the _cmd.exe /c_ return code!
94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
95 | exit /b 1
96 |
97 | :mainEnd
98 | if "%OS%"=="Windows_NT" endlocal
99 |
100 | :omega
101 |
--------------------------------------------------------------------------------
/db/src/main/java/cd/go/plugin/secret/filebased/db/Cipher.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.db;
18 |
19 | import javax.crypto.KeyGenerator;
20 | import javax.crypto.spec.IvParameterSpec;
21 | import javax.crypto.spec.SecretKeySpec;
22 | import java.nio.charset.StandardCharsets;
23 | import java.security.GeneralSecurityException;
24 | import java.security.NoSuchAlgorithmException;
25 | import java.util.Base64;
26 |
27 | import static cd.go.plugin.secret.filebased.db.Util.isBlank;
28 |
29 | public class Cipher {
30 | public static String decrypt(String cipherKey, String encryptedValue) throws BadSecretException, GeneralSecurityException {
31 | byte[] keyBytes = Base64.getDecoder().decode(cipherKey);
32 |
33 | if (!canDecrypt(encryptedValue)) {
34 | throw new BadSecretException("Bad cipher text");
35 | }
36 |
37 | String[] splits = encryptedValue.split(":");
38 |
39 | String encodedIV = splits[1];
40 | String encodedCipherText = splits[2];
41 |
42 | byte[] initializationVector = Base64.getDecoder().decode(encodedIV);
43 | javax.crypto.Cipher decryptCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
44 | decryptCipher.init(javax.crypto.Cipher.DECRYPT_MODE, createSecretKeySpec(keyBytes), new IvParameterSpec(initializationVector));
45 |
46 | byte[] decryptedBytes = decryptCipher.doFinal(Base64.getDecoder().decode(encodedCipherText));
47 | return new String(decryptedBytes, StandardCharsets.UTF_8);
48 | }
49 |
50 | public static byte[] generateKey() throws NoSuchAlgorithmException {
51 | KeyGenerator keygen = KeyGenerator.getInstance("AES");
52 | keygen.init(128);
53 | return keygen.generateKey().getEncoded();
54 | }
55 |
56 | public static String encrypt(String cipherKey, String value) throws GeneralSecurityException {
57 | byte[] keyBytes = Base64.getDecoder().decode(cipherKey);
58 | byte[] initializationVector = generateKey();
59 | byte[] bytesToEncrypt = value.getBytes(StandardCharsets.UTF_8);
60 |
61 | javax.crypto.Cipher encryptCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding");
62 | encryptCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, createSecretKeySpec(keyBytes), new IvParameterSpec(initializationVector));
63 |
64 | byte[] encryptedBytes = encryptCipher.doFinal(bytesToEncrypt);
65 |
66 | return String.join(":", "AES", encode(initializationVector), encode(encryptedBytes));
67 | }
68 |
69 | private static String encode(byte[] initializationVector) {
70 | return Base64.getEncoder().encodeToString(initializationVector);
71 | }
72 |
73 | private static SecretKeySpec createSecretKeySpec(byte[] key) {
74 | return new SecretKeySpec(key, "AES");
75 | }
76 |
77 | private static boolean canDecrypt(String cipherText) {
78 | if (isBlank(cipherText)) {
79 | return false;
80 | }
81 | String[] splits = cipherText.split(":");
82 | return splits.length == 3 && "AES".equals(splits[0]) && (!isBlank(splits[1])) && (!isBlank(splits[2]));
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/util/FileStat.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.util;
18 |
19 | import lombok.Getter;
20 | import org.apache.commons.io.IOUtils;
21 | import org.apache.commons.io.output.NullOutputStream;
22 | import org.apache.commons.lang3.ArrayUtils;
23 |
24 | import java.io.*;
25 | import java.security.DigestOutputStream;
26 | import java.security.MessageDigest;
27 | import java.security.NoSuchAlgorithmException;
28 | import java.util.Arrays;
29 | import java.util.Collections;
30 | import java.util.List;
31 |
32 | public class FileStat {
33 |
34 | private final File file;
35 |
36 | private long lastStatTime;
37 |
38 | private boolean exists;
39 |
40 | private long lastModified;
41 |
42 | private boolean directory;
43 |
44 | private long length;
45 |
46 | private List digest;
47 |
48 | @Getter(lazy = true)
49 | // lazy initialize, because performance
50 | private final MessageDigest messageDigest = createDigester();
51 |
52 | public FileStat(File file) {
53 | this.file = file;
54 | }
55 |
56 | public boolean changed(int withinInterval) {
57 | if (System.currentTimeMillis() <= lastStatTime + withinInterval) {
58 | return false;
59 | }
60 | lastStatTime = System.currentTimeMillis();
61 | // cache original values
62 | final boolean origExists = exists;
63 | final long origLastModified = lastModified;
64 | final boolean origDirectory = directory;
65 | final long origLength = length;
66 | final List origDigest = digest;
67 |
68 | // refresh the values
69 | refresh();
70 |
71 | // check if any values have changed
72 | return exists != origExists ||
73 | lastModified != origLastModified ||
74 | directory != origDirectory ||
75 | length != origLength ||
76 | !digest.equals(origDigest);
77 | }
78 |
79 | void refresh() {
80 | exists = file.exists();
81 | directory = exists && file.isDirectory();
82 | lastModified = exists ? file.lastModified() : 0;
83 | length = (exists && !directory) ? file.length() : 0;
84 | digest = (exists && !directory) ? computeDigest() : Collections.emptyList();
85 | }
86 |
87 | private List computeDigest() {
88 | MessageDigest messageDigest = getMessageDigest();
89 | messageDigest.reset();
90 | try (InputStream is = new BufferedInputStream(new FileInputStream(file), 1024 * 32)) {
91 | IOUtils.copy(is, new DigestOutputStream(new NullOutputStream(), messageDigest));
92 | } catch (IOException e) {
93 | throw new UncheckedIOException(e);
94 | }
95 |
96 | return Arrays.asList(ArrayUtils.toObject(messageDigest.digest()));
97 | }
98 |
99 | private static MessageDigest createDigester() {
100 | try {
101 | return MessageDigest.getInstance("SHA-256");
102 | } catch (NoSuchAlgorithmException e) {
103 | throw new RuntimeException();
104 | }
105 | }
106 |
107 | public File getFile() {
108 | return file;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/db/src/test/java/cd/go/plugin/secret/filebased/db/SecretsDatabaseTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.db;
18 |
19 | import org.junit.jupiter.api.Nested;
20 | import org.junit.jupiter.api.Test;
21 | import org.junit.jupiter.api.io.TempDir;
22 |
23 | import java.io.File;
24 | import java.io.IOException;
25 | import java.security.GeneralSecurityException;
26 |
27 | import static org.assertj.core.api.Assertions.assertThat;
28 |
29 | class SecretsDatabaseTest {
30 |
31 | @Test
32 | void shouldAddASecret() throws GeneralSecurityException {
33 | SecretsDatabase secretsDatabase = new SecretsDatabase();
34 | secretsDatabase.addSecret("foo", "bar");
35 | assertThat(secretsDatabase.getSecret("foo")).isEqualTo("bar");
36 |
37 | }
38 |
39 | @Test
40 | void decryptingASecretShouldCacheIt() throws GeneralSecurityException {
41 | SecretsDatabase secretsDatabase = new SecretsDatabase();
42 |
43 | secretsDatabase.addSecret("foo", "bar");
44 | assertThat(secretsDatabase.decryptedSecrets).doesNotContainKeys("foo");
45 |
46 | secretsDatabase.getSecret("foo");
47 | assertThat(secretsDatabase.decryptedSecrets).containsKeys("foo");
48 | }
49 |
50 | @Test
51 | void removingASecretShouldRemoveItFromCache() throws GeneralSecurityException {
52 | SecretsDatabase secretsDatabase = new SecretsDatabase();
53 |
54 | secretsDatabase.addSecret("foo", "bar");
55 |
56 | // populate cache
57 | secretsDatabase.getSecret("foo");
58 | assertThat(secretsDatabase.decryptedSecrets).containsKeys("foo");
59 |
60 | // remove secret
61 | secretsDatabase.removeSecret("foo");
62 | assertThat(secretsDatabase.decryptedSecrets).doesNotContainKeys("foo");
63 | }
64 |
65 | @Test
66 | void addingASecretShouldRemoveItFromCache() throws GeneralSecurityException {
67 | SecretsDatabase secretsDatabase = new SecretsDatabase();
68 |
69 | secretsDatabase.addSecret("foo", "bar");
70 |
71 | // populate cache
72 | secretsDatabase.getSecret("foo");
73 | assertThat(secretsDatabase.decryptedSecrets).containsKeys("foo");
74 |
75 | // add secret
76 | secretsDatabase.addSecret("foo", "bar");
77 | assertThat(secretsDatabase.decryptedSecrets).doesNotContainKeys("foo");
78 | }
79 |
80 | @Nested
81 | class Persistance {
82 |
83 | @Test
84 | void shouldPersistDBToDisk(@TempDir File tempDir) throws GeneralSecurityException, IOException {
85 | SecretsDatabase secretsDatabase = new SecretsDatabase();
86 |
87 | secretsDatabase.addSecret("foo", "bar");
88 |
89 | secretsDatabase.saveTo(new File(tempDir, "db.json"));
90 |
91 | SecretsDatabase loadedDB = SecretsDatabase.readFrom(new File(tempDir, "db.json"));
92 |
93 | // is able to decrypt
94 | assertThat(loadedDB.getSecret("foo")).isEqualTo("bar");
95 |
96 | // and saves the passphrase and all secrets
97 | assertThat(secretsDatabase.getSecretKey()).isEqualTo(loadedDB.getSecretKey());
98 | assertThat(secretsDatabase.getSecrets()).isEqualTo(loadedDB.getSecrets());
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/cli/src/main/java/cd/go/plugin/secret/filebased/cli/Main.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli;
18 |
19 | import cd.go.plugin.secret.filebased.cli.args.*;
20 | import com.beust.jcommander.JCommander;
21 | import com.beust.jcommander.ParameterException;
22 |
23 | import java.util.function.Consumer;
24 |
25 | public class Main {
26 |
27 | private final String[] argv;
28 |
29 | public Main(String... argv) {
30 | this.argv = argv;
31 | }
32 |
33 | public static void main(String[] argv) {
34 | new Main(argv).run((exitStatus) -> System.exit(exitStatus));
35 | }
36 |
37 | void run(Consumer exitter) {
38 | RootArgs rootArgs = new RootArgs();
39 | InitArgs initArgs = new InitArgs();
40 | AddSecretArgs addSecretArgs = new AddSecretArgs();
41 | RemoveSecretArgs removeSecretArgs = new RemoveSecretArgs();
42 | ShowSecretArgs showSecretArgs = new ShowSecretArgs();
43 | ShowAllSecretKeysArgs keysArgs = new ShowAllSecretKeysArgs();
44 |
45 | JCommander cmd = JCommander.newBuilder()
46 | .addObject(rootArgs)
47 | .addCommand(initArgs)
48 | .addCommand(addSecretArgs)
49 | .addCommand(removeSecretArgs)
50 | .addCommand(showSecretArgs)
51 | .addCommand(keysArgs)
52 | .build();
53 |
54 | String parsedCommand = null;
55 |
56 | try {
57 | cmd.parse(argv);
58 | parsedCommand = cmd.getParsedCommand();
59 |
60 | if (rootArgs.help || parsedCommand == null) {
61 | printUsageAndExit(cmd, parsedCommand, 1, exitter);
62 | }
63 |
64 | switch (parsedCommand) {
65 | case "init":
66 | initArgs.execute(exitter);
67 | break;
68 | case "add":
69 | addSecretArgs.execute(exitter);
70 | break;
71 | case "remove":
72 | removeSecretArgs.execute(exitter);
73 | break;
74 | case "show":
75 | showSecretArgs.execute(exitter);
76 | break;
77 | case "keys":
78 | keysArgs.execute(exitter);
79 | break;
80 | default:
81 | throw new UnsupportedOperationException(parsedCommand);
82 | }
83 | } catch (ParameterException e) {
84 | System.err.println(e.getMessage());
85 | printUsageAndExit(cmd, parsedCommand, 1, exitter);
86 | } catch (Exception e) {
87 | e.printStackTrace(System.err);
88 | exitter.accept(-1);
89 | }
90 | }
91 |
92 | private static void printUsageAndExit(JCommander cmd, String parsedCommand, int statusCode, Consumer exitter) {
93 | StringBuilder out = new StringBuilder();
94 | cmd.setProgramName("java -jar ");
95 | cmd.setColumnSize(100);
96 | if (parsedCommand == null) {
97 | cmd.usage(out);
98 | } else {
99 | cmd.usage(parsedCommand, out);
100 | }
101 | System.err.println(out.toString());
102 | exitter.accept(statusCode);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/db/src/main/java/cd/go/plugin/secret/filebased/db/SecretsDatabase.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.db;
18 |
19 | import com.google.gson.Gson;
20 | import com.google.gson.GsonBuilder;
21 | import com.google.gson.annotations.Expose;
22 | import com.google.gson.annotations.SerializedName;
23 | import org.apache.commons.io.FileUtils;
24 |
25 | import java.io.File;
26 | import java.io.IOException;
27 | import java.nio.charset.StandardCharsets;
28 | import java.security.GeneralSecurityException;
29 | import java.security.NoSuchAlgorithmException;
30 | import java.util.Base64;
31 | import java.util.LinkedHashMap;
32 | import java.util.Set;
33 |
34 | import static org.apache.commons.io.FileUtils.readFileToString;
35 |
36 | public class SecretsDatabase {
37 |
38 | private static final Gson GSON = new GsonBuilder()
39 | .excludeFieldsWithoutExposeAnnotation()
40 | .serializeNulls()
41 | .setPrettyPrinting()
42 | .create();
43 |
44 | @Expose
45 | @SerializedName("secret_key")
46 | private final String secretKey;
47 |
48 | @Expose
49 | @SerializedName("secrets")
50 | private final LinkedHashMap secrets = new LinkedHashMap<>();
51 |
52 | final LinkedHashMap decryptedSecrets = new LinkedHashMap<>();
53 |
54 | public SecretsDatabase(String secretKey) {
55 | this.secretKey = secretKey;
56 | }
57 |
58 | public SecretsDatabase() throws NoSuchAlgorithmException {
59 | this(Base64.getEncoder().encodeToString(Cipher.generateKey()));
60 | }
61 |
62 | public SecretsDatabase addSecret(String name, String value) throws GeneralSecurityException {
63 | synchronized (this) {
64 | secrets.put(name, Cipher.encrypt(secretKey, value));
65 | decryptedSecrets.remove(name);
66 | }
67 | return this;
68 | }
69 |
70 | public String getSecret(String name) {
71 | synchronized (this) {
72 | return decryptedSecrets.computeIfAbsent(name, key -> {
73 | if (secrets.containsKey(name)) {
74 | try {
75 | return Cipher.decrypt(secretKey, secrets.get(name));
76 | } catch (BadSecretException | GeneralSecurityException e) {
77 | throw new RuntimeException(e);
78 | }
79 | }
80 | return null;
81 | });
82 | }
83 | }
84 |
85 | public Set getAllSecretKeys() {
86 | return secrets.keySet();
87 | }
88 |
89 | public SecretsDatabase removeSecret(String name) {
90 | synchronized (this) {
91 | secrets.remove(name);
92 | decryptedSecrets.remove(name);
93 | }
94 | return this;
95 | }
96 |
97 | public static SecretsDatabase readFrom(File secretFile) throws IOException {
98 | return GSON.fromJson(readFileToString(secretFile, StandardCharsets.UTF_8), SecretsDatabase.class);
99 | }
100 |
101 | public SecretsDatabase saveTo(File secretFile) throws IOException {
102 | FileUtils.write(secretFile, toJSON(), StandardCharsets.UTF_8);
103 | return this;
104 | }
105 |
106 | public String toJSON() {
107 | return GSON.toJson(this);
108 | }
109 |
110 |
111 | // for testing
112 | String getSecretKey() {
113 | return secretKey;
114 | }
115 |
116 | LinkedHashMap getSecrets() {
117 | return secrets;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/.idea/codeStyleSettings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/main/java/cd/go/plugin/secret/filebased/executors/LookupSecretsRequestExecutor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.executors;
18 |
19 | import cd.go.plugin.base.executors.secrets.LookupExecutor;
20 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
21 | import cd.go.plugin.secret.filebased.model.LookupSecretRequest;
22 | import cd.go.plugin.secret.filebased.util.LRUCache;
23 | import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse;
24 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse;
25 |
26 | import java.io.File;
27 | import java.io.IOException;
28 | import java.io.UncheckedIOException;
29 | import java.util.*;
30 | import java.util.function.BiFunction;
31 |
32 | import static java.util.Collections.synchronizedMap;
33 |
34 | public class LookupSecretsRequestExecutor extends LookupExecutor {
35 |
36 | private static final int NOT_FOUND_ERROR_CODE = 404;
37 |
38 | private static final int MAX_ENTRIES = 512;
39 |
40 | // cheap cache implementation
41 | private static final Map FILE_STAT_CACHE = synchronizedMap(new LRUCache<>(MAX_ENTRIES));
42 |
43 | @Override
44 | protected GoPluginApiResponse execute(LookupSecretRequest lookupSecretsRequest) {
45 | List> responseList = new ArrayList<>();
46 |
47 | File secretsFile = new File(lookupSecretsRequest.getSecretsFilePath());
48 |
49 |
50 | List unresolvedKeys = new ArrayList<>();
51 |
52 | try {
53 | CacheEntry cacheEntry = FILE_STAT_CACHE.compute(secretsFile, FileCacheEntryCacheEntryBiFunction.INSTANCE);
54 | SecretsDatabase secretsDatabase = cacheEntry.getSecretsDatabase();
55 |
56 | for (String key : lookupSecretsRequest.getKeys()) {
57 | String secret = secretsDatabase.getSecret(key);
58 | if (secret != null) {
59 | Map response = new HashMap<>();
60 | response.put("key", key);
61 | response.put("value", secret);
62 | responseList.add(response);
63 | } else {
64 | unresolvedKeys.add(key);
65 | }
66 | }
67 |
68 | if (unresolvedKeys.isEmpty()) {
69 | return DefaultGoPluginApiResponse.success(GSON.toJson(responseList));
70 | }
71 |
72 | Map response = Collections.singletonMap("message", String.format("Secrets with keys %s not found.", unresolvedKeys));
73 | return new DefaultGoPluginApiResponse(NOT_FOUND_ERROR_CODE, GSON.toJson(response));
74 | } catch (IOException e) {
75 | Map errorMessage = Collections.singletonMap("message", "Error while looking up secrets: " + e.getMessage());
76 | return DefaultGoPluginApiResponse.error(GSON.toJson(errorMessage));
77 | }
78 | }
79 |
80 | @Override
81 | protected LookupSecretRequest parseRequest(String body) {
82 | return LookupSecretRequest.fromJSON(body);
83 | }
84 |
85 | public static class FileCacheEntryCacheEntryBiFunction implements BiFunction {
86 |
87 | public static FileCacheEntryCacheEntryBiFunction INSTANCE = new FileCacheEntryCacheEntryBiFunction();
88 |
89 | private FileCacheEntryCacheEntryBiFunction() {
90 | }
91 |
92 | @Override
93 | public CacheEntry apply(File file, CacheEntry existingCacheEntry) {
94 | if (existingCacheEntry == null) {
95 | existingCacheEntry = new CacheEntry(file);
96 | }
97 |
98 | try {
99 | existingCacheEntry.refresh();
100 | } catch (IOException e) {
101 | throw new UncheckedIOException(e);
102 | }
103 |
104 | return existingCacheEntry;
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/test/java/cd/go/plugin/secret/filebased/executors/LookupSecretsRequestExecutorTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.executors;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import cd.go.plugin.secret.filebased.model.LookupSecretRequest;
21 | import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest;
22 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse;
23 | import org.json.JSONException;
24 | import org.junit.jupiter.api.BeforeEach;
25 | import org.junit.jupiter.api.Test;
26 | import org.junit.jupiter.api.io.TempDir;
27 |
28 | import java.io.File;
29 | import java.io.IOException;
30 | import java.nio.file.Path;
31 | import java.security.GeneralSecurityException;
32 | import java.util.Arrays;
33 | import java.util.UUID;
34 |
35 | import static org.assertj.core.api.Assertions.assertThat;
36 | import static org.mockito.Mockito.mock;
37 | import static org.mockito.Mockito.when;
38 | import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
39 |
40 | class LookupSecretsRequestExecutorTest {
41 |
42 | private File databaseFile;
43 |
44 | @BeforeEach
45 | void setup(@TempDir Path tempDir) throws GeneralSecurityException, IOException {
46 | this.databaseFile = new File(tempDir.toFile(), UUID.randomUUID().toString().substring(0, 8));
47 | new SecretsDatabase()
48 | .addSecret("secret-key", "secret-value")
49 | .addSecret("username", "foo")
50 | .addSecret("password", "bar").saveTo(databaseFile);
51 | }
52 |
53 | @Test
54 | void shouldLookupSecrets() throws JSONException {
55 | GoPluginApiRequest request = mock(GoPluginApiRequest.class);
56 | when(request.requestBody()).thenReturn(new LookupSecretRequest(databaseFile.getAbsolutePath(),
57 | Arrays.asList("secret-key", "username", "password")).toJSON());
58 |
59 | GoPluginApiResponse response = new LookupSecretsRequestExecutor().execute(request);
60 |
61 | assertThat(response.responseCode()).isEqualTo(200);
62 | assertEquals("[\n" +
63 | " {\n" +
64 | " \"key\": \"secret-key\",\n" +
65 | " \"value\": \"secret-value\"\n" +
66 | " },\n" +
67 | " {\n" +
68 | " \"key\": \"username\",\n" +
69 | " \"value\": \"foo\"\n" +
70 | " },\n" +
71 | " {\n" +
72 | " \"key\": \"password\",\n" +
73 | " \"value\": \"bar\"\n" +
74 | " }\n" +
75 | "]", response.responseBody(), true);
76 | }
77 |
78 | @Test
79 | void shouldReturnEmptyResponseWhenSecretsAreNotPresent() throws JSONException {
80 | GoPluginApiRequest request = mock(GoPluginApiRequest.class);
81 | when(request.requestBody()).thenReturn(
82 | new LookupSecretRequest(databaseFile.getAbsolutePath(), Arrays.asList("randomKey1", "randomKey2")).toJSON());
83 |
84 | GoPluginApiResponse response = new LookupSecretsRequestExecutor().execute(request);
85 |
86 | assertThat(response.responseCode()).isEqualTo(404);
87 | assertEquals("{\"message\":\"Secrets with keys [randomKey1, randomKey2] not found.\"}", response.responseBody(), true);
88 | }
89 |
90 | @Test
91 | void shouldErrorAsAMapWhenAnyExceptionOccurs() throws JSONException {
92 | GoPluginApiRequest goPluginApiRequest = mock(GoPluginApiRequest.class);
93 | when(goPluginApiRequest.requestBody()).thenReturn(
94 | new LookupSecretRequest("some-non-existent-file.db", Arrays.asList("randomKey1", "randomKey2")).toJSON());
95 |
96 | GoPluginApiResponse response = new LookupSecretsRequestExecutor().execute(goPluginApiRequest);
97 |
98 | assertThat(response.responseCode()).isEqualTo(500);
99 | assertEquals("{\"message\":\"Error while looking up secrets: File \\u0027some-non-existent-file.db\\u0027 does not exist\"}", response.responseBody(), true);
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/jar-class-loader/src/main/java/com/thoughtworks/gocd/Boot.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.thoughtworks.gocd;
18 |
19 | import com.thoughtworks.gocd.onejar.Handler;
20 |
21 | import java.io.*;
22 | import java.lang.management.ManagementFactory;
23 | import java.lang.management.RuntimeMXBean;
24 | import java.net.URISyntaxException;
25 | import java.net.URL;
26 | import java.net.URLClassLoader;
27 | import java.util.ArrayList;
28 | import java.util.Arrays;
29 | import java.util.Date;
30 | import java.util.List;
31 | import java.util.function.Predicate;
32 | import java.util.jar.JarEntry;
33 | import java.util.jar.JarFile;
34 | import java.util.stream.Collectors;
35 |
36 | public class Boot {
37 |
38 | private static final Predicate FIND_JAR_FILES_UNDER_LIB_FOLDER = jarEntry -> jarEntry.getName().startsWith("lib/") && jarEntry.getName().endsWith(".jar");
39 |
40 | private final String[] args;
41 |
42 | private Boot(String... args) {
43 | this.args = args;
44 | }
45 |
46 | public static void main(String... args) {
47 | if (shouldRedirectStdOutAndErr()) {
48 | redirectStdOutAndErr();
49 | }
50 |
51 | log("Starting process: ");
52 | log(" Working directory : " + System.getProperty("user.dir"));
53 | log(" JVM arguments : " + jvmArgs());
54 | log(" Application arguments: " + Arrays.asList(args));
55 | log(" GoCD Version: " + Boot.class.getPackage().getImplementationVersion());
56 | log(" JVM properties: " + System.getProperties());
57 | log(" Environment Variables: " + System.getenv());
58 | new Boot(args).run();
59 | }
60 |
61 | private static void redirectStdOutAndErr() {
62 | try {
63 | PrintStream out = new PrintStream(new FileOutputStream(getOutFile(), true), true);
64 | System.setErr(out);
65 | System.setOut(out);
66 | } catch (FileNotFoundException ignore) {
67 | // cannot redirect out and err to file, so we don't
68 | log("Unable to redirect stdout/stderr to file " + getOutFile() + ". Will continue without redirecting stdout/stderr.");
69 | ignore.printStackTrace();
70 | }
71 | }
72 |
73 | private static List jvmArgs() {
74 | RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
75 | return bean.getInputArguments();
76 | }
77 |
78 | private static void log(String message) {
79 | if ("true".equalsIgnoreCase(System.getProperty("boot.debug"))) {
80 | System.err.println("[" + new Date() + "] " + message);
81 | }
82 | }
83 |
84 | private static String getOutFile() {
85 | return System.getProperty("gocd.redirect.stdout.to.file");
86 | }
87 |
88 | private static boolean shouldRedirectStdOutAndErr() {
89 | return getOutFile() != null;
90 | }
91 |
92 | private void run() {
93 | Handler.init();
94 | try (JarFile jarFile = new JarFile(currentJarFile())) {
95 | String mainClassName = mainClassName(jarFile);
96 |
97 | List jarsInJar = jarFile.stream()
98 | .filter(FIND_JAR_FILES_UNDER_LIB_FOLDER)
99 | .map(jarEntry -> Handler.toOneJarUrl(jarEntry.getName()))
100 | .collect(Collectors.toList());
101 |
102 | log("Extracting jars files: " + jarsInJar);
103 | List urls = new ArrayList<>();
104 | urls.addAll(jarsInJar);
105 |
106 | ClassLoader jcl = new URLClassLoader(urls.toArray(new URL[0]), getClass().getClassLoader());
107 | Thread.currentThread().setContextClassLoader(jcl);
108 | Class> mainClass = jcl.loadClass(mainClassName);
109 | mainClass.getMethod("main", String[].class).invoke(null, new Object[]{args});
110 | } catch (IOException | ReflectiveOperationException e) {
111 | e.printStackTrace();
112 | System.exit(1);
113 | }
114 | }
115 |
116 | static File currentJarFile() {
117 | try {
118 | return new File(Boot.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath());
119 | } catch (URISyntaxException e) {
120 | throw new RuntimeException(e);
121 | }
122 | }
123 |
124 | static String mainClassName(JarFile jarFile) throws IOException {
125 | return jarFile.getManifest().getMainAttributes().getValue("GoCD-Main-Class");
126 | }
127 |
128 | }
129 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 | # Determine the Java command to use to start the JVM.
86 | if [ -n "$JAVA_HOME" ] ; then
87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
88 | # IBM's JDK on AIX uses strange locations for the executables
89 | JAVACMD="$JAVA_HOME/jre/sh/java"
90 | else
91 | JAVACMD="$JAVA_HOME/bin/java"
92 | fi
93 | if [ ! -x "$JAVACMD" ] ; then
94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
95 |
96 | Please set the JAVA_HOME variable in your environment to match the
97 | location of your Java installation."
98 | fi
99 | else
100 | JAVACMD="java"
101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
102 |
103 | Please set the JAVA_HOME variable in your environment to match the
104 | location of your Java installation."
105 | fi
106 |
107 | # Increase the maximum file descriptors if we can.
108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
109 | MAX_FD_LIMIT=`ulimit -H -n`
110 | if [ $? -eq 0 ] ; then
111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
112 | MAX_FD="$MAX_FD_LIMIT"
113 | fi
114 | ulimit -n $MAX_FD
115 | if [ $? -ne 0 ] ; then
116 | warn "Could not set maximum file descriptor limit: $MAX_FD"
117 | fi
118 | else
119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
120 | fi
121 | fi
122 |
123 | # For Darwin, add options to specify how the application appears in the dock
124 | if $darwin; then
125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
126 | fi
127 |
128 | # For Cygwin or MSYS, switch paths to Windows format before running java
129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
132 | JAVACMD=`cygpath --unix "$JAVACMD"`
133 |
134 | # We build the pattern for arguments to be converted via cygpath
135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
136 | SEP=""
137 | for dir in $ROOTDIRSRAW ; do
138 | ROOTDIRS="$ROOTDIRS$SEP$dir"
139 | SEP="|"
140 | done
141 | OURCYGPATTERN="(^($ROOTDIRS))"
142 | # Add a user-defined pattern to the cygpath arguments
143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
145 | fi
146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
147 | i=0
148 | for arg in "$@" ; do
149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
151 |
152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
154 | else
155 | eval `echo args$i`="\"$arg\""
156 | fi
157 | i=$((i+1))
158 | done
159 | case $i in
160 | (0) set -- ;;
161 | (1) set -- "$args0" ;;
162 | (2) set -- "$args0" "$args1" ;;
163 | (3) set -- "$args0" "$args1" "$args2" ;;
164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
170 | esac
171 | fi
172 |
173 | # Escape application args
174 | save () {
175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
176 | echo " "
177 | }
178 | APP_ARGS=$(save "$@")
179 |
180 | # Collect all arguments for the java command, following the shell quoting and substitution rules
181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
182 |
183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
185 | cd "$(dirname "$0")"
186 | fi
187 |
188 | exec "$JAVACMD" "$@"
189 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GoCD File Based Secrets Plugin
2 |
3 | This is a file based secrets plugin which implements the GoCD [Secret Plugin](https://plugin-api.gocd.org/current/secrets) endpoint. This plugin allows retrieving of secrets that are stored in encrypted files.
4 |
5 | For comprehensive details about Secret Management in GoCD please refer to the [documentation](https://docs.gocd.org/current/configuration/secrets_management.html).
6 |
7 | ## Building the code base
8 |
9 | To build the jar, run `./gradlew clean test assemble`
10 |
11 | ## Installation
12 |
13 | - This plugin comes bundled along with the GoCD server, hence a separate installation is not required.
14 | - GoCD introduced support for Secrets Management in v19.6.0, in order to use this plugin your GoCD version should >= 19.6.0.
15 |
16 | ## Usage instructions
17 |
18 | 1. Download the plugin jar from the [GitHub Releases page](https://github.com/gocd/gocd-file-based-secrets-plugin)
19 | 2. Execute the `init` command to initialize the secret database. Although it's optional but it is recommended to
20 | store your secret file under CONFIG_DIR. Doing this will make secrets database file part of the backup process.
21 | The CONFIG_DIR is typically /etc/go on Linux and C:\Program Files\Go Server\config on Windows.
22 |
23 | ```shell
24 | java -jar gocd-file-based-secrets-plugin-$VERSION$.jar init -f secret.db
25 | ```
26 | 3. Add/Update a secret:
27 | ```shell
28 | java -jar gocd-file-based-secrets-plugin-$VERSION$.jar add -f secret.db -n my-password -v
29 | ```
30 | 4. Show the value of the secret:
31 | ```shell
32 | java -jar gocd-file-based-secrets-plugin-$VERSION$.jar show -f secret.db -n my-password
33 | ```
34 | 5. Show all secret keys:
35 | ```shell
36 | java -jar gocd-file-based-secrets-plugin-$VERSION$.jar keys -f secret.db
37 | ```
38 | 6. Remove a secret:
39 | ```shell
40 | java -jar gocd-file-based-secrets-plugin-$VERSION$.jar remove -f secret.db -n my-password
41 | ```
42 |
43 | ## Configuration
44 |
45 | The plugin needs to be configured to use the secrets database file.
46 |
47 | The configuration can be added directly to the `config.xml` using the `` configuration.
48 |
49 | * Example Configuration
50 |
51 | ```xml
52 |
53 |
54 | All secrets for env1
55 |
56 |
57 | SecretsFilePath
58 | /godata/config/secretsDatabase.json
59 |
60 |
61 |
62 | env_*
63 | my_group
64 | other_group
65 |
66 |
67 |
68 | ```
69 |
70 | `` tag defines where this secretConfig is allowed/denied to be referred. For more details about rules and examples refer the GoCD Secret Management [documentation](https://docs.gocd.org/current/configuration/secrets_management.html)
71 |
72 | * The plugin can also be configured to use multiple secret database files if required:
73 |
74 | ```xml
75 |
76 |
77 | All secrets for env1
78 |
79 |
80 | SecretsFilePath
81 | /godata/config/secretsDatabase.json
82 |
83 |
84 |
85 | env_*
86 | my_group
87 | other_group
88 |
89 |
90 |
91 | All secrets for env1
92 |
93 |
94 | SecretsFilePath
95 | /godata/config/secretsDatabase_env2.json
96 |
97 |
98 |
99 | env_*
100 | my_group
101 | other_group
102 |
103 |
104 |
105 | ```
106 |
107 | A secret file is made of JSON, and has the following data structure:
108 |
109 | ```json
110 | [
111 | { "key": "foo", "value": "bar" }
112 | ]
113 | ```
114 |
115 | The secret defined in the above example can be used as: `{{SECRET:[Env1Secrets][foo]}}` and will have the value `bar` at run-time.
116 |
117 | ## Troubleshooting
118 |
119 | ### Verify Connection
120 |
121 | For a given secret config verify if the file database can be accessed by the plugin. The *Secrets Configuration* page under *Admin > Security* gives an option to verify connection.
122 |
123 | ### Enable Debug Logs
124 |
125 | Edit the file `wrapper-properties.conf` on your GoCD server and add the following options. The location of the `wrapper-properties.conf` can be found in the [installation documentation](https://docs.gocd.org/current/installation/installing_go_server.html) of the GoCD server.
126 |
127 | ```properties
128 | # We recommend that you begin with the index `100` and increment the index for each system property
129 | wrapper.java.additional.100=-Dplugin.cd.go.secrets.file-based-plugin.log.level=debug
130 | ```
131 |
132 | If you're running with GoCD server 19.6 and above on docker using one of the supported GoCD server images, set the environment variable `GOCD_SERVER_JVM_OPTIONS`:
133 |
134 | ```shell
135 | docker run -e "GOCD_SERVER_JVM_OPTIONS=-Dplugin.cd.go.secrets.file-based-plugin.log.level=debug" ...
136 | ```
137 |
138 | ## License
139 |
140 | ```plain
141 | Copyright 2019 ThoughtWorks, Inc.
142 |
143 | Licensed under the Apache License, Version 2.0 (the "License");
144 | you may not use this file except in compliance with the License.
145 | You may obtain a copy of the License at
146 |
147 | http://www.apache.org/licenses/LICENSE-2.0
148 |
149 | Unless required by applicable law or agreed to in writing, software
150 | distributed under the License is distributed on an "AS IS" BASIS,
151 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
152 | See the License for the specific language governing permissions and
153 | limitations under the License.
154 | ```
155 |
--------------------------------------------------------------------------------
/gocd-file-based-secrets-plugin/src/test/java/cd/go/plugin/secret/filebased/FileBasedSecretsPluginTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import cd.go.plugin.secret.filebased.model.LookupSecretRequest;
21 | import com.google.gson.Gson;
22 | import com.thoughtworks.go.plugin.api.GoApplicationAccessor;
23 | import com.thoughtworks.go.plugin.api.exceptions.UnhandledRequestTypeException;
24 | import com.thoughtworks.go.plugin.api.request.DefaultGoPluginApiRequest;
25 | import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse;
26 | import org.json.JSONException;
27 | import org.junit.jupiter.api.BeforeEach;
28 | import org.junit.jupiter.api.Nested;
29 | import org.junit.jupiter.api.Test;
30 | import org.junit.jupiter.api.io.TempDir;
31 | import org.skyscreamer.jsonassert.JSONAssert;
32 |
33 | import java.io.File;
34 | import java.io.IOException;
35 | import java.nio.file.Path;
36 | import java.security.GeneralSecurityException;
37 | import java.util.Arrays;
38 | import java.util.Collections;
39 | import java.util.UUID;
40 |
41 | import static cd.go.plugin.secret.filebased.model.SecretsConfiguration.SECRETS_FILE_PATH_PROPERTY;
42 | import static org.assertj.core.api.Assertions.assertThat;
43 | import static org.mockito.Mockito.mock;
44 | import static org.skyscreamer.jsonassert.JSONAssert.assertEquals;
45 |
46 | class FileBasedSecretsPluginTest {
47 |
48 | private static final String LOOKUP_SECRET_REQUEST_NAME = "go.cd.secrets.secrets-lookup";
49 |
50 | private static final String REQUEST_VALIDATE_CONFIG = "go.cd.secrets.secrets-config.validate";
51 |
52 | private FileBasedSecretsPlugin secretsPlugin;
53 |
54 | private File databaseFile;
55 |
56 | @BeforeEach
57 | void setup(@TempDir Path tempDir) throws GeneralSecurityException, IOException {
58 | this.databaseFile = new File(tempDir.toFile(), UUID.randomUUID().toString().substring(0, 8));
59 | new SecretsDatabase()
60 | .addSecret("secret-key", "secret-value")
61 | .addSecret("username", "foo")
62 | .addSecret("password", "bar").saveTo(databaseFile);
63 |
64 |
65 | secretsPlugin = new FileBasedSecretsPlugin();
66 | secretsPlugin.initializeGoApplicationAccessor(mock(GoApplicationAccessor.class));
67 | }
68 |
69 | @Nested
70 | class ValidateConfig {
71 |
72 | @Test
73 | void shouldBeValidWhenSecretConfigPathIsValid(@TempDir File testDir) throws UnhandledRequestTypeException, IOException {
74 | File secretFile = new File(testDir, "secret.db");
75 | secretFile.createNewFile();
76 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("secrets", "1.0", REQUEST_VALIDATE_CONFIG);
77 | request.setRequestBody(new Gson().toJson(Collections.singletonMap(SECRETS_FILE_PATH_PROPERTY, secretFile.getAbsolutePath())));
78 |
79 | GoPluginApiResponse response = secretsPlugin.handle(request);
80 |
81 | assertThat(response.responseCode()).isEqualTo(200);
82 | assertThat(response.responseBody()).isEqualTo("[]");
83 | }
84 |
85 | @Test
86 | void shouldBeErrorWhenSecretFilePathIsNotProvided() throws UnhandledRequestTypeException {
87 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("secrets", "1.0", REQUEST_VALIDATE_CONFIG);
88 | request.setRequestBody(new Gson().toJson(Collections.singletonMap(SECRETS_FILE_PATH_PROPERTY, "")));
89 |
90 | GoPluginApiResponse response = secretsPlugin.handle(request);
91 |
92 | assertThat(response.responseCode()).isEqualTo(200);
93 | assertThat(response.responseBody()).isEqualTo("[{\"key\":\"SecretsFilePath\",\"message\":\"SecretsFilePath must not be blank.\"}]");
94 | }
95 |
96 | @Test
97 | void shouldBeErrorWhenSecretFileDoesNotExist(@TempDir File testDir) throws UnhandledRequestTypeException, JSONException {
98 | File noneExistingFile = new File(testDir, "none-existing-file");
99 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("secrets", "1.0", REQUEST_VALIDATE_CONFIG);
100 | request.setRequestBody(new Gson().toJson(Collections.singletonMap(SECRETS_FILE_PATH_PROPERTY, noneExistingFile.getAbsolutePath())));
101 |
102 | GoPluginApiResponse response = secretsPlugin.handle(request);
103 |
104 | assertThat(response.responseCode()).isEqualTo(200);
105 |
106 | String expected = "[\n" +
107 | " {\n" +
108 | " \"key\": \"SecretsFilePath\",\n" +
109 | " \"message\": \"" + String.format("No secret config file at path '%s'.", noneExistingFile.getAbsolutePath()) + "\"\n" +
110 | " }\n" +
111 | "]";
112 |
113 | JSONAssert.assertEquals(expected, response.responseBody(), true);
114 | }
115 |
116 | @Test
117 | void shouldBeErrorWhenSecretFileIsDirectory(@TempDir File testDir) throws UnhandledRequestTypeException, JSONException {
118 | File notAFile = new File(testDir, "this-is-dir");
119 | notAFile.mkdir();
120 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("secrets", "1.0", REQUEST_VALIDATE_CONFIG);
121 | request.setRequestBody(new Gson().toJson(Collections.singletonMap(SECRETS_FILE_PATH_PROPERTY, notAFile.getAbsolutePath())));
122 |
123 | GoPluginApiResponse response = secretsPlugin.handle(request);
124 |
125 | assertThat(response.responseCode()).isEqualTo(200);
126 |
127 | String expected = "[\n" +
128 | " {\n" +
129 | " \"key\": \"SecretsFilePath\",\n" +
130 | " \"message\": \"" + String.format("Secret config file path '%s' is not a normal file.", notAFile.getAbsolutePath()) + "\"\n" +
131 | " }\n" +
132 | "]";
133 |
134 | JSONAssert.assertEquals(expected, response.responseBody(), true);
135 | }
136 | }
137 |
138 | @Nested
139 | class LookupSecret {
140 |
141 | @Test
142 | void shouldReturnValidResponseOnlyIfAllKeysArePresent() throws UnhandledRequestTypeException, JSONException {
143 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("secrets", "1.0", LOOKUP_SECRET_REQUEST_NAME);
144 | LookupSecretRequest lookupSecretRequest = new LookupSecretRequest(databaseFile.getAbsolutePath(), Arrays.asList("secret-key", "username", "password"));
145 |
146 | request.setRequestBody(lookupSecretRequest.toJSON());
147 | GoPluginApiResponse response = secretsPlugin.handle(request);
148 |
149 | assertThat(response.responseCode()).isEqualTo(200);
150 | assertEquals("[\n" +
151 | " {\n" +
152 | " \"key\": \"secret-key\",\n" +
153 | " \"value\": \"secret-value\"\n" +
154 | " },\n" +
155 | " {\n" +
156 | " \"key\": \"username\",\n" +
157 | " \"value\": \"foo\"\n" +
158 | " },\n" +
159 | " {\n" +
160 | " \"key\": \"password\",\n" +
161 | " \"value\": \"bar\"\n" +
162 | " }\n" +
163 | "]", response.responseBody(), true);
164 | }
165 |
166 | @Test
167 | void shouldReturnNotFoundErrorResponseIfOneOrMoreSecretsWithGivenKeyAreNotPresent() throws UnhandledRequestTypeException, JSONException {
168 | DefaultGoPluginApiRequest request = new DefaultGoPluginApiRequest("secrets", "1.0", LOOKUP_SECRET_REQUEST_NAME);
169 |
170 | LookupSecretRequest lookupSecretRequest = new LookupSecretRequest(databaseFile.getAbsolutePath(), Arrays.asList("non-exiting-key"));
171 |
172 | request.setRequestBody(lookupSecretRequest.toJSON());
173 | GoPluginApiResponse response = secretsPlugin.handle(request);
174 |
175 | assertThat(response.responseCode()).isEqualTo(404);
176 | assertEquals("{\"message\":\"Secrets with keys [non-exiting-key] not found.\"}", response.responseBody(), true);
177 | }
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
--------------------------------------------------------------------------------
/cli/src/test/java/cd/go/plugin/secret/filebased/cli/MainTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2019 ThoughtWorks, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package cd.go.plugin.secret.filebased.cli;
18 |
19 | import cd.go.plugin.secret.filebased.db.SecretsDatabase;
20 | import cd.go.plugin.secret.filebased.db.Util;
21 | import org.junit.jupiter.api.Nested;
22 | import org.junit.jupiter.api.Test;
23 | import org.junit.jupiter.api.io.TempDir;
24 | import org.mockito.Mock;
25 |
26 | import java.io.File;
27 | import java.nio.file.Path;
28 | import java.util.UUID;
29 | import java.util.function.Consumer;
30 |
31 | import static org.assertj.core.api.Assertions.assertThat;
32 | import static org.mockito.Mockito.*;
33 |
34 |
35 | class MainTest {
36 |
37 | @Mock
38 | Consumer dummyExitter = mock(Consumer.class);
39 |
40 | @Nested
41 | class WithoutArguments {
42 | @Test
43 | void shouldPrintUsageAndExitWithBadStatusWhenNoOptionIsProvided() throws Exception {
44 | Util.withCapturedSysOut((out, err) -> {
45 | new Main().run(dummyExitter);
46 | assertThat(err.toString()).startsWith("Usage: java -jar [options] [command] [command options]");
47 | assertThat(out.toString()).isEmpty();
48 | verify(dummyExitter).accept(1);
49 | });
50 | }
51 |
52 | @Test
53 | void shouldPrintUsageAndExitWithBadStatusWhenHelpOptionIsProvided() throws Exception {
54 | Util.withCapturedSysOut((out, err) -> {
55 | new Main("-h").run(dummyExitter);
56 | assertThat(err.toString()).startsWith("Usage: java -jar [options] [command] [command options]");
57 | assertThat(out.toString()).isEmpty();
58 | verify(dummyExitter).accept(1);
59 | });
60 | }
61 | }
62 |
63 | @Nested
64 | class Initialize {
65 | @Test
66 | void shouldInitializeSecretsDatabaseAndExitWithZero(@TempDir Path tempDirectory) throws Exception {
67 | Util.withCapturedSysOut((out, err) -> {
68 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
69 |
70 | assertThat(databaseFile).doesNotExist();
71 |
72 | new Main("init", "-f", databaseFile.getAbsolutePath()).run(dummyExitter);
73 |
74 | assertThat(databaseFile).exists();
75 | assertThat(err.toString()).isEqualToIgnoringNewLines("Initialized secret database file in " + databaseFile.getAbsolutePath());
76 | assertThat(out.toString()).isEmpty();
77 | verifyNoMoreInteractions(dummyExitter);
78 | });
79 | }
80 | }
81 |
82 | @Nested
83 | class AddSecret {
84 | @Test
85 | void shouldBlowUpIfSecretsFileDoesNotExist(@TempDir Path tempDirectory) throws Exception {
86 | Util.withCapturedSysOut((out, err) -> {
87 |
88 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
89 |
90 | assertThat(databaseFile).doesNotExist();
91 |
92 | new Main("add", "-f", databaseFile.getAbsolutePath(), "-n", "password", "-v", "p@ssw0rd").run(dummyExitter);
93 | assertThat(out.toString()).isEmpty();
94 | assertThat(err.toString()).contains("FileNotFoundException");
95 | assertThat(err.toString()).contains(databaseFile.getAbsolutePath());
96 | verify(dummyExitter).accept(-1);
97 | });
98 | }
99 |
100 | @Test
101 | void shouldUpdateExistingSecretIfItAlreadyExists(@TempDir Path tempDirectory) throws Exception {
102 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
103 | new SecretsDatabase().addSecret("ssh-key", "some-ssh-key").saveTo(databaseFile);
104 |
105 | Util.withCapturedSysOut((out, err) -> {
106 | new Main("add", "-f", databaseFile.getAbsolutePath(), "-n", "ssh-key", "-v", "new-ssh-key").run(dummyExitter);
107 | assertThat(out.toString()).isEmpty();
108 | assertThat(err.toString()).isEqualToIgnoringNewLines("Added secret named ssh-key.");
109 | verifyNoMoreInteractions(dummyExitter);
110 | });
111 |
112 | assertThat(SecretsDatabase.readFrom(databaseFile).getSecret("ssh-key")).isEqualTo("new-ssh-key");
113 | }
114 |
115 | @Test
116 | void shouldAddANewSecretIfItDoesNotExists(@TempDir Path tempDirectory) throws Exception {
117 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
118 | new SecretsDatabase().addSecret("ssh-key", "some-ssh-key").saveTo(databaseFile);
119 |
120 | Util.withCapturedSysOut((out, err) -> {
121 | new Main("add", "-f", databaseFile.getAbsolutePath(), "-n", "new-ssh-key", "-v", "foobar-key").run(dummyExitter);
122 | assertThat(out.toString()).isEmpty();
123 | assertThat(err.toString()).isEqualToIgnoringNewLines("Added secret named new-ssh-key.");
124 | verifyNoMoreInteractions(dummyExitter);
125 | });
126 |
127 | assertThat(SecretsDatabase.readFrom(databaseFile).getSecret("ssh-key")).isEqualTo("some-ssh-key");
128 | assertThat(SecretsDatabase.readFrom(databaseFile).getSecret("new-ssh-key")).isEqualTo("foobar-key");
129 | }
130 | }
131 |
132 | @Nested
133 | class LookupSecret {
134 | @Test
135 | void shouldLookupSecret(@TempDir Path tempDirectory) throws Exception {
136 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
137 | new SecretsDatabase()
138 | .addSecret("username", "foo")
139 | .addSecret("password", "bar")
140 | .saveTo(databaseFile);
141 |
142 | Util.withCapturedSysOut((out, err) -> {
143 | new Main("show", "-f", databaseFile.getAbsolutePath(), "-n", "username").run(dummyExitter);
144 | assertThat(out.toString()).isEqualToIgnoringNewLines("foo");
145 | assertThat(err.toString()).isEmpty();
146 | verifyNoMoreInteractions(dummyExitter);
147 | });
148 | }
149 |
150 | @Test
151 | void shouldPrintNotFoundMessageWhenSecretIsNotPresent(@TempDir Path tempDirectory) throws Exception {
152 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
153 | new SecretsDatabase()
154 | .saveTo(databaseFile);
155 |
156 | Util.withCapturedSysOut((out, err) -> {
157 | new Main("show", "-f", databaseFile.getAbsolutePath(), "-n", "deploy-key").run(dummyExitter);
158 | assertThat(out.toString()).isEmpty();
159 | assertThat(err.toString()).isEqualToIgnoringNewLines("Secret named deploy-key was not found.");
160 | verify(dummyExitter).accept(-1);
161 | });
162 | }
163 |
164 | @Test
165 | void shouldPrintErrorWhenSecretsFileDoesNotExists(@TempDir Path tempDirectory) throws Exception {
166 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
167 |
168 | Util.withCapturedSysOut((out, err) -> {
169 | new Main("show", "-f", databaseFile.getAbsolutePath(), "-n", "deploy-key").run(dummyExitter);
170 | assertThat(out.toString()).isEmpty();
171 | assertThat(err.toString())
172 | .contains("FileNotFoundException")
173 | .contains(databaseFile.getAbsolutePath());
174 | verify(dummyExitter).accept(-1);
175 | });
176 | }
177 | }
178 |
179 |
180 | @Nested
181 | class LookupAllKeys {
182 | @Test
183 | void shouldLookupSecretKeys(@TempDir Path tempDirectory) throws Exception {
184 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
185 | new SecretsDatabase()
186 | .addSecret("username", "foo")
187 | .addSecret("password", "bar")
188 | .saveTo(databaseFile);
189 |
190 | Util.withCapturedSysOut((out, err) -> {
191 | new Main("keys", "-f", databaseFile.getAbsolutePath()).run(dummyExitter);
192 | assertThat(out.toString()).isEqualToIgnoringNewLines("[username, password]");
193 | verifyNoMoreInteractions(dummyExitter);
194 | });
195 | }
196 |
197 | @Test
198 | void shouldPrintNoKeysMessageWhenSecretsAreNotPresent(@TempDir Path tempDirectory) throws Exception {
199 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
200 | new SecretsDatabase()
201 | .saveTo(databaseFile);
202 |
203 | Util.withCapturedSysOut((out, err) -> {
204 | new Main("keys", "-f", databaseFile.getAbsolutePath()).run(dummyExitter);
205 | assertThat(out.toString()).isEmpty();
206 | assertThat(err.toString()).isEqualToIgnoringNewLines("There are no secrets in the secrets database file.");
207 | verify(dummyExitter).accept(-1);
208 | });
209 | }
210 | }
211 |
212 | @Nested
213 | class DeleteSecret {
214 | @Test
215 | void shouldDeleteSecret(@TempDir Path tempDirectory) throws Exception {
216 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
217 | new SecretsDatabase()
218 | .addSecret("ssh-key-1", "some-ssh-key-1")
219 | .addSecret("ssh-key-2", "some-ssh-key-2")
220 | .saveTo(databaseFile);
221 |
222 | Util.withCapturedSysOut((out, err) -> {
223 | new Main("remove", "-f", databaseFile.getAbsolutePath(), "-n", "ssh-key-2").run(dummyExitter);
224 | assertThat(out.toString()).isEmpty();
225 | assertThat(err.toString()).isEqualToIgnoringNewLines("Removed secret named ssh-key-2.");
226 | verifyNoMoreInteractions(dummyExitter);
227 | });
228 |
229 | assertThat(SecretsDatabase.readFrom(databaseFile).getSecret("ssh-key-1")).isEqualTo("some-ssh-key-1");
230 | assertThat(SecretsDatabase.readFrom(databaseFile).getSecret("ssh-key-2")).isNull();
231 | }
232 |
233 | @Test
234 | void shouldPrintErrorSecretWithGivenKeyDoesNotExist(@TempDir Path tempDirectory) throws Exception {
235 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
236 | new SecretsDatabase()
237 | .saveTo(databaseFile);
238 |
239 | Util.withCapturedSysOut((out, err) -> {
240 | new Main("remove", "-f", databaseFile.getAbsolutePath(), "-n", "foobar").run(dummyExitter);
241 | assertThat(out.toString()).isEmpty();
242 | assertThat(err.toString()).isEqualToIgnoringNewLines("Secret named foobar was not found.");
243 | verifyNoMoreInteractions(dummyExitter);
244 | });
245 | }
246 |
247 | @Test
248 | void shouldPrintErrorWhenSecretsFileDoesNotExists(@TempDir Path tempDirectory) throws Exception {
249 | File databaseFile = new File(tempDirectory.toFile(), UUID.randomUUID().toString().substring(0, 8));
250 |
251 | Util.withCapturedSysOut((out, err) -> {
252 | new Main("remove", "-f", databaseFile.getAbsolutePath(), "-n", "deploy-key").run(dummyExitter);
253 | assertThat(out.toString()).isEmpty();
254 | assertThat(err.toString())
255 | .contains("FileNotFoundException")
256 | .contains(databaseFile.getAbsolutePath());
257 | verify(dummyExitter).accept(-1);
258 | });
259 | }
260 | }
261 | }
262 |
--------------------------------------------------------------------------------