├── .idea ├── copyright │ ├── profiles_settings.xml │ └── Apache_2_0.xml ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml └── codeStyleSettings.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gocd-file-based-secrets-plugin ├── src │ ├── main │ │ ├── resource-templates │ │ │ ├── plugin.properties │ │ │ └── plugin.xml │ │ ├── resources │ │ │ ├── plugin-icon.svg │ │ │ └── secrets.template.html │ │ └── java │ │ │ └── cd │ │ │ └── go │ │ │ └── plugin │ │ │ └── secret │ │ │ └── filebased │ │ │ ├── util │ │ │ ├── LRUCache.java │ │ │ └── FileStat.java │ │ │ ├── model │ │ │ ├── SecretsConfiguration.java │ │ │ ├── LookupSecretRequest.java │ │ │ └── Metadata.java │ │ │ ├── executors │ │ │ ├── CacheEntry.java │ │ │ └── LookupSecretsRequestExecutor.java │ │ │ ├── SecretFilePathValidator.java │ │ │ └── FileBasedSecretsPlugin.java │ ├── test │ │ └── java │ │ │ └── cd │ │ │ └── go │ │ │ └── plugin │ │ │ └── secret │ │ │ └── filebased │ │ │ ├── util │ │ │ └── FileStatTest.java │ │ │ ├── executors │ │ │ └── LookupSecretsRequestExecutorTest.java │ │ │ └── FileBasedSecretsPluginTest.java │ └── jmh │ │ └── java │ │ └── cd │ │ └── go │ │ └── plugin │ │ └── secret │ │ └── filebased │ │ └── executors │ │ └── SecretsDatabasePerformanceBenchmark.java └── build.gradle ├── cli ├── src │ ├── main │ │ └── java │ │ │ └── cd │ │ │ └── go │ │ │ └── plugin │ │ │ └── secret │ │ │ └── filebased │ │ │ └── cli │ │ │ ├── args │ │ │ ├── RootArgs.java │ │ │ ├── HelpArgs.java │ │ │ ├── HasNameArgs.java │ │ │ ├── DatabaseFileArgs.java │ │ │ ├── InitArgs.java │ │ │ ├── AddSecretArgs.java │ │ │ ├── ShowSecretArgs.java │ │ │ ├── ShowAllSecretKeysArgs.java │ │ │ └── RemoveSecretArgs.java │ │ │ └── Main.java │ └── test │ │ └── java │ │ └── cd │ │ └── go │ │ └── plugin │ │ └── secret │ │ └── filebased │ │ └── cli │ │ └── MainTest.java └── build.gradle ├── settings.gradle ├── db ├── src │ ├── main │ │ └── java │ │ │ └── cd │ │ │ └── go │ │ │ └── plugin │ │ │ └── secret │ │ │ └── filebased │ │ │ └── db │ │ │ ├── ThrowingBiConsumer.java │ │ │ ├── BadSecretException.java │ │ │ ├── Util.java │ │ │ ├── Cipher.java │ │ │ └── SecretsDatabase.java │ └── test │ │ └── java │ │ └── cd │ │ └── go │ │ └── plugin │ │ └── secret │ │ └── filebased │ │ └── db │ │ ├── CipherTest.java │ │ └── SecretsDatabaseTest.java └── build.gradle ├── .gitignore ├── jar-class-loader └── src │ └── main │ └── java │ └── com │ └── thoughtworks │ └── gocd │ ├── onejar │ └── Handler.java │ └── Boot.java ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dAnjou/gocd-file-based-secrets-plugin/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gocd-file-based-secrets-plugin/src/main/resource-templates/plugin.properties: -------------------------------------------------------------------------------- 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 | pluginId=${id} 18 | -------------------------------------------------------------------------------- /gocd-file-based-secrets-plugin/src/main/resources/plugin-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /cli/src/main/java/cd/go/plugin/secret/filebased/cli/args/RootArgs.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 | public class RootArgs extends HelpArgs { 20 | } 21 | -------------------------------------------------------------------------------- /settings.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 | rootProject.name = 'gocd-file-based-secrets-plugin' 18 | 19 | include ':db' 20 | include ':cli' 21 | include ':jar-class-loader' 22 | include ':gocd-file-based-secrets-plugin' 23 | -------------------------------------------------------------------------------- /.idea/copyright/Apache_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /db/src/main/java/cd/go/plugin/secret/filebased/db/ThrowingBiConsumer.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 java.io.ByteArrayOutputStream; 20 | 21 | public interface ThrowingBiConsumer { 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 | 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 | 9 | 103 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 98 | 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 | --------------------------------------------------------------------------------