From 923f62d1227360c6e1e04ab370361061102971ae Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Tue, 22 Mar 2022 15:15:53 +0100 Subject: [PATCH 01/11] Refactored secret lookup and added option for oidc --- build.gradle | 2 +- .../vault/SecretConfigLookupExecutor.java | 22 ++++---- .../gocd/secretmanager/vault/VaultPlugin.java | 7 +-- .../vault/builders/SecretEngineBuilder.java | 52 +++++++++++++++++++ .../vault/models/SecretConfig.java | 25 ++++++++- .../vault/secretengines/KVSecretEngine.java | 47 +++++++++++++++++ .../OIDCPipelineIdentityProvider.java | 34 ++++++++++++ .../vault/secretengines/SecretEngine.java | 37 +++++++++++++ .../validation/SecretEngineValidator.java | 41 +++++++++++++++ src/main/resources/secrets.template.html | 17 ++++++ .../vault/SecretConfigLookupExecutorTest.java | 27 ++++++---- .../resources/secret-config-metadata.json | 10 +++- 12 files changed, 292 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/SecretEngineValidator.java diff --git a/build.gradle b/build.gradle index a2769c9..2791d56 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ apply plugin: 'java' gocdPlugin { id = 'com.thoughtworks.gocd.secretmanager.vault' - pluginVersion = '1.2.0' + pluginVersion = '1.2.1-SNAPSHOT' goCdVersion = '20.9.0' name = 'Vault secret manager plugin' description = 'The plugin allows to use hashicorp vault as secret manager for the GoCD server' diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java index b31695a..08a525f 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java @@ -24,8 +24,8 @@ import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; import com.thoughtworks.gocd.secretmanager.vault.models.Secrets; import com.thoughtworks.gocd.secretmanager.vault.request.SecretConfigRequest; - -import java.util.Map; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.SecretEngine; +import com.thoughtworks.gocd.secretmanager.vault.builders.SecretEngineBuilder; import static cd.go.plugin.base.GsonTransformer.fromJson; import static cd.go.plugin.base.GsonTransformer.toJson; @@ -46,17 +46,14 @@ public SecretConfigLookupExecutor() { @Override protected GoPluginApiResponse execute(SecretConfigRequest request) { try { - final Secrets secrets = new Secrets(); final Vault vault = vaultProvider.vaultFor(request.getConfiguration()); + final Secrets secrets = new Secrets(); + final String vaultPath = request.getConfiguration().getVaultPath(); - final Map secretsFromVault = vault.logical() - .read(request.getConfiguration().getVaultPath()) - .getData(); + SecretEngine secretEngine = buildSecretEngine(request, vault); for (String key : request.getKeys()) { - if (secretsFromVault.containsKey(key)) { - secrets.add(key, secretsFromVault.get(key)); - } + secretEngine.getSecret(vaultPath, key).ifPresent(secret -> secrets.add(key, secret)); } return DefaultGoPluginApiResponse.success(toJson(secrets)); @@ -66,6 +63,13 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) { } } + protected SecretEngine buildSecretEngine(SecretConfigRequest request, Vault vault) { + return new SecretEngineBuilder() + .secretConfig(request.getConfiguration()) + .vault(vault) + .build(); + } + @Override protected SecretConfigRequest parseRequest(String body) { return fromJson(body, SecretConfigRequest.class); diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java index ccbce6b..f503fa4 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java @@ -29,10 +29,7 @@ import com.thoughtworks.go.plugin.api.request.GoPluginApiRequest; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; -import com.thoughtworks.gocd.secretmanager.vault.validation.AppRoleAuthMethodValidator; -import com.thoughtworks.gocd.secretmanager.vault.validation.AuthMethodValidator; -import com.thoughtworks.gocd.secretmanager.vault.validation.CertAuthMethodValidator; -import com.thoughtworks.gocd.secretmanager.vault.validation.TokenAuthMethodValidator; +import com.thoughtworks.gocd.secretmanager.vault.validation.*; import static java.util.Collections.singletonList; @@ -49,7 +46,7 @@ public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationA .configMetadata(SecretConfig.class) .configView("/secrets.template.html") .validateSecretConfig(new AuthMethodValidator(), new CertAuthMethodValidator(), - new AppRoleAuthMethodValidator(), new TokenAuthMethodValidator()) + new AppRoleAuthMethodValidator(), new TokenAuthMethodValidator(), new SecretEngineValidator()) .lookup(new SecretConfigLookupExecutor()) .build(); } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java new file mode 100644 index 0000000..a815740 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.builders; + +import com.bettercloud.vault.Vault; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.KVSecretEngine; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.OIDCPipelineIdentityProvider; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.SecretEngine; + +public class SecretEngineBuilder { + + private Vault vault; + private String secretEngineIdentifier; + + + public SecretEngineBuilder secretConfig(SecretConfig secretConfig) { + this.secretEngineIdentifier = secretConfig.getSecretEngine(); + return this; + } + + public SecretEngineBuilder vault(Vault vault) { + this.vault = vault; + return this; + } + + public SecretEngine build() { + switch (secretEngineIdentifier) { + case SecretConfig.OIDC_ENGINE: + return new OIDCPipelineIdentityProvider(vault); + case SecretConfig.SECRET_ENGINE: + default: + return new KVSecretEngine(vault); + } + } + + +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java index 477c6ae..87c493c 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java @@ -38,18 +38,27 @@ public class SecretConfig { public static final String TOKEN_AUTH_METHOD = "token"; public static final String APPROLE_AUTH_METHOD = "approle"; public static final String CERT_AUTH_METHOD = "cert"; + public static final String SECRET_ENGINE = "secret"; + public static final String OIDC_ENGINE = "oidc"; public static final List SUPPORTED_AUTH_METHODS = asList(TOKEN_AUTH_METHOD, APPROLE_AUTH_METHOD, CERT_AUTH_METHOD); + public static final List SUPPORTED_SECRET_ENGINES = asList(SECRET_ENGINE, OIDC_ENGINE); public static final int DEFAULT_CONNECTION_TIMEOUT = 5; public static final int DEFAULT_READ_TIMEOUT = 30; public static final int DEFAULT_MAX_RETRIES = 0; public static final int DEFAULT_RETRY_INTERVAL_MS = 100; + public static final String DEFAULT_SECRET_ENGINE = SECRET_ENGINE; @Expose @SerializedName("VaultUrl") @Property(name = "VaultUrl", required = true) private String vaultUrl; + @Expose + @SerializedName("SecretEngine") + @Property(name = "SecretEngine") + private String secretEngine; + @Expose @SerializedName("VaultPath") @Property(name = "VaultPath", required = true) @@ -183,10 +192,21 @@ public String getServerPem() { return serverPem; } + public String getSecretEngine() { + if (isBlank(secretEngine)) { + return DEFAULT_SECRET_ENGINE; + } + return secretEngine; + } + public boolean isAuthMethodSupported() { return SUPPORTED_AUTH_METHODS.contains(authMethod.toLowerCase()); } + public boolean isSecretEngineSupported() { + return SUPPORTED_SECRET_ENGINES.contains(getSecretEngine().toLowerCase()); + } + public static SecretConfig fromJSON(Map request) { String json = GsonTransformer.toJson(request); return GSON.fromJson(json, SecretConfig.class); @@ -210,12 +230,13 @@ public boolean equals(Object o) { Objects.equals(secretId, that.secretId) && Objects.equals(clientKeyPem, that.clientKeyPem) && Objects.equals(clientPem, that.clientPem) && - Objects.equals(serverPem, that.serverPem); + Objects.equals(serverPem, that.serverPem) && + Objects.equals(secretEngine, that.secretEngine); } @Override public int hashCode() { - return Objects.hash(vaultUrl, vaultPath, nameSpace, connectionTimeout, readTimeout, maxRetries, retryIntervalMilliseconds, authMethod, token, roleId, secretId, clientKeyPem, clientPem, serverPem); + return Objects.hash(vaultUrl, vaultPath, nameSpace, connectionTimeout, readTimeout, maxRetries, retryIntervalMilliseconds, authMethod, token, roleId, secretId, clientKeyPem, clientPem, serverPem, secretEngine); } public boolean isTokenAuthentication() { diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java new file mode 100644 index 0000000..b35726e --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.secretengines; + +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultException; + +import java.util.Map; +import java.util.Optional; + +public class KVSecretEngine extends SecretEngine { + + private Map secretsFromVault; + + public KVSecretEngine(Vault vault) { + super(vault); + } + + @Override + public Optional getSecret(String path, String key) throws VaultException { + if (secretsFromVault == null) { + secretsFromVault = getSecretData(path); + } + + return Optional.ofNullable(secretsFromVault.get(key)); + } + + private Map getSecretData(String path) throws VaultException { + return getVault().logical() + .read(path) + .getData(); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java new file mode 100644 index 0000000..36124d5 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.secretengines; + +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultException; + +import java.util.Optional; + +public class OIDCPipelineIdentityProvider extends SecretEngine { + + public OIDCPipelineIdentityProvider(Vault vault) { + super(vault); + } + + @Override + public Optional getSecret(String path, String key) throws VaultException { + return Optional.ofNullable(getVault().mounts().read(path + '/' + key).getData().get("token")); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java new file mode 100644 index 0000000..916fcaf --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.secretengines; + +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultException; + +import java.util.Optional; + +public abstract class SecretEngine { + + private final Vault vault; + + public SecretEngine(Vault vault) { + this.vault = vault; + } + + public abstract Optional getSecret(String path, String key) throws VaultException; + + public Vault getVault() { + return vault; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/SecretEngineValidator.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/SecretEngineValidator.java new file mode 100644 index 0000000..32c2a29 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/SecretEngineValidator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.validation; + +import cd.go.plugin.base.validation.ValidationResult; +import cd.go.plugin.base.validation.Validator; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; + +import java.util.Map; + +import static java.lang.String.format; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import static org.apache.commons.lang3.StringUtils.join; + +public class SecretEngineValidator implements Validator { + @Override + public ValidationResult validate(Map requestBody) { + SecretConfig secretConfig = SecretConfig.fromJSON(requestBody); + ValidationResult validationResult = new ValidationResult(); + + if (isNotEmpty(secretConfig.getSecretEngine()) && !secretConfig.isSecretEngineSupported()) { + validationResult.add("SecretEngine", format("Invalid 'SecretEngine`, should be one of [%s]", join(SecretConfig.SUPPORTED_SECRET_ENGINES, ","))); + } + + return validationResult; + } +} \ No newline at end of file diff --git a/src/main/resources/secrets.template.html b/src/main/resources/secrets.template.html index 364842b..125e558 100755 --- a/src/main/resources/secrets.template.html +++ b/src/main/resources/secrets.template.html @@ -143,6 +143,23 @@ ng-show="GOINPUTNAME[VaultUrl].$error.server">{{ GOINPUTNAME[VaultUrl].$error.server }} +
+
+
+ + + {{GOINPUTNAME[SecretEngine].$error.server}} +

+ This defines the secret engine type that is mounted to a specific path. +

+
+
+
+
diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java index af85c73..075c34b 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java @@ -23,19 +23,21 @@ import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; import com.thoughtworks.gocd.secretmanager.vault.request.SecretConfigRequest; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.KVSecretEngine; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoSettings; import java.util.Arrays; import java.util.HashMap; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import static org.skyscreamer.jsonassert.JSONAssert.assertEquals; @MockitoSettings @@ -47,28 +49,31 @@ class SecretConfigLookupExecutorTest { @Mock private Logical logical; + private SecretConfigLookupExecutor secretConfigLookupExecutor; + @BeforeEach void setUp() throws VaultException { when(vaultProvider.vaultFor(any())).thenReturn(vault); - when(vault.logical()).thenReturn(logical); + + secretConfigLookupExecutor = spy(new SecretConfigLookupExecutor(vaultProvider)); } @Test void shouldReturnLookupResponse() throws VaultException, JSONException { - final LogicalResponse logicalResponse = mock(LogicalResponse.class); final SecretConfigRequest request = mock(SecretConfigRequest.class); final SecretConfig secretConfig = mock(SecretConfig.class); - when(logical.read("/secret/gocd")).thenReturn(logicalResponse); - when(logicalResponse.getData()).thenReturn(new HashMap() {{ - put("AWS_ACCESS_KEY", "ASKDMDASDKLASDI"); - put("AWS_SECRET_KEY", "slfjskldfjsdjflfsdfsffdadsdfsdfsdfsd;"); - }}); + final KVSecretEngine kvSecretEngine = mock(KVSecretEngine.class); + when(request.getConfiguration()).thenReturn(secretConfig); + doReturn(kvSecretEngine).when(secretConfigLookupExecutor).buildSecretEngine(request, vault); + + when(kvSecretEngine.getSecret(anyString(), eq("AWS_ACCESS_KEY"))).thenReturn(Optional.of("ASKDMDASDKLASDI")); + when(kvSecretEngine.getSecret(anyString(), eq("AWS_SECRET_KEY"))).thenReturn(Optional.of("slfjskldfjsdjflfsdfsffdadsdfsdfsdfsd;")); + when(secretConfig.getVaultPath()).thenReturn("/secret/gocd"); when(request.getKeys()).thenReturn(Arrays.asList("AWS_ACCESS_KEY", "AWS_SECRET_KEY")); - final GoPluginApiResponse response = new SecretConfigLookupExecutor(vaultProvider) - .execute(request); + final GoPluginApiResponse response = secretConfigLookupExecutor.execute(request); assertThat(response.responseCode()).isEqualTo(200); final String expectedResponse = "[\n" + diff --git a/src/test/resources/secret-config-metadata.json b/src/test/resources/secret-config-metadata.json index 3933113..2ed7762 100644 --- a/src/test/resources/secret-config-metadata.json +++ b/src/test/resources/secret-config-metadata.json @@ -6,6 +6,14 @@ "required": true, "secure": false } + }, + { + "key": "SecretEngine", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } }, { "key": "VaultPath", @@ -14,7 +22,7 @@ "required": true, "secure": false } -}, + }, { "key": "NameSpace", "metadata": { From f99fce8c48c16969ec12f986ac2a408b11e03b96 Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Tue, 22 Mar 2022 17:40:14 +0100 Subject: [PATCH 02/11] Added OIDC logic to create entity alias for pipelines --- .../vault/SecretConfigLookupExecutor.java | 7 +- .../secretmanager/vault/VaultProvider.java | 27 +++-- .../vault/builders/SecretEngineBuilder.java | 15 ++- .../vault/request/CustomMetadataRequest.java | 55 +++++++++++ .../vault/request/DataResponse.java | 34 +++++++ .../vault/request/EntityAliasRequest.java | 49 +++++++++ .../vault/request/OICDTokenResponse.java | 51 ++++++++++ .../OIDCPipelineIdentityProvider.java | 99 ++++++++++++++++++- .../vault/SecretConfigLookupExecutorTest.java | 7 +- 9 files changed, 322 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java index 08a525f..b763d21 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java @@ -19,9 +19,11 @@ import cd.go.plugin.base.executors.secrets.LookupExecutor; import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; import com.thoughtworks.go.plugin.api.logging.Logger; import com.thoughtworks.go.plugin.api.response.DefaultGoPluginApiResponse; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; import com.thoughtworks.gocd.secretmanager.vault.models.Secrets; import com.thoughtworks.gocd.secretmanager.vault.request.SecretConfigRequest; import com.thoughtworks.gocd.secretmanager.vault.secretengines.SecretEngine; @@ -50,7 +52,7 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) { final Secrets secrets = new Secrets(); final String vaultPath = request.getConfiguration().getVaultPath(); - SecretEngine secretEngine = buildSecretEngine(request, vault); + SecretEngine secretEngine = buildSecretEngine(request, vault, vaultProvider.getVaultConfig(), request.getConfiguration()); for (String key : request.getKeys()) { secretEngine.getSecret(vaultPath, key).ifPresent(secret -> secrets.add(key, secret)); @@ -63,10 +65,11 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) { } } - protected SecretEngine buildSecretEngine(SecretConfigRequest request, Vault vault) { + protected SecretEngine buildSecretEngine(SecretConfigRequest request, Vault vault, VaultConfig vaultConfig, SecretConfig secretConfig) { return new SecretEngineBuilder() .secretConfig(request.getConfiguration()) .vault(vault) + .vaultConfig(vaultConfig) .build(); } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultProvider.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultProvider.java index 5dd53d8..7a46cf7 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultProvider.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultProvider.java @@ -29,7 +29,9 @@ public class VaultProvider { private final VaultConfigBuilderFactory vaultConfigBuilderFactory; private final VaultAuthenticatorFactory vaultAuthenticatorFactory; -// Used only in tests + private VaultConfig vaultConfig; + + // Used only in tests VaultProvider(VaultConfigBuilderFactory vaultConfigBuilderFactory, VaultAuthenticatorFactory vaultAuthenticatorFactory) { this.vaultConfigBuilderFactory = vaultConfigBuilderFactory; this.vaultAuthenticatorFactory = vaultAuthenticatorFactory; @@ -40,17 +42,26 @@ public VaultProvider() { } public Vault vaultFor(SecretConfig secretConfig) throws VaultException { - VaultConfigBuilder configBuilder = vaultConfigBuilderFactory.builderFor(secretConfig); - VaultConfig vaultConfig = configBuilder.configFrom(secretConfig); - - VaultAuthenticator vaultAuthenticator = vaultAuthenticatorFactory.authenticatorFor(secretConfig); + vaultConfig = vaultConfig(secretConfig); Vault vault = new Vault(vaultConfig) .withRetries(secretConfig.getMaxRetries(), secretConfig.getRetryIntervalMilliseconds()); + String token = authenticate(vault, secretConfig); + vaultConfig.token(token); + return vault; + } - String token = vaultAuthenticator.authenticate(vault, secretConfig); + private VaultConfig vaultConfig(SecretConfig secretConfig) throws VaultException { + VaultConfigBuilder configBuilder = vaultConfigBuilderFactory.builderFor(secretConfig); + VaultConfig vaultConfig = configBuilder.configFrom(secretConfig); + return vaultConfig; + } - vaultConfig.token(token); + private String authenticate(Vault vault, SecretConfig secretConfig) throws VaultException { + VaultAuthenticator vaultAuthenticator = vaultAuthenticatorFactory.authenticatorFor(secretConfig); + return vaultAuthenticator.authenticate(vault, secretConfig); + } - return vault; + public VaultConfig getVaultConfig() { + return vaultConfig; } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java index a815740..8c7ade1 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java @@ -17,6 +17,7 @@ package com.thoughtworks.gocd.secretmanager.vault.builders; import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; import com.thoughtworks.gocd.secretmanager.vault.secretengines.KVSecretEngine; import com.thoughtworks.gocd.secretmanager.vault.secretengines.OIDCPipelineIdentityProvider; @@ -25,11 +26,12 @@ public class SecretEngineBuilder { private Vault vault; - private String secretEngineIdentifier; + private VaultConfig vaultConfig; + private SecretConfig secretConfig; public SecretEngineBuilder secretConfig(SecretConfig secretConfig) { - this.secretEngineIdentifier = secretConfig.getSecretEngine(); + this.secretConfig = secretConfig; return this; } @@ -39,14 +41,17 @@ public SecretEngineBuilder vault(Vault vault) { } public SecretEngine build() { - switch (secretEngineIdentifier) { + switch (secretConfig.getSecretEngine()) { case SecretConfig.OIDC_ENGINE: - return new OIDCPipelineIdentityProvider(vault); + return new OIDCPipelineIdentityProvider(vault, vaultConfig, secretConfig); case SecretConfig.SECRET_ENGINE: default: return new KVSecretEngine(vault); } } - + public SecretEngineBuilder vaultConfig(VaultConfig vaultConfig) { + this.vaultConfig = vaultConfig; + return this; + } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java new file mode 100644 index 0000000..559e0ac --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class CustomMetadataRequest { + + @Expose + @SerializedName("group") + private String group; + + @Expose + @SerializedName("pipeline") + private String pipeline; + + @Expose + @SerializedName("repository") + private String repository; + + @Expose + @SerializedName("organization") + private String organization; + + @Expose + @SerializedName("branch") + private String branch; + + public CustomMetadataRequest(String group, String pipeline, String repository, String organization, String branch) { + this.group = group; + this.pipeline = pipeline; + this.repository = repository; + this.organization = organization; + this.branch = branch; + } + + public String getPipeline() { + return pipeline; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java new file mode 100644 index 0000000..efedd5d --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class DataResponse { + + @Expose + @SerializedName("data") + private OICDTokenResponse data; + + public DataResponse() { + } + + public OICDTokenResponse getData() { + return data; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java new file mode 100644 index 0000000..7322f0c --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; + +import java.util.List; + +public class EntityAliasRequest { + + @Expose + @SerializedName("name") + private String name; + + @Expose + @SerializedName("canonical_id") + private String canonicalId; + + @Expose + @SerializedName("mount_accessor") + private String mountAccessor; + + @Expose + @SerializedName("custom_metadata") + private CustomMetadataRequest customMetadata; + + public EntityAliasRequest(String name, String canonicalId, String mountAccessor, CustomMetadataRequest customMetadata) { + this.name = name; + this.canonicalId = canonicalId; + this.mountAccessor = mountAccessor; + this.customMetadata = customMetadata; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java new file mode 100644 index 0000000..6ada3d9 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class OICDTokenResponse { + + + @Expose + @SerializedName("client_id") + private String clientId; + + @Expose + @SerializedName("token") + private String token; + + @Expose + @SerializedName("ttl") + private long ttl; + + public OICDTokenResponse() { + } + + public String getClientId() { + return clientId; + } + + public String getToken() { + return token; + } + + public long getTtl() { + return ttl; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java index 36124d5..3125fb3 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java @@ -16,19 +16,112 @@ package com.thoughtworks.gocd.secretmanager.vault.secretengines; +import cd.go.plugin.base.GsonTransformer; import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.api.Auth; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestException; +import com.bettercloud.vault.rest.RestResponse; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.CustomMetadataRequest; +import com.thoughtworks.gocd.secretmanager.vault.request.DataResponse; +import com.thoughtworks.gocd.secretmanager.vault.request.EntityAliasRequest; +import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static cd.go.plugin.base.GsonTransformer.toJson; public class OIDCPipelineIdentityProvider extends SecretEngine { - public OIDCPipelineIdentityProvider(Vault vault) { + private VaultConfig vaultConfig; + private SecretConfig secretConfig; + + public OIDCPipelineIdentityProvider(Vault vault, VaultConfig vaultConfig, SecretConfig secretConfig) { super(vault); + this.vaultConfig = vaultConfig; + this.secretConfig = secretConfig; } @Override - public Optional getSecret(String path, String key) throws VaultException { - return Optional.ofNullable(getVault().mounts().read(path + '/' + key).getData().get("token")); + public Optional getSecret(String path, String pipelineName) throws VaultException { + CustomMetadataRequest customMetadataRequest = new CustomMetadataRequest( + "some_group", + pipelineName, + "some_repository", + "some_organization", + "some_branch" + ); + + createPipelineEntityAlias(customMetadataRequest); + + String pipelineAuthToken = assumePipeline(customMetadataRequest.getPipeline()); + return Optional.of(oidcToken(pipelineAuthToken, path)); + } + + protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataRequest) throws VaultException { + EntityAliasRequest entityAliasRequest = new EntityAliasRequest( + entityAliasName(customMetadataRequest.getPipeline()), + "7b399f73-1547-9300-4a28-6e0d536571c4", + "auth_token_40382420", + customMetadataRequest + ); + + try { + RestResponse restResponse = new Rest() + .url(vaultConfig.getAddress() + "/v1/identity/entity-alias") + .header("X-Vault-Token", vaultConfig.getToken()) + .body(toJson(entityAliasRequest).getBytes(StandardCharsets.UTF_8)) + .post(); + + if (restResponse.getStatus() < 200 || restResponse.getStatus() >= 300) { + String response = new String(restResponse.getBody(), StandardCharsets.UTF_8); + throw new VaultException("Could not create entity alias. Due to: " + response, restResponse.getStatus()); + } + + } catch (RestException e) { + throw new VaultException(e); + } + } + + protected String assumePipeline(String pipelineName) throws VaultException { + + // TODO: Fech these from secret COnfig + Auth.TokenRequest tokenRequest = new Auth.TokenRequest() + .entityAlias(entityAliasName(pipelineName)) + .role("gocd-pipeline-dev-test") + .polices(Stream.of("gocd-pipeline-policy-dev-test").collect(Collectors.toList())); + + String pipelineAuthToken = getVault().auth().createToken(tokenRequest).getAuthClientToken(); + return pipelineAuthToken; + } + + protected String oidcToken(String pipelineAuthToken, String path) throws VaultException { + + try { + RestResponse restResponse = new Rest() + .url(vaultConfig.getAddress() + path) + .header("X-Vault-Token", pipelineAuthToken) + .get(); + + String response = new String(restResponse.getBody(), StandardCharsets.UTF_8); + if (restResponse.getStatus() != 200) { + throw new VaultException("Could not read OIDC token. Due to: " + response, restResponse.getStatus()); + } + + DataResponse dataResponse = GsonTransformer.fromJson(response, DataResponse.class); + return dataResponse.getData().getToken(); + + } catch (RestException e) { + throw new VaultException(e); + } + } + + private String entityAliasName(String pipelineName) { + return String.format("gocd-pipeline-dev-test-%s", pipelineName.toLowerCase().replaceAll("\\s+", "-")); } } diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java index 075c34b..5cbbeba 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java @@ -17,9 +17,9 @@ package com.thoughtworks.gocd.secretmanager.vault; import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; import com.bettercloud.vault.api.Logical; -import com.bettercloud.vault.response.LogicalResponse; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; import com.thoughtworks.gocd.secretmanager.vault.request.SecretConfigRequest; @@ -28,11 +28,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoSettings; import java.util.Arrays; -import java.util.HashMap; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -63,9 +61,10 @@ void shouldReturnLookupResponse() throws VaultException, JSONException { final SecretConfigRequest request = mock(SecretConfigRequest.class); final SecretConfig secretConfig = mock(SecretConfig.class); final KVSecretEngine kvSecretEngine = mock(KVSecretEngine.class); + final VaultConfig vaultConfig = mock(VaultConfig.class); when(request.getConfiguration()).thenReturn(secretConfig); - doReturn(kvSecretEngine).when(secretConfigLookupExecutor).buildSecretEngine(request, vault); + doReturn(kvSecretEngine).when(secretConfigLookupExecutor).buildSecretEngine(request, vault, vaultConfig, secretConfig); when(kvSecretEngine.getSecret(anyString(), eq("AWS_ACCESS_KEY"))).thenReturn(Optional.of("ASKDMDASDKLASDI")); when(kvSecretEngine.getSecret(anyString(), eq("AWS_SECRET_KEY"))).thenReturn(Optional.of("slfjskldfjsdjflfsdfsffdadsdfsdfsdfsd;")); From 41b4f93aa45ffb378c000179152623542a6b74c3 Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Wed, 23 Mar 2022 16:34:35 +0100 Subject: [PATCH 03/11] Added logic to communicate to vault for OIDC tokens --- build.gradle | 2 + .../vault/SecretConfigLookupExecutor.java | 4 +- .../gocd/secretmanager/vault/VaultPlugin.java | 3 +- .../vault/http/DataResponseExtractor.java | 52 +++++ .../http/DefaultContentTypeInterceptor.java | 43 +++++ .../vault/http/OkHTTPClientFactory.java | 39 ++++ .../vault/http/RetryInterceptor.java | 56 ++++++ .../vault/http/VaultHeaderInterceptor.java | 46 +++++ .../vault/models/SecretConfig.java | 33 +++- .../vault/request/AuthMountsResponse.java | 34 ++++ .../vault/request/CreateTokenRequest.java | 43 +++++ .../vault/request/DataResponse.java | 6 +- .../vault/request/LookupResponse.java | 35 ++++ .../vault/request/TokenAuthMountResponse.java | 34 ++++ .../vault/request/TokenResponse.java | 47 +++++ .../OIDCPipelineIdentityProvider.java | 148 ++++++++++---- .../validation/OIDCSecretEngineValidator.java | 43 +++++ src/main/resources/secrets.template.html | 35 +++- .../vault/SecretConfigLookupExecutorTest.java | 3 +- .../vault/http/OkHTTPClientTest.java | 147 ++++++++++++++ .../OIDCPipelineIdentityProviderTest.java | 180 ++++++++++++++++++ .../resources/mocks/vault/auth-mounts.json | 15 ++ .../resources/mocks/vault/auth-token.json | 27 +++ .../resources/mocks/vault/lookup-self.json | 23 +++ .../resources/mocks/vault/oidc-token.json | 7 + .../resources/secret-config-metadata.json | 16 ++ .../secret-config-network-settings.json | 11 ++ src/test/resources/secret-config-oidc.json | 11 ++ 28 files changed, 1090 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/RetryInterceptor.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/VaultHeaderInterceptor.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/OIDCSecretEngineValidator.java create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java create mode 100644 src/test/resources/mocks/vault/auth-mounts.json create mode 100644 src/test/resources/mocks/vault/auth-token.json create mode 100644 src/test/resources/mocks/vault/lookup-self.json create mode 100644 src/test/resources/mocks/vault/oidc-token.json create mode 100644 src/test/resources/secret-config-network-settings.json create mode 100644 src/test/resources/secret-config-oidc.json diff --git a/build.gradle b/build.gradle index 2791d56..2442739 100644 --- a/build.gradle +++ b/build.gradle @@ -52,6 +52,7 @@ dependencies { implementation group: 'cd.go.plugin.base', name: 'gocd-plugin-base', version: '0.0.2' implementation group: 'com.bettercloud', name: 'vault-java-driver', version: '5.1.0' implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.9.3' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.8.2' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2' @@ -62,6 +63,7 @@ dependencies { testImplementation group: 'org.jsoup', name: 'jsoup', version: '1.14.3' testImplementation group: 'cd.go.plugin', name: 'go-plugin-api', version: '21.4.0' testImplementation group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.0' + testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '4.9.3' } test { diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java index b763d21..7351cb2 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java @@ -52,7 +52,7 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) { final Secrets secrets = new Secrets(); final String vaultPath = request.getConfiguration().getVaultPath(); - SecretEngine secretEngine = buildSecretEngine(request, vault, vaultProvider.getVaultConfig(), request.getConfiguration()); + SecretEngine secretEngine = buildSecretEngine(request, vault, vaultProvider.getVaultConfig()); for (String key : request.getKeys()) { secretEngine.getSecret(vaultPath, key).ifPresent(secret -> secrets.add(key, secret)); @@ -65,7 +65,7 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) { } } - protected SecretEngine buildSecretEngine(SecretConfigRequest request, Vault vault, VaultConfig vaultConfig, SecretConfig secretConfig) { + protected SecretEngine buildSecretEngine(SecretConfigRequest request, Vault vault, VaultConfig vaultConfig) { return new SecretEngineBuilder() .secretConfig(request.getConfiguration()) .vault(vault) diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java index f503fa4..9c86ed0 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/VaultPlugin.java @@ -46,7 +46,8 @@ public void initializeGoApplicationAccessor(GoApplicationAccessor goApplicationA .configMetadata(SecretConfig.class) .configView("/secrets.template.html") .validateSecretConfig(new AuthMethodValidator(), new CertAuthMethodValidator(), - new AppRoleAuthMethodValidator(), new TokenAuthMethodValidator(), new SecretEngineValidator()) + new AppRoleAuthMethodValidator(), new TokenAuthMethodValidator(), + new SecretEngineValidator(), new OIDCSecretEngineValidator()) .lookup(new SecretConfigLookupExecutor()) .build(); } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java new file mode 100644 index 0000000..4f6e422 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import cd.go.plugin.base.GsonTransformer; +import com.thoughtworks.gocd.secretmanager.vault.request.DataResponse; +import okhttp3.Response; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +public class DataResponseExtractor { + + public T extract(Response response, Class clazz) throws IOException { + DataResponse dataResponse = GsonTransformer.fromJson(response.body().string(), getType(DataResponse.class, clazz)); + return dataResponse.getData(); + } + + private Type getType(Class rawClass, Class parameter) { + return new ParameterizedType() { + @Override + public Type[] getActualTypeArguments() { + return new Type[] {parameter}; + } + + @Override + public Type getRawType() { + return rawClass; + } + + @Override + public Type getOwnerType() { + return null; + } + }; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java new file mode 100644 index 0000000..c439fe6 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +public class DefaultContentTypeInterceptor implements Interceptor { + + private final String contentType; + + public DefaultContentTypeInterceptor(String contentType) { + this.contentType = contentType; + } + + public Response intercept(Interceptor.Chain chain) throws IOException { + + Request originalRequest = chain.request(); + Request requestWithUserAgent = originalRequest + .newBuilder() + .header("Content-Type", OkHTTPClientFactory.JSON.toString()) + .build(); + + return chain.proceed(requestWithUserAgent); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java new file mode 100644 index 0000000..62f3627 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; + +import java.util.concurrent.TimeUnit; + +public class OkHTTPClientFactory { + + public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + public OkHttpClient buildFor(SecretConfig secretConfig) { + // TODO: Handle SSL Config + return new OkHttpClient.Builder() + .readTimeout(secretConfig.getReadTimeout(), TimeUnit.SECONDS) + .connectTimeout(secretConfig.getConnectionTimeout(), TimeUnit.SECONDS) + .addInterceptor(new DefaultContentTypeInterceptor(JSON.toString())) + .addInterceptor(new VaultHeaderInterceptor(secretConfig.getNameSpace())) + .addInterceptor(new RetryInterceptor(secretConfig.getRetryIntervalMilliseconds(), secretConfig.getMaxRetries())) + .build(); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/RetryInterceptor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/RetryInterceptor.java new file mode 100644 index 0000000..46cd4a3 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/RetryInterceptor.java @@ -0,0 +1,56 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import okhttp3.Interceptor; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +public class RetryInterceptor implements Interceptor { + private final int retryIntervalMilliseconds; + private final int maxRetries; + + public RetryInterceptor(int retryIntervalMilliseconds, int maxRetries) { + this.retryIntervalMilliseconds = retryIntervalMilliseconds; + this.maxRetries = maxRetries; + } + + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + int retryCount = 0; + while (true) { + Response response = chain.proceed(chain.request()); + if (response.code() >= 400) { + if (retryCount < maxRetries) { + retryCount++; + try { + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + return response; + } + } else { + return response; + } + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/VaultHeaderInterceptor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/VaultHeaderInterceptor.java new file mode 100644 index 0000000..64091bd --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/VaultHeaderInterceptor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +public class VaultHeaderInterceptor implements Interceptor { + + private final String namespace; + + public VaultHeaderInterceptor(String namespace) { + this.namespace = namespace; + } + + @NotNull + @Override + public Response intercept(@NotNull Chain chain) throws IOException { + Request request = chain.request(); + if (namespace != null && !namespace.isEmpty()) { + request = request + .newBuilder() + .header("X-Vault-Namespace", namespace) + .build(); + } + return chain.proceed(request); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java index 87c493c..066564c 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java @@ -23,9 +23,7 @@ import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import static java.util.Arrays.asList; import static org.apache.commons.lang3.StringUtils.isBlank; @@ -59,6 +57,16 @@ public class SecretConfig { @Property(name = "SecretEngine") private String secretEngine; + @Expose + @SerializedName("PipelineTokenAuthBackendRole") + @Property(name = "PipelineTokenAuthBackendRole") + private String pipelineTokenAuthBackendRole; + + @Expose + @SerializedName("PipelinePolicy") + @Property(name = "PipelinePolicy") + private String pipelinePolicy; + @Expose @SerializedName("VaultPath") @Property(name = "VaultPath", required = true) @@ -199,6 +207,17 @@ public String getSecretEngine() { return secretEngine; } + public String getPipelineTokenAuthBackendRole() { + return pipelineTokenAuthBackendRole; + } + + public List getPipelinePolicy() { + if (isBlank(pipelinePolicy)) { + return new ArrayList<>(); + } + return Arrays.asList(pipelinePolicy.split(",\\s*")); + } + public boolean isAuthMethodSupported() { return SUPPORTED_AUTH_METHODS.contains(authMethod.toLowerCase()); } @@ -231,7 +250,9 @@ public boolean equals(Object o) { Objects.equals(clientKeyPem, that.clientKeyPem) && Objects.equals(clientPem, that.clientPem) && Objects.equals(serverPem, that.serverPem) && - Objects.equals(secretEngine, that.secretEngine); + Objects.equals(secretEngine, that.secretEngine) && + Objects.equals(pipelineTokenAuthBackendRole, that.pipelineTokenAuthBackendRole) && + Objects.equals(pipelinePolicy, that.pipelinePolicy); } @Override @@ -250,4 +271,8 @@ public boolean isAppRoleAuthentication() { public boolean isCertAuthentication() { return CERT_AUTH_METHOD.equalsIgnoreCase(authMethod); } + + public boolean isOIDCSecretEngine() { + return OIDC_ENGINE.equalsIgnoreCase(secretEngine); + } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java new file mode 100644 index 0000000..dc7178f --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class AuthMountsResponse { + + @Expose + @SerializedName("token/") + private TokenAuthMountResponse token; + + public AuthMountsResponse() { + } + + public TokenAuthMountResponse getToken() { + return token; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java new file mode 100644 index 0000000..9c88d27 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class CreateTokenRequest { + + @Expose + @SerializedName("role_name") + private String roleName; + + @Expose + @SerializedName("policies") + private List policies; + + @Expose + @SerializedName("entity_alias") + private String entityAlias; + + public CreateTokenRequest(String roleName, List policies, String entityAlias) { + this.roleName = roleName; + this.policies = policies; + this.entityAlias = entityAlias; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java index efedd5d..1db0fa8 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java @@ -19,16 +19,16 @@ import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; -public class DataResponse { +public class DataResponse { @Expose @SerializedName("data") - private OICDTokenResponse data; + private T data; public DataResponse() { } - public OICDTokenResponse getData() { + public T getData() { return data; } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java new file mode 100644 index 0000000..64c1838 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class LookupResponse { + + @Expose + @SerializedName("entity_id") + private String entityId; + + public LookupResponse() { + } + + public String getEntityId() { + return entityId; + } + +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java new file mode 100644 index 0000000..3dcd649 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class TokenAuthMountResponse { + + @Expose + @SerializedName("accessor") + private String accessor; + + public TokenAuthMountResponse() { + } + + public String getAccessor() { + return accessor; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java new file mode 100644 index 0000000..4884cba --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class TokenResponse { + + @Expose + @SerializedName("auth") + private AuthTokenResponse auth; + + public TokenResponse() { + } + + public AuthTokenResponse getAuth() { + return auth; + } + + public class AuthTokenResponse { + @Expose + @SerializedName("client_token") + private String clientToken; + + public AuthTokenResponse() { + } + + public String getClientToken() { + return clientToken; + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java index 3125fb3..9efbdf4 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java @@ -20,31 +20,37 @@ import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; -import com.bettercloud.vault.api.Auth; -import com.bettercloud.vault.rest.Rest; -import com.bettercloud.vault.rest.RestException; -import com.bettercloud.vault.rest.RestResponse; +import com.google.gson.reflect.TypeToken; +import com.thoughtworks.gocd.secretmanager.vault.http.DataResponseExtractor; +import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; -import com.thoughtworks.gocd.secretmanager.vault.request.CustomMetadataRequest; -import com.thoughtworks.gocd.secretmanager.vault.request.DataResponse; -import com.thoughtworks.gocd.secretmanager.vault.request.EntityAliasRequest; +import com.thoughtworks.gocd.secretmanager.vault.request.*; +import okhttp3.*; -import java.nio.charset.StandardCharsets; +import java.io.IOException; +import java.lang.reflect.Type; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; import static cd.go.plugin.base.GsonTransformer.toJson; +import static com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory.JSON; public class OIDCPipelineIdentityProvider extends SecretEngine { + private final OkHttpClient client; + private final DataResponseExtractor dataResponseExtractor; private VaultConfig vaultConfig; private SecretConfig secretConfig; + private static final String X_VAULT_TOKEN = "X-Vault-Token"; + public OIDCPipelineIdentityProvider(Vault vault, VaultConfig vaultConfig, SecretConfig secretConfig) { super(vault); this.vaultConfig = vaultConfig; this.secretConfig = secretConfig; + this.dataResponseExtractor = new DataResponseExtractor(); + + OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); + this.client = okHTTPClientFactory.buildFor(secretConfig); } @Override @@ -64,59 +70,121 @@ public Optional getSecret(String path, String pipelineName) throws Vault } protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataRequest) throws VaultException { + String entityId = getEntityId(); + String mountAccessor = getMountAccessor(); + EntityAliasRequest entityAliasRequest = new EntityAliasRequest( entityAliasName(customMetadataRequest.getPipeline()), - "7b399f73-1547-9300-4a28-6e0d536571c4", - "auth_token_40382420", + entityId, + mountAccessor, customMetadataRequest ); + RequestBody body = RequestBody.create(toJson(entityAliasRequest), JSON); + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/identity/entity-alias") + .post(body) + .build(); + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new VaultException("Could not create entity alias. Due to: " + response.body().string(), response.code()); + } + } catch (IOException e) { + throw new VaultException(e); + } + } + + private String getMountAccessor() throws VaultException { + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/sys/auth") + .get() + .build(); + + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new VaultException("Could not fetch auth mounts own token. Due to: " + response.body().string(), response.code()); + } + + AuthMountsResponse authMountsResponse = GsonTransformer.fromJson(response.body().string(), AuthMountsResponse.class); + return authMountsResponse.getToken().getAccessor(); + } catch (IOException e) { + throw new VaultException(e); + } + } + + private String getEntityId() throws VaultException { + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/auth/token/lookup-self") + .get() + .build(); + try { - RestResponse restResponse = new Rest() - .url(vaultConfig.getAddress() + "/v1/identity/entity-alias") - .header("X-Vault-Token", vaultConfig.getToken()) - .body(toJson(entityAliasRequest).getBytes(StandardCharsets.UTF_8)) - .post(); - - if (restResponse.getStatus() < 200 || restResponse.getStatus() >= 300) { - String response = new String(restResponse.getBody(), StandardCharsets.UTF_8); - throw new VaultException("Could not create entity alias. Due to: " + response, restResponse.getStatus()); + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new VaultException("Could not lookup own token. Due to: " + response.body().string(), response.code()); } - } catch (RestException e) { + LookupResponse lookupResponse = dataResponseExtractor.extract(response, LookupResponse.class); + return lookupResponse.getEntityId(); + } catch (IOException e) { throw new VaultException(e); } } protected String assumePipeline(String pipelineName) throws VaultException { + CreateTokenRequest createTokenRequest = new CreateTokenRequest( + secretConfig.getPipelineTokenAuthBackendRole(), + secretConfig.getPipelinePolicy().isEmpty() ? null : secretConfig.getPipelinePolicy(), + entityAliasName(pipelineName) + ); - // TODO: Fech these from secret COnfig - Auth.TokenRequest tokenRequest = new Auth.TokenRequest() - .entityAlias(entityAliasName(pipelineName)) - .role("gocd-pipeline-dev-test") - .polices(Stream.of("gocd-pipeline-policy-dev-test").collect(Collectors.toList())); + RequestBody body = RequestBody.create(toJson(createTokenRequest), JSON); + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/auth/token/create/" + secretConfig.getPipelineTokenAuthBackendRole()) + .post(body) + .build(); + try { + Response response = client.newCall(request).execute(); - String pipelineAuthToken = getVault().auth().createToken(tokenRequest).getAuthClientToken(); - return pipelineAuthToken; + if (response.code() < 200 || response.code() >= 300) { + throw new VaultException("Could not create pipeline token. Due to: " + response.body().string(), response.code()); + } + + TokenResponse tokenResponse = GsonTransformer.fromJson(response.body().string(), TokenResponse.class); + return tokenResponse.getAuth().getClientToken(); + } catch (IOException e) { + throw new VaultException(e); + } } protected String oidcToken(String pipelineAuthToken, String path) throws VaultException { + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, pipelineAuthToken) + .url(vaultConfig.getAddress() + path) + .get() + .build(); + try { - RestResponse restResponse = new Rest() - .url(vaultConfig.getAddress() + path) - .header("X-Vault-Token", pipelineAuthToken) - .get(); - - String response = new String(restResponse.getBody(), StandardCharsets.UTF_8); - if (restResponse.getStatus() != 200) { - throw new VaultException("Could not read OIDC token. Due to: " + response, restResponse.getStatus()); + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new VaultException("Could not read OIDC token. Due to: " + response.body().string(), response.code()); } - DataResponse dataResponse = GsonTransformer.fromJson(response, DataResponse.class); + Type type = new TypeToken>() {}.getType(); + DataResponse dataResponse = GsonTransformer.fromJson(response.body().string(), type); return dataResponse.getData().getToken(); - - } catch (RestException e) { + } catch (IOException e) { throw new VaultException(e); } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/OIDCSecretEngineValidator.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/OIDCSecretEngineValidator.java new file mode 100644 index 0000000..3cbf5d2 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/validation/OIDCSecretEngineValidator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.validation; + +import cd.go.plugin.base.validation.ValidationResult; +import cd.go.plugin.base.validation.Validator; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; + +import java.util.Map; + +import static org.apache.commons.lang3.StringUtils.isEmpty; + +public class OIDCSecretEngineValidator implements Validator { + @Override + public ValidationResult validate(Map requestBody) { + SecretConfig secretConfig = SecretConfig.fromJSON(requestBody); + + ValidationResult result = new ValidationResult(); + + if (secretConfig.isOIDCSecretEngine()) { + if (isEmpty(secretConfig.getPipelineTokenAuthBackendRole())) { + result.add("PipelineTokenAuthBackendRole", "Pipeline Token Auth Backend Role can not be empty."); + } + } + + return result; + } +} + diff --git a/src/main/resources/secrets.template.html b/src/main/resources/secrets.template.html index 125e558..f0ec883 100755 --- a/src/main/resources/secrets.template.html +++ b/src/main/resources/secrets.template.html @@ -162,12 +162,43 @@
- + {{ GOINPUTNAME[VaultPath].$error.server }} -

+

This should be the path which holds the secrets. The plugin reads secrets from a single path.

+

+ This should be the path which holds the OIDC token. Usually this starts with '/v1/identity/oidc/token/...' +

+
+ +
+
+ + + {{ GOINPUTNAME[PipelineTokenAuthBackendRole].$error.server }} +

+ The Token-Auth Backend Role which is used by this plugin to assume a certain pipeline entity alias. +

+
+
+ +
+
+ + + {{ GOINPUTNAME[PipelinePolicy].$error.server }} +

+ An comma separated list of optional pipeline policy names to restrict the permissions this assumed pipeline entity alias will have. +

+
diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java index 5cbbeba..930cf66 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java @@ -63,8 +63,9 @@ void shouldReturnLookupResponse() throws VaultException, JSONException { final KVSecretEngine kvSecretEngine = mock(KVSecretEngine.class); final VaultConfig vaultConfig = mock(VaultConfig.class); + when(vaultProvider.getVaultConfig()).thenReturn(vaultConfig); when(request.getConfiguration()).thenReturn(secretConfig); - doReturn(kvSecretEngine).when(secretConfigLookupExecutor).buildSecretEngine(request, vault, vaultConfig, secretConfig); + doReturn(kvSecretEngine).when(secretConfigLookupExecutor).buildSecretEngine(request, vault, vaultConfig); when(kvSecretEngine.getSecret(anyString(), eq("AWS_ACCESS_KEY"))).thenReturn(Optional.of("ASKDMDASDKLASDI")); when(kvSecretEngine.getSecret(anyString(), eq("AWS_SECRET_KEY"))).thenReturn(Optional.of("slfjskldfjsdjflfsdfsffdadsdfsdfsdfsd;")); diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java new file mode 100644 index 0000000..ce2bd7a --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import cd.go.plugin.base.GsonTransformer; +import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; + +import java.io.IOException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +public class OkHTTPClientTest { + + private MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + } + + @AfterEach + public void cleanup() throws IOException { + mockWebServer.shutdown(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = "/secret-config-network-settings.json") + public void requestSucceedWithDefaultContentTypeAdded(String secretConfigJson) throws InterruptedException, IOException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); + OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + + mockWebServer.enqueue(new MockResponse()); + + mockWebServer.start(); + + okHttpClient.newCall(new Request.Builder().url(mockWebServer.url("")).build()).execute(); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo(OkHTTPClientFactory.JSON.toString()); + } + + @ParameterizedTest + @JsonSource(jsonFiles = "/secret-config-network-settings.json") + public void requestSucceedWithRetries(String secretConfigJson) throws IOException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); + OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse()); + + mockWebServer.start(); + + Response response = okHttpClient.newCall(new Request.Builder().url(mockWebServer.url("")).build()).execute(); + + assertThat(mockWebServer.getRequestCount()).isEqualTo(4); + assertThat(response.code()).isEqualTo(200); + } + + @ParameterizedTest + @JsonSource(jsonFiles = "/secret-config-network-settings.json") + public void requestFailsWithMaxRetriesExceeded(String secretConfigJson) throws IOException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); + OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse().setResponseCode(500)); + mockWebServer.enqueue(new MockResponse()); + + mockWebServer.start(); + + Response response = okHttpClient.newCall(new Request.Builder().url(mockWebServer.url("")).build()).execute(); + + assertThat(mockWebServer.getRequestCount()).isEqualTo(4); + assertThat(response.code()).isEqualTo(500); + } + + @ParameterizedTest + @JsonSource(jsonFiles = "/secret-config-network-settings.json") + public void requestSucceedsWithAddingNamespaceHeader(String secretConfigJson) throws IOException, InterruptedException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); + OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + + mockWebServer.enqueue(new MockResponse()); + + mockWebServer.start(); + + okHttpClient.newCall(new Request.Builder().url(mockWebServer.url("")).build()).execute(); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + assertThat(recordedRequest.getHeader("X-Vault-Namespace")).isEqualTo("dev"); + } + + @ParameterizedTest + @JsonSource(jsonFiles = "/secret-config-network-settings.json") + public void requestSucceedsWithNotAddingNamespaceHeader(String secretConfigJson) throws IOException, InterruptedException { + SecretConfig secretConfig = spy(GsonTransformer.fromJson(secretConfigJson, SecretConfig.class)); + OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); + doReturn("").when(secretConfig).getNameSpace(); + OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + + mockWebServer.enqueue(new MockResponse()); + + mockWebServer.start(); + + okHttpClient.newCall(new Request.Builder().url(mockWebServer.url("")).build()).execute(); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + assertThat(recordedRequest.getHeader("X-Vault-Namespace")).isNull(); + } +} diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java new file mode 100644 index 0000000..64653fd --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java @@ -0,0 +1,180 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.secretengines; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.CustomMetadataRequest; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +class OIDCPipelineIdentityProviderTest { + + private VaultConfig vaultConfig; + private MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + } + + @AfterEach + public void cleanup() throws IOException { + mockWebServer.shutdown(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/lookup-self.json", + "/mocks/vault/auth-mounts.json" + }) + public void createPipelineEntityAliasTest(String secretConfigJson, String lookupSelfResponse, String authMountsResponse) throws VaultException, IOException, InterruptedException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + + mockWebServer.enqueue(new MockResponse().setBody(lookupSelfResponse)); + mockWebServer.enqueue(new MockResponse().setBody(authMountsResponse)); + mockWebServer.enqueue(new MockResponse().setResponseCode(204)); + + mockWebServer.start(); + + vaultConfig = new VaultConfig() + .address(mockWebServer.url("").url().toString()) + .token("some-token") + .build(); + + OIDCPipelineIdentityProvider oidcPipelineIdentityProvider = new OIDCPipelineIdentityProvider( + mock(Vault.class), + vaultConfig, + secretConfig + ); + + CustomMetadataRequest customMetadataRequest = new CustomMetadataRequest( + "some_group", + "some_pipelinename", + "some_repository", + "some_organization", + "some_branch" + ); + + oidcPipelineIdentityProvider.createPipelineEntityAlias(customMetadataRequest); + + RecordedRequest lookupself = mockWebServer.takeRequest(); + assertThat(lookupself.getPath()).isEqualTo("/v1/auth/token/lookup-self"); + assertThat(lookupself.getMethod()).isEqualTo("GET"); + + RecordedRequest authList = mockWebServer.takeRequest(); + assertThat(authList.getPath()).isEqualTo("/v1/sys/auth"); + assertThat(authList.getMethod()).isEqualTo("GET"); + + RecordedRequest createEntityAlias = mockWebServer.takeRequest(); + assertThat(createEntityAlias.getPath()).isEqualTo("/v1/identity/entity-alias"); + assertThat(createEntityAlias.getMethod()).isEqualTo("POST"); + + String body = extractBodyAsString(createEntityAlias); + + assertThat(body).isEqualToNormalizingWhitespace("{\"name\":\"gocd-pipeline-dev-test-some_pipelinename\",\"canonical_id\":\"7d2e3179-f69b-450c-7179-ac8ee8bd8ca9\",\"mount_accessor\":\"auth_token_12ac23\",\"custom_metadata\":{\"group\":\"some_group\",\"pipeline\":\"some_pipelinename\",\"repository\":\"some_repository\",\"organization\":\"some_organization\",\"branch\":\"some_branch\"}}\n"); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/auth-token.json" + }) + public void assumePipelineTest(String secretConfigJson, String authTokenResponse) throws VaultException, IOException, InterruptedException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + + mockWebServer.enqueue(new MockResponse().setBody(authTokenResponse)); + mockWebServer.start(); + vaultConfig = new VaultConfig() + .address(mockWebServer.url("").url().toString()) + .token("some-token") + .build(); + OIDCPipelineIdentityProvider oidcPipelineIdentityProvider = new OIDCPipelineIdentityProvider( + mock(Vault.class), + vaultConfig, + secretConfig + ); + + String pipelineToken = oidcPipelineIdentityProvider.assumePipeline("some_pipelinename"); + assertThat(pipelineToken).isEqualTo("s.wOrq9dO9kzOcuvB06CMviJhZ"); + + RecordedRequest tokenCreation = mockWebServer.takeRequest(); + assertThat(tokenCreation.getPath()).isEqualTo("/v1/auth/token/create/some-backend-role"); + assertThat(tokenCreation.getMethod()).isEqualTo("POST"); + + String body = extractBodyAsString(tokenCreation); + assertThat(body).isEqualTo("{\"role_name\":\"some-backend-role\",\"policies\":[\"some-policy\"],\"entity_alias\":\"gocd-pipeline-dev-test-some_pipelinename\"}"); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/oidc-token.json" + }) + public void oidcTokenTest(String secretConfigJson, String oidcTokenResponse) throws VaultException, IOException, InterruptedException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + + mockWebServer.enqueue(new MockResponse().setBody(oidcTokenResponse)); + mockWebServer.start(); + vaultConfig = new VaultConfig() + .address(mockWebServer.url("").url().toString()) + .token("some-token") + .build(); + OIDCPipelineIdentityProvider oidcPipelineIdentityProvider = new OIDCPipelineIdentityProvider( + mock(Vault.class), + vaultConfig, + secretConfig + ); + + String oidcToken = oidcPipelineIdentityProvider.oidcToken("some_token", "/v1/identity/oidc/token/some-oidc-backend"); + assertThat(oidcToken).isEqualTo("eyJhbGciOiJSUzI1NiIsImtpZCI6IjJkMGI4YjlkLWYwNGQtNzFlYy1iNjc0LWM3MzU4NDMyYmM1YiJ9.eyJhdWQiOiJQNkNmQ3p5SHNRWTRwTWNBNmtXQU9DSXRBNyIsImV4cCI6MTU2MTQ4ODQxMiwiaWF0IjoxNTYxNDAyMDEyLCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tOjEyMzQiLCJzdWIiOiI2YzY1ZWFmNy1kNGY0LTEzMzMtMDJiYy0xYzc1MjE5YzMxMDIifQ.IcbWTmks7P5eVtwmIBl5rL1B88MI55a9JJuYVLIlwE9aP_ilXpX5fE38CDm5PixDDVJb8TI2Q_FO4GMMH0ymHDO25ZvA917WcyHCSBGaQlgcS-WUL2fYTqFjSh-pezszaYBgPuGvH7hJjlTZO6g0LPCyUWat3zcRIjIQdXZum-OyhWAelQlveEL8sOG_ldyZ8v7fy7GXDxfJOK1kpw5AX9DXJKylbwZTBS8tLb-7edq8uZ0lNQyWy9VPEW_EEIZvGWy0AHua-Loa2l59GRRP8mPxuMYxH_c88x1lsSw0vH9E3rU8AXLyF3n4d40PASXEjZ-7dnIf4w4hf2P4L0xs_g"); + + RecordedRequest tokenCreation = mockWebServer.takeRequest(); + assertThat(tokenCreation.getPath()).isEqualTo("/v1/identity/oidc/token/some-oidc-backend"); + assertThat(tokenCreation.getMethod()).isEqualTo("GET"); + assertThat(tokenCreation.getHeader("X-Vault-Token")).isEqualTo("some_token"); + } + + @NotNull + private String extractBodyAsString(RecordedRequest request) { + String body = new BufferedReader(new InputStreamReader(request.getBody().inputStream(), StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("")); + return body; + } + + +} \ No newline at end of file diff --git a/src/test/resources/mocks/vault/auth-mounts.json b/src/test/resources/mocks/vault/auth-mounts.json new file mode 100644 index 0000000..9a2c2fe --- /dev/null +++ b/src/test/resources/mocks/vault/auth-mounts.json @@ -0,0 +1,15 @@ +{ + "github/": { + "type": "github", + "description": "GitHub auth" + }, + "token/": { + "config": { + "default_lease_ttl": 0, + "max_lease_ttl": 0 + }, + "description": "token based credentials", + "accessor": "auth_token_12ac23", + "type": "token" + } +} \ No newline at end of file diff --git a/src/test/resources/mocks/vault/auth-token.json b/src/test/resources/mocks/vault/auth-token.json new file mode 100644 index 0000000..5ca59c5 --- /dev/null +++ b/src/test/resources/mocks/vault/auth-token.json @@ -0,0 +1,27 @@ +{ + "request_id": "f00341c1-fad5-f6e6-13fd-235617f858a1", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": null, + "wrap_info": null, + "warnings": [ + "Policy \"stage\" does not exist", + "Policy \"web\" does not exist" + ], + "auth": { + "client_token": "s.wOrq9dO9kzOcuvB06CMviJhZ", + "accessor": "B6oixijqmeR4bsLOJH88Ska9", + "policies": ["default", "stage", "web"], + "token_policies": ["default", "stage", "web"], + "metadata": { + "user": "armon" + }, + "lease_duration": 3600, + "renewable": true, + "entity_id": "", + "token_type": "service", + "orphan": false, + "num_uses": 0 + } +} \ No newline at end of file diff --git a/src/test/resources/mocks/vault/lookup-self.json b/src/test/resources/mocks/vault/lookup-self.json new file mode 100644 index 0000000..bfb8cfc --- /dev/null +++ b/src/test/resources/mocks/vault/lookup-self.json @@ -0,0 +1,23 @@ +{ + "data": { + "accessor":"8609694a-cdbc-db9b-d345-e782dbb562ed", + "creation_time": 1523979354, + "creation_ttl": 2764800, + "display_name":"ldap2-tesla", + "entity_id":"7d2e3179-f69b-450c-7179-ac8ee8bd8ca9", + "expire_time":"2018-05-19T11:35:54.466476215-04:00", + "explicit_max_ttl": 0, + "id":"cf64a70f-3a12-3f6c-791d-6cef6d390eed", + "identity_policies": ["dev-group-policy"], + "issue_time":"2018-04-17T11:35:54.466476078-04:00", + "meta": { + "username":"tesla" + }, + "num_uses": 0, + "orphan": true, + "path":"auth/ldap2/login/tesla", + "policies": ["default","testgroup2-policy"], + "renewable": true, + "ttl": 2764790 + } +} \ No newline at end of file diff --git a/src/test/resources/mocks/vault/oidc-token.json b/src/test/resources/mocks/vault/oidc-token.json new file mode 100644 index 0000000..f98127a --- /dev/null +++ b/src/test/resources/mocks/vault/oidc-token.json @@ -0,0 +1,7 @@ +{ + "data": { + "client_id": "P6CfCzyHsQY4pMcA6kWAOCItA7", + "token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjJkMGI4YjlkLWYwNGQtNzFlYy1iNjc0LWM3MzU4NDMyYmM1YiJ9.eyJhdWQiOiJQNkNmQ3p5SHNRWTRwTWNBNmtXQU9DSXRBNyIsImV4cCI6MTU2MTQ4ODQxMiwiaWF0IjoxNTYxNDAyMDEyLCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tOjEyMzQiLCJzdWIiOiI2YzY1ZWFmNy1kNGY0LTEzMzMtMDJiYy0xYzc1MjE5YzMxMDIifQ.IcbWTmks7P5eVtwmIBl5rL1B88MI55a9JJuYVLIlwE9aP_ilXpX5fE38CDm5PixDDVJb8TI2Q_FO4GMMH0ymHDO25ZvA917WcyHCSBGaQlgcS-WUL2fYTqFjSh-pezszaYBgPuGvH7hJjlTZO6g0LPCyUWat3zcRIjIQdXZum-OyhWAelQlveEL8sOG_ldyZ8v7fy7GXDxfJOK1kpw5AX9DXJKylbwZTBS8tLb-7edq8uZ0lNQyWy9VPEW_EEIZvGWy0AHua-Loa2l59GRRP8mPxuMYxH_c88x1lsSw0vH9E3rU8AXLyF3n4d40PASXEjZ-7dnIf4w4hf2P4L0xs_g", + "ttl": 86400 + } +} \ No newline at end of file diff --git a/src/test/resources/secret-config-metadata.json b/src/test/resources/secret-config-metadata.json index 2ed7762..dafbb3a 100644 --- a/src/test/resources/secret-config-metadata.json +++ b/src/test/resources/secret-config-metadata.json @@ -14,6 +14,22 @@ "required": false, "secure": false } + }, + { + "key": "PipelineTokenAuthBackendRole", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } + }, + { + "key": "PipelinePolicy", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } }, { "key": "VaultPath", diff --git a/src/test/resources/secret-config-network-settings.json b/src/test/resources/secret-config-network-settings.json new file mode 100644 index 0000000..e961cac --- /dev/null +++ b/src/test/resources/secret-config-network-settings.json @@ -0,0 +1,11 @@ +{ + "VaultUrl": "https://foo.bar", + "Token": "some-token", + "VaultPath": "secret/data", + "NameSpace": "dev", + "ConnectionTimeout": 10, + "ReadTimeout": 20, + "MaxRetries": 3, + "RetryIntervalMilliseconds": 300, + "AuthMethod": "token" +} diff --git a/src/test/resources/secret-config-oidc.json b/src/test/resources/secret-config-oidc.json new file mode 100644 index 0000000..a7bf649 --- /dev/null +++ b/src/test/resources/secret-config-oidc.json @@ -0,0 +1,11 @@ +{ + "VaultUrl": "https://not.a.domain.com", + "Token": "some-token", + "VaultPath": "secret/data", + "ConnectionTimeout": 10, + "ReadTimeout": 20, + "AuthMethod": "token", + "SecretEngine": "oidc", + "PipelineTokenAuthBackendRole": "some-backend-role", + "PipelinePolicy": "some-policy" +} From e5b4a7217a3cef72348b716294bc063557a46fbb Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Thu, 24 Mar 2022 17:58:42 +0100 Subject: [PATCH 04/11] Added GoCD API Integration --- .../vault/gocd/GoCDPipelineApi.java | 113 +++++++++++++++ .../vault/http/DataResponseExtractor.java | 2 +- .../http/DefaultContentTypeInterceptor.java | 2 +- .../http/GoCDAuthenticationInterceptor.java | 55 +++++++ .../vault/http/OkHTTPClientFactory.java | 17 ++- .../vault/http/exceptions/APIException.java | 28 ++++ .../vault/models/PipelineMaterial.java | 87 +++++++++++ .../vault/models/SecretConfig.java | 35 ++++- .../request/gocd/PipelineConfigResponse.java | 96 +++++++++++++ .../vault/request/gocd/SCMResponse.java | 57 ++++++++ .../{ => vault}/AuthMountsResponse.java | 2 +- .../{ => vault}/CreateTokenRequest.java | 2 +- .../{ => vault}/CustomMetadataRequest.java | 13 +- .../request/{ => vault}/DataResponse.java | 2 +- .../{ => vault}/EntityAliasRequest.java | 5 +- .../request/{ => vault}/LookupResponse.java | 2 +- .../{ => vault}/OICDTokenResponse.java | 2 +- .../{ => vault}/TokenAuthMountResponse.java | 2 +- .../request/{ => vault}/TokenResponse.java | 2 +- .../vault/secretengines/KVSecretEngine.java | 9 +- .../OIDCPipelineIdentityProvider.java | 57 ++++---- .../vault/secretengines/SecretEngine.java | 3 +- src/main/resources/secrets.template.html | 37 ++++- .../vault/SecretConfigLookupExecutorTest.java | 3 +- .../vault/gocd/GoCDPipelineApiTest.java | 135 ++++++++++++++++++ .../vault/http/OkHTTPClientTest.java | 12 +- .../OIDCPipelineIdentityProviderTest.java | 14 +- .../mocks/gocd/pipeline-config-scm.json | 96 +++++++++++++ .../resources/mocks/gocd/pipeline-config.json | 101 +++++++++++++ .../resources/mocks/gocd/scm-response.json | 37 +++++ .../resources/secret-config-metadata.json | 24 ++++ src/test/resources/secret-config-oidc.json | 5 +- 32 files changed, 991 insertions(+), 66 deletions(-) create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/GoCDAuthenticationInterceptor.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/exceptions/APIException.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/PipelineMaterial.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/AuthMountsResponse.java (93%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/CreateTokenRequest.java (94%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/CustomMetadataRequest.java (75%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/DataResponse.java (93%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/EntityAliasRequest.java (89%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/LookupResponse.java (93%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/OICDTokenResponse.java (94%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/TokenAuthMountResponse.java (93%) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/{ => vault}/TokenResponse.java (94%) create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java create mode 100644 src/test/resources/mocks/gocd/pipeline-config-scm.json create mode 100644 src/test/resources/mocks/gocd/pipeline-config.json create mode 100644 src/test/resources/mocks/gocd/scm-response.json diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java new file mode 100644 index 0000000..c84e1cd --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java @@ -0,0 +1,113 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.gocd; + +import cd.go.plugin.base.GsonTransformer; +import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.gocd.PipelineConfigResponse; +import com.thoughtworks.gocd.secretmanager.vault.request.gocd.SCMResponse; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +public class GoCDPipelineApi { + + private final OkHttpClient client; + private final String gocdServerURL; + + public static final MediaType ACCEPT_GOCD_V11_JSON = MediaType.parse("application/vnd.go.cd.v11+json"); + public static final MediaType ACCEPT_GOCD_V4_JSON = MediaType.parse("application/vnd.go.cd.v4+json"); + + public GoCDPipelineApi(SecretConfig secretConfig) { + this.client = new OkHTTPClientFactory().gocd(secretConfig); + this.gocdServerURL = secretConfig.getGocdServerURL(); + } + + public PipelineMaterial fetchPipelineMaterial(String pipeline) throws APIException { + Request request = new Request.Builder() + .url(gocdServerURL + "/go/api/admin/pipelines/" + pipeline) + .header("Accept", ACCEPT_GOCD_V11_JSON.toString()) + .get() + .build(); + try { + Response response = client.newCall(request).execute(); + PipelineConfigResponse pipelineConfigResponse = GsonTransformer.fromJson(response.body().string(), PipelineConfigResponse.class); + if (pipelineConfigResponse.getMaterials().isEmpty()) { + throw new IllegalStateException(String.format("Material configuration for pipeline %s is empty. Can not infer material context.", pipeline)); + } + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException(String.format("Could not fetch pipeline configuration for pipeline %s. Due to: %s", pipeline, response.body().string()), response.code()); + } + + // Even if there are multiple pipeline definitions, we will always use the first found material. + PipelineConfigResponse.PipelineConfigMaterialResponse pipelineConfigMaterialResponse = pipelineConfigResponse.getMaterials().get(0); + String materialType = pipelineConfigMaterialResponse.getType(); + String repositoryUrl = pipelineConfigMaterialResponse.getAttributes().getUrl(); + if(materialType.equalsIgnoreCase("plugin")) { + repositoryUrl = fetchSCMRepositoryUrl(pipelineConfigResponse.getName()); + } + + return new PipelineMaterial( + pipelineConfigResponse.getName(), + pipelineConfigResponse.getGroup(), + pipelineConfigMaterialResponse.getAttributes().getBranch(), + repositoryUrl + ); + } catch (IOException e) { + throw new APIException(e); + } + } + + private String fetchSCMRepositoryUrl(String name) throws APIException { + Request request = new Request.Builder() + .url(gocdServerURL + "/go/api/admin/scms/" + name) + .header("Accept", ACCEPT_GOCD_V4_JSON.toString()) + .get() + .build(); + + try { + Response response = client.newCall(request).execute(); + SCMResponse pipelineConfigResponse = GsonTransformer.fromJson(response.body().string(), SCMResponse.class); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException(String.format("Could not fetch pipeline configuration for pipeline %s. Due to: %s", name, response.body().string()), response.code()); + } + + if (pipelineConfigResponse.getConfigurations().isEmpty()) { + throw new IllegalStateException(String.format("Material configuration for scm %s is empty. Can not infer material context.", name)); + } + + return pipelineConfigResponse.getConfigurations() + .stream() + .filter(scmConfiguration -> scmConfiguration.getKey().equalsIgnoreCase("url")) + .findFirst() + .map(scmConfiguration -> scmConfiguration.getValue()) + .orElseThrow(() -> new IllegalStateException(String.format("Material configuration for scm %s does not contain repository url.", name))); + } catch (IOException e) { + throw new APIException(e); + } + + + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java index 4f6e422..e53b828 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java @@ -17,7 +17,7 @@ package com.thoughtworks.gocd.secretmanager.vault.http; import cd.go.plugin.base.GsonTransformer; -import com.thoughtworks.gocd.secretmanager.vault.request.DataResponse; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.DataResponse; import okhttp3.Response; import java.io.IOException; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java index c439fe6..4bc40ef 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DefaultContentTypeInterceptor.java @@ -35,7 +35,7 @@ public Response intercept(Interceptor.Chain chain) throws IOException { Request originalRequest = chain.request(); Request requestWithUserAgent = originalRequest .newBuilder() - .header("Content-Type", OkHTTPClientFactory.JSON.toString()) + .header("Content-Type", contentType) .build(); return chain.proceed(requestWithUserAgent); diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/GoCDAuthenticationInterceptor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/GoCDAuthenticationInterceptor.java new file mode 100644 index 0000000..cb891d7 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/GoCDAuthenticationInterceptor.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class GoCDAuthenticationInterceptor implements Interceptor { + + private final String username; + private final String password; + + public GoCDAuthenticationInterceptor(String username, String password) { + this.username = username; + this.password = password; + } + + @NotNull + @Override + public Response intercept(@NotNull Interceptor.Chain chain) throws IOException { + Request request = chain.request(); + request = request + .newBuilder() + .header("Authorization", basicAuth()) + .build(); + return chain.proceed(request); + } + + @NotNull + private String basicAuth() { + return "Basic " + Base64.getEncoder().encodeToString( + (username + ":" + password).getBytes(StandardCharsets.UTF_8) + ); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java index 62f3627..4f8ec00 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.java @@ -24,16 +24,27 @@ public class OkHTTPClientFactory { - public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + public static final MediaType CONTENT_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); - public OkHttpClient buildFor(SecretConfig secretConfig) { + public OkHttpClient vault(SecretConfig secretConfig) { // TODO: Handle SSL Config return new OkHttpClient.Builder() .readTimeout(secretConfig.getReadTimeout(), TimeUnit.SECONDS) .connectTimeout(secretConfig.getConnectionTimeout(), TimeUnit.SECONDS) - .addInterceptor(new DefaultContentTypeInterceptor(JSON.toString())) + .addInterceptor(new DefaultContentTypeInterceptor(CONTENT_TYPE_JSON.toString())) .addInterceptor(new VaultHeaderInterceptor(secretConfig.getNameSpace())) .addInterceptor(new RetryInterceptor(secretConfig.getRetryIntervalMilliseconds(), secretConfig.getMaxRetries())) .build(); } + + public OkHttpClient gocd(SecretConfig secretConfig) { + // TODO: Handle SSL Config + return new OkHttpClient.Builder() + .readTimeout(secretConfig.getReadTimeout(), TimeUnit.SECONDS) + .connectTimeout(secretConfig.getConnectionTimeout(), TimeUnit.SECONDS) + .addInterceptor(new GoCDAuthenticationInterceptor(secretConfig.getGoCDUsername(), secretConfig.getGoCDPassword())) + .build(); + } + + } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/exceptions/APIException.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/exceptions/APIException.java new file mode 100644 index 0000000..763af92 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/exceptions/APIException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.http.exceptions; + +public class APIException extends Exception { + + public APIException(Throwable cause) { + super(cause); + } + + public APIException(String message, int code) { + super(String.format("%s [Response code: %d]", message, code)); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/PipelineMaterial.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/PipelineMaterial.java new file mode 100644 index 0000000..e7eafd7 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/PipelineMaterial.java @@ -0,0 +1,87 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.models; + +import com.bettercloud.vault.VaultException; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PipelineMaterial { + + private final String name; + private final String group; + private final String organization; + private final String repositoryName; + // Nullable + private final String branch; + + private final Pattern githubRepositoryURLRegex = Pattern.compile("git@github\\.com:(?[^\\/]+)\\/(?.+?)(\\.git)?", Pattern.CASE_INSENSITIVE); + + public PipelineMaterial(String name, String group, String organization, String repositoryName, String branch) { + this.name = name; + this.group = group; + this.organization = organization; + this.repositoryName = repositoryName; + this.branch = branch; + } + + public PipelineMaterial(String name, String group, String branch, String repositoryURL) { + this.name = name; + this.group = group; + this.branch = branch; + Matcher matcher = githubRepositoryURLRegex.matcher(repositoryURL); + if (! matcher.matches()) { + throw new IllegalStateException(String.format("Given URL [%s] is not a valid git ssh URL.", repositoryURL)); + } + this.organization = matcher.group("organization"); + this.repositoryName = matcher.group("repository"); + } + + public String getName() { + return name; + } + + public String getGroup() { + return group; + } + + public String getOrganization() { + return organization; + } + + public String getRepositoryName() { + return repositoryName; + } + + public String getBranch() { + return branch; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PipelineMaterial that = (PipelineMaterial) o; + return Objects.equals(name, that.name) && + Objects.equals(group, that.group) && + Objects.equals(organization, that.organization) && + Objects.equals(repositoryName, that.repositoryName) && + Objects.equals(branch, that.branch); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java index 066564c..fc8d086 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java @@ -132,6 +132,21 @@ public class SecretConfig { @Property(name = "ServerPem", secure = true) private String serverPem; + @Expose + @SerializedName("GoCDServerUrl") + @Property(name = "GoCDServerUrl") + private String gocdServerURL; + + @Expose + @SerializedName("GoCDUsername") + @Property(name = "GoCDUsername") + private String gocdUsername; + + @Expose + @SerializedName("GoCDPassword") + @Property(name = "GoCDPassword", secure = true) + private String gocdPassword; + public String getVaultUrl() { return vaultUrl; } @@ -218,6 +233,21 @@ public List getPipelinePolicy() { return Arrays.asList(pipelinePolicy.split(",\\s*")); } + public String getGocdServerURL() { + if (gocdServerURL.endsWith("/")) { + return gocdServerURL.substring(0, gocdServerURL.length() - 1); + } + return gocdServerURL; + } + + public String getGoCDUsername() { + return gocdUsername; + } + + public String getGoCDPassword() { + return gocdPassword; + } + public boolean isAuthMethodSupported() { return SUPPORTED_AUTH_METHODS.contains(authMethod.toLowerCase()); } @@ -252,7 +282,10 @@ public boolean equals(Object o) { Objects.equals(serverPem, that.serverPem) && Objects.equals(secretEngine, that.secretEngine) && Objects.equals(pipelineTokenAuthBackendRole, that.pipelineTokenAuthBackendRole) && - Objects.equals(pipelinePolicy, that.pipelinePolicy); + Objects.equals(pipelinePolicy, that.pipelinePolicy) && + Objects.equals(gocdUsername, that.gocdUsername) && + Objects.equals(gocdPassword, that.gocdPassword); + } @Override diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java new file mode 100644 index 0000000..5629b54 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java @@ -0,0 +1,96 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.gocd; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class PipelineConfigResponse { + + @Expose + @SerializedName("name") + private String name; + + @Expose + @SerializedName("group") + private String group; + + @Expose + @SerializedName("materials") + private List materials; + + public PipelineConfigResponse() { + } + + public String getName() { + return name; + } + + public String getGroup() { + return group; + } + + public List getMaterials() { + return materials; + } + + public class PipelineConfigMaterialResponse { + + @Expose + @SerializedName("type") + private String type; + + @Expose + @SerializedName("attributes") + private PipelineConfigMaterialAttributesResponse attributes; + + public PipelineConfigMaterialResponse() { + } + + public String getType() { + return type; + } + + public PipelineConfigMaterialAttributesResponse getAttributes() { + return attributes; + } + } + + public class PipelineConfigMaterialAttributesResponse { + + @Expose + @SerializedName("url") + private String url; + + @Expose + @SerializedName("branch") + private String branch; + + public PipelineConfigMaterialAttributesResponse() { + } + + public String getUrl() { + return url; + } + + public String getBranch() { + return branch; + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java new file mode 100644 index 0000000..cc2a229 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java @@ -0,0 +1,57 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.gocd; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class SCMResponse { + @Expose + @SerializedName("configuration") + private List configurations; + + public SCMResponse() { + } + + public List getConfigurations() { + return configurations; + } + + public class SCMConfiguration { + + @Expose + @SerializedName("key") + private String key; + + @Expose + @SerializedName("value") + private String value; + + public SCMConfiguration() { + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthMountsResponse.java similarity index 93% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthMountsResponse.java index dc7178f..e65b29a 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/AuthMountsResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthMountsResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CreateTokenRequest.java similarity index 94% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CreateTokenRequest.java index 9c88d27..81aeb03 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CreateTokenRequest.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CreateTokenRequest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CustomMetadataRequest.java similarity index 75% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CustomMetadataRequest.java index 559e0ac..1a28f16 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/CustomMetadataRequest.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CustomMetadataRequest.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; public class CustomMetadataRequest { @@ -49,6 +50,16 @@ public CustomMetadataRequest(String group, String pipeline, String repository, S this.branch = branch; } + public CustomMetadataRequest(PipelineMaterial pipelineMaterial) { + this( + pipelineMaterial.getGroup(), + pipelineMaterial.getName(), + pipelineMaterial.getRepositoryName(), + pipelineMaterial.getOrganization(), + pipelineMaterial.getBranch() + ); + } + public String getPipeline() { return pipeline; } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/DataResponse.java similarity index 93% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/DataResponse.java index 1db0fa8..d050d09 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/DataResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/DataResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java similarity index 89% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java index 7322f0c..3aa384a 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/EntityAliasRequest.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java @@ -14,13 +14,10 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; -import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; - -import java.util.List; public class EntityAliasRequest { diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/LookupResponse.java similarity index 93% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/LookupResponse.java index 64c1838..c2bc966 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/LookupResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/LookupResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/OICDTokenResponse.java similarity index 94% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/OICDTokenResponse.java index 6ada3d9..6a45915 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/OICDTokenResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/OICDTokenResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenAuthMountResponse.java similarity index 93% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenAuthMountResponse.java index 3dcd649..a2ce905 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenAuthMountResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenAuthMountResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java similarity index 94% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java index 4884cba..79c0706 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/TokenResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.request; +package com.thoughtworks.gocd.secretmanager.vault.request.vault; import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java index b35726e..64490c1 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.java @@ -18,6 +18,7 @@ import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultException; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; import java.util.Map; import java.util.Optional; @@ -31,9 +32,13 @@ public KVSecretEngine(Vault vault) { } @Override - public Optional getSecret(String path, String key) throws VaultException { + public Optional getSecret(String path, String key) throws APIException { if (secretsFromVault == null) { - secretsFromVault = getSecretData(path); + try { + secretsFromVault = getSecretData(path); + } catch (VaultException vaultException) { + throw new APIException(vaultException); + } } return Optional.ofNullable(secretsFromVault.get(key)); diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java index 9efbdf4..596d98f 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java @@ -19,12 +19,14 @@ import cd.go.plugin.base.GsonTransformer; import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; -import com.bettercloud.vault.VaultException; import com.google.gson.reflect.TypeToken; +import com.thoughtworks.gocd.secretmanager.vault.gocd.GoCDPipelineApi; import com.thoughtworks.gocd.secretmanager.vault.http.DataResponseExtractor; import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; -import com.thoughtworks.gocd.secretmanager.vault.request.*; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.*; import okhttp3.*; import java.io.IOException; @@ -32,12 +34,13 @@ import java.util.Optional; import static cd.go.plugin.base.GsonTransformer.toJson; -import static com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory.JSON; +import static com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory.CONTENT_TYPE_JSON; public class OIDCPipelineIdentityProvider extends SecretEngine { private final OkHttpClient client; private final DataResponseExtractor dataResponseExtractor; + private final GoCDPipelineApi gocdPipelineApi; private VaultConfig vaultConfig; private SecretConfig secretConfig; @@ -47,21 +50,17 @@ public OIDCPipelineIdentityProvider(Vault vault, VaultConfig vaultConfig, Secret super(vault); this.vaultConfig = vaultConfig; this.secretConfig = secretConfig; + this.gocdPipelineApi = new GoCDPipelineApi(secretConfig); this.dataResponseExtractor = new DataResponseExtractor(); OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); - this.client = okHTTPClientFactory.buildFor(secretConfig); + this.client = okHTTPClientFactory.vault(secretConfig); } @Override - public Optional getSecret(String path, String pipelineName) throws VaultException { - CustomMetadataRequest customMetadataRequest = new CustomMetadataRequest( - "some_group", - pipelineName, - "some_repository", - "some_organization", - "some_branch" - ); + public Optional getSecret(String path, String pipelineName) throws APIException { + PipelineMaterial pipelineMaterial = gocdPipelineApi.fetchPipelineMaterial(pipelineName); + CustomMetadataRequest customMetadataRequest = new CustomMetadataRequest(pipelineMaterial); createPipelineEntityAlias(customMetadataRequest); @@ -69,7 +68,7 @@ public Optional getSecret(String path, String pipelineName) throws Vault return Optional.of(oidcToken(pipelineAuthToken, path)); } - protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataRequest) throws VaultException { + protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataRequest) throws APIException { String entityId = getEntityId(); String mountAccessor = getMountAccessor(); @@ -80,7 +79,7 @@ protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataReq customMetadataRequest ); - RequestBody body = RequestBody.create(toJson(entityAliasRequest), JSON); + RequestBody body = RequestBody.create(toJson(entityAliasRequest), CONTENT_TYPE_JSON); Request request = new Request.Builder() .header(X_VAULT_TOKEN, vaultConfig.getToken()) .url(vaultConfig.getAddress() + "/v1/identity/entity-alias") @@ -90,14 +89,14 @@ protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataReq Response response = client.newCall(request).execute(); if (response.code() < 200 || response.code() >= 300) { - throw new VaultException("Could not create entity alias. Due to: " + response.body().string(), response.code()); + throw new APIException("Could not create entity alias. Due to: " + response.body().string(), response.code()); } } catch (IOException e) { - throw new VaultException(e); + throw new APIException(e); } } - private String getMountAccessor() throws VaultException { + private String getMountAccessor() throws APIException { Request request = new Request.Builder() .header(X_VAULT_TOKEN, vaultConfig.getToken()) .url(vaultConfig.getAddress() + "/v1/sys/auth") @@ -108,17 +107,17 @@ private String getMountAccessor() throws VaultException { Response response = client.newCall(request).execute(); if (response.code() < 200 || response.code() >= 300) { - throw new VaultException("Could not fetch auth mounts own token. Due to: " + response.body().string(), response.code()); + throw new APIException("Could not fetch auth mounts own token. Due to: " + response.body().string(), response.code()); } AuthMountsResponse authMountsResponse = GsonTransformer.fromJson(response.body().string(), AuthMountsResponse.class); return authMountsResponse.getToken().getAccessor(); } catch (IOException e) { - throw new VaultException(e); + throw new APIException(e); } } - private String getEntityId() throws VaultException { + private String getEntityId() throws APIException { Request request = new Request.Builder() .header(X_VAULT_TOKEN, vaultConfig.getToken()) .url(vaultConfig.getAddress() + "/v1/auth/token/lookup-self") @@ -129,24 +128,24 @@ private String getEntityId() throws VaultException { Response response = client.newCall(request).execute(); if (response.code() < 200 || response.code() >= 300) { - throw new VaultException("Could not lookup own token. Due to: " + response.body().string(), response.code()); + throw new APIException("Could not lookup own token. Due to: " + response.body().string(), response.code()); } LookupResponse lookupResponse = dataResponseExtractor.extract(response, LookupResponse.class); return lookupResponse.getEntityId(); } catch (IOException e) { - throw new VaultException(e); + throw new APIException(e); } } - protected String assumePipeline(String pipelineName) throws VaultException { + protected String assumePipeline(String pipelineName) throws APIException { CreateTokenRequest createTokenRequest = new CreateTokenRequest( secretConfig.getPipelineTokenAuthBackendRole(), secretConfig.getPipelinePolicy().isEmpty() ? null : secretConfig.getPipelinePolicy(), entityAliasName(pipelineName) ); - RequestBody body = RequestBody.create(toJson(createTokenRequest), JSON); + RequestBody body = RequestBody.create(toJson(createTokenRequest), CONTENT_TYPE_JSON); Request request = new Request.Builder() .header(X_VAULT_TOKEN, vaultConfig.getToken()) .url(vaultConfig.getAddress() + "/v1/auth/token/create/" + secretConfig.getPipelineTokenAuthBackendRole()) @@ -156,17 +155,17 @@ protected String assumePipeline(String pipelineName) throws VaultException { Response response = client.newCall(request).execute(); if (response.code() < 200 || response.code() >= 300) { - throw new VaultException("Could not create pipeline token. Due to: " + response.body().string(), response.code()); + throw new APIException("Could not create pipeline token. Due to: " + response.body().string(), response.code()); } TokenResponse tokenResponse = GsonTransformer.fromJson(response.body().string(), TokenResponse.class); return tokenResponse.getAuth().getClientToken(); } catch (IOException e) { - throw new VaultException(e); + throw new APIException(e); } } - protected String oidcToken(String pipelineAuthToken, String path) throws VaultException { + protected String oidcToken(String pipelineAuthToken, String path) throws APIException { Request request = new Request.Builder() .header(X_VAULT_TOKEN, pipelineAuthToken) @@ -178,14 +177,14 @@ protected String oidcToken(String pipelineAuthToken, String path) throws VaultEx Response response = client.newCall(request).execute(); if (response.code() < 200 || response.code() >= 300) { - throw new VaultException("Could not read OIDC token. Due to: " + response.body().string(), response.code()); + throw new APIException("Could not read OIDC token. Due to: " + response.body().string(), response.code()); } Type type = new TypeToken>() {}.getType(); DataResponse dataResponse = GsonTransformer.fromJson(response.body().string(), type); return dataResponse.getData().getToken(); } catch (IOException e) { - throw new VaultException(e); + throw new APIException(e); } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java index 916fcaf..9ff010f 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java @@ -18,6 +18,7 @@ import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultException; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; import java.util.Optional; @@ -29,7 +30,7 @@ public SecretEngine(Vault vault) { this.vault = vault; } - public abstract Optional getSecret(String path, String key) throws VaultException; + public abstract Optional getSecret(String path, String key) throws APIException; public Vault getVault() { return vault; diff --git a/src/main/resources/secrets.template.html b/src/main/resources/secrets.template.html index f0ec883..8f5efdf 100755 --- a/src/main/resources/secrets.template.html +++ b/src/main/resources/secrets.template.html @@ -178,7 +178,7 @@ - + {{ GOINPUTNAME[PipelineTokenAuthBackendRole].$error.server }}

@@ -192,7 +192,7 @@ - + {{ GOINPUTNAME[PipelinePolicy].$error.server }}

@@ -201,6 +201,39 @@

+
+
+ + + {{ GOINPUTNAME[GoCDServerUrl].$error.server }} +
+
+ +
+
+ + + {{ GOINPUTNAME[GoCDUsername].$error.server }} +
+
+ +
+
+ + + {{ GOINPUTNAME[GoCDPassword].$error.server }} +
+
+
diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java index 930cf66..54a142e 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java @@ -21,6 +21,7 @@ import com.bettercloud.vault.VaultException; import com.bettercloud.vault.api.Logical; import com.thoughtworks.go.plugin.api.response.GoPluginApiResponse; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; import com.thoughtworks.gocd.secretmanager.vault.request.SecretConfigRequest; import com.thoughtworks.gocd.secretmanager.vault.secretengines.KVSecretEngine; @@ -57,7 +58,7 @@ void setUp() throws VaultException { } @Test - void shouldReturnLookupResponse() throws VaultException, JSONException { + void shouldReturnLookupResponse() throws APIException, JSONException { final SecretConfigRequest request = mock(SecretConfigRequest.class); final SecretConfig secretConfig = mock(SecretConfig.class); final KVSecretEngine kvSecretEngine = mock(KVSecretEngine.class); diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java new file mode 100644 index 0000000..e112924 --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.gocd; + +import cd.go.plugin.base.GsonTransformer; +import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class GoCDPipelineApiTest { + + private MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + } + + @AfterEach + public void cleanup() throws IOException { + mockWebServer.shutdown(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/gocd/pipeline-config.json" + }) + public void fetchPipelineMaterialTestSucceedsForGitMaterial(String secretConfigJson, String pipelineConfigResponse) throws IOException, InterruptedException, APIException { + SecretConfig secretConfig = spy(GsonTransformer.fromJson(secretConfigJson, SecretConfig.class)); + + mockWebServer.enqueue(new MockResponse().setBody(pipelineConfigResponse)); + mockWebServer.start(); + + doReturn(getMockServerAddress()).when(secretConfig).getGocdServerURL(); + GoCDPipelineApi goCDPipelineApi = new GoCDPipelineApi(secretConfig); + + PipelineMaterial pipelineMaterial = goCDPipelineApi.fetchPipelineMaterial("some-pipeline"); + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + + assertThat(recordedRequest.getHeader("Authorization")).isEqualTo("Basic " + Base64.getEncoder().encodeToString("username:supersecret".getBytes(StandardCharsets.UTF_8))); + assertThat(recordedRequest.getMethod()).isEqualTo("GET"); + assertThat(recordedRequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V11_JSON.toString()); + assertThat(recordedRequest.getPath()).isEqualTo("/go/api/admin/pipelines/some-pipeline"); + + assertThat(pipelineMaterial).isEqualTo( + new PipelineMaterial( + "some-pipeline", + "dev", + "important-organization", + "some-repository", + "main" + ) + ); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/gocd/pipeline-config-scm.json", + "/mocks/gocd/scm-response.json" + }) + public void fetchPipelineMaterialTestSucceedsForSCMMaterial(String secretConfigJson, String pipelineConfigResponse, String scmResponse) throws IOException, InterruptedException, APIException { + SecretConfig secretConfig = spy(GsonTransformer.fromJson(secretConfigJson, SecretConfig.class)); + + mockWebServer.enqueue(new MockResponse().setBody(pipelineConfigResponse)); + mockWebServer.enqueue(new MockResponse().setBody(scmResponse)); + mockWebServer.start(); + + doReturn(getMockServerAddress()).when(secretConfig).getGocdServerURL(); + GoCDPipelineApi goCDPipelineApi = new GoCDPipelineApi(secretConfig); + + PipelineMaterial pipelineMaterial = goCDPipelineApi.fetchPipelineMaterial("some-pipeline"); + RecordedRequest pipelineConfigrequest = mockWebServer.takeRequest(); + + String basicAuth = "Basic " + Base64.getEncoder().encodeToString("username:supersecret".getBytes(StandardCharsets.UTF_8)); + assertThat(pipelineConfigrequest.getHeader("Authorization")).isEqualTo(basicAuth); + assertThat(pipelineConfigrequest.getMethod()).isEqualTo("GET"); + assertThat(pipelineConfigrequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V11_JSON.toString()); + assertThat(pipelineConfigrequest.getPath()).isEqualTo("/go/api/admin/pipelines/some-pipeline"); + + RecordedRequest scmConfigRequest = mockWebServer.takeRequest(); + assertThat(scmConfigRequest.getHeader("Authorization")).isEqualTo(basicAuth); + assertThat(scmConfigRequest.getMethod()).isEqualTo("GET"); + assertThat(scmConfigRequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V4_JSON.toString()); + assertThat(scmConfigRequest.getPath()).isEqualTo("/go/api/admin/scms/some-pipeline"); + + + assertThat(pipelineMaterial).isEqualTo( + new PipelineMaterial( + "some-pipeline", + "dev", + "important-organization", + "some-repository", + null + ) + ); + } + + private String getMockServerAddress() { + String address = mockWebServer.url("").url().toString(); + return address.substring(0, address.length() - 1); + } + +} \ No newline at end of file diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java index ce2bd7a..061521e 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientTest.java @@ -54,7 +54,7 @@ public void cleanup() throws IOException { public void requestSucceedWithDefaultContentTypeAdded(String secretConfigJson) throws InterruptedException, IOException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); - OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + OkHttpClient okHttpClient = okHTTPClientFactory.vault(secretConfig); mockWebServer.enqueue(new MockResponse()); @@ -64,7 +64,7 @@ public void requestSucceedWithDefaultContentTypeAdded(String secretConfigJson) t RecordedRequest recordedRequest = mockWebServer.takeRequest(); - assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo(OkHTTPClientFactory.JSON.toString()); + assertThat(recordedRequest.getHeader("Content-Type")).isEqualTo(OkHTTPClientFactory.CONTENT_TYPE_JSON.toString()); } @ParameterizedTest @@ -72,7 +72,7 @@ public void requestSucceedWithDefaultContentTypeAdded(String secretConfigJson) t public void requestSucceedWithRetries(String secretConfigJson) throws IOException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); - OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + OkHttpClient okHttpClient = okHTTPClientFactory.vault(secretConfig); mockWebServer.enqueue(new MockResponse().setResponseCode(500)); mockWebServer.enqueue(new MockResponse().setResponseCode(500)); @@ -92,7 +92,7 @@ public void requestSucceedWithRetries(String secretConfigJson) throws IOExceptio public void requestFailsWithMaxRetriesExceeded(String secretConfigJson) throws IOException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); - OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + OkHttpClient okHttpClient = okHTTPClientFactory.vault(secretConfig); mockWebServer.enqueue(new MockResponse().setResponseCode(500)); mockWebServer.enqueue(new MockResponse().setResponseCode(500)); @@ -113,7 +113,7 @@ public void requestFailsWithMaxRetriesExceeded(String secretConfigJson) throws I public void requestSucceedsWithAddingNamespaceHeader(String secretConfigJson) throws IOException, InterruptedException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); - OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + OkHttpClient okHttpClient = okHTTPClientFactory.vault(secretConfig); mockWebServer.enqueue(new MockResponse()); @@ -132,7 +132,7 @@ public void requestSucceedsWithNotAddingNamespaceHeader(String secretConfigJson) SecretConfig secretConfig = spy(GsonTransformer.fromJson(secretConfigJson, SecretConfig.class)); OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); doReturn("").when(secretConfig).getNameSpace(); - OkHttpClient okHttpClient = okHTTPClientFactory.buildFor(secretConfig); + OkHttpClient okHttpClient = okHTTPClientFactory.vault(secretConfig); mockWebServer.enqueue(new MockResponse()); diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java index 64653fd..196e7d6 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java @@ -21,8 +21,10 @@ import com.bettercloud.vault.VaultConfig; import com.bettercloud.vault.VaultException; import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; -import com.thoughtworks.gocd.secretmanager.vault.request.CustomMetadataRequest; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.CustomMetadataRequest; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -35,6 +37,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.Optional; import java.util.stream.Collectors; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; @@ -61,7 +64,7 @@ public void cleanup() throws IOException { "/mocks/vault/lookup-self.json", "/mocks/vault/auth-mounts.json" }) - public void createPipelineEntityAliasTest(String secretConfigJson, String lookupSelfResponse, String authMountsResponse) throws VaultException, IOException, InterruptedException { + public void createPipelineEntityAliasTest(String secretConfigJson, String lookupSelfResponse, String authMountsResponse) throws VaultException, IOException, InterruptedException, APIException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); mockWebServer.enqueue(new MockResponse().setBody(lookupSelfResponse)); @@ -113,7 +116,7 @@ public void createPipelineEntityAliasTest(String secretConfigJson, String lookup "/secret-config-oidc.json", "/mocks/vault/auth-token.json" }) - public void assumePipelineTest(String secretConfigJson, String authTokenResponse) throws VaultException, IOException, InterruptedException { + public void assumePipelineTest(String secretConfigJson, String authTokenResponse) throws VaultException, IOException, InterruptedException, APIException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); mockWebServer.enqueue(new MockResponse().setBody(authTokenResponse)); @@ -144,7 +147,7 @@ public void assumePipelineTest(String secretConfigJson, String authTokenResponse "/secret-config-oidc.json", "/mocks/vault/oidc-token.json" }) - public void oidcTokenTest(String secretConfigJson, String oidcTokenResponse) throws VaultException, IOException, InterruptedException { + public void oidcTokenTest(String secretConfigJson, String oidcTokenResponse) throws VaultException, IOException, InterruptedException, APIException { SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); mockWebServer.enqueue(new MockResponse().setBody(oidcTokenResponse)); @@ -170,10 +173,9 @@ public void oidcTokenTest(String secretConfigJson, String oidcTokenResponse) thr @NotNull private String extractBodyAsString(RecordedRequest request) { - String body = new BufferedReader(new InputStreamReader(request.getBody().inputStream(), StandardCharsets.UTF_8)) + return new BufferedReader(new InputStreamReader(request.getBody().inputStream(), StandardCharsets.UTF_8)) .lines() .collect(Collectors.joining("")); - return body; } diff --git a/src/test/resources/mocks/gocd/pipeline-config-scm.json b/src/test/resources/mocks/gocd/pipeline-config-scm.json new file mode 100644 index 0000000..90136ea --- /dev/null +++ b/src/test/resources/mocks/gocd/pipeline-config-scm.json @@ -0,0 +1,96 @@ +{ + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/pipelines/some-pipeline-pr" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#pipeline-config" + }, + "find": { + "href": "https://gocd.com/go/api/admin/pipelines/:pipeline_name" + } + }, + "label_template": "${COUNT}-${git[:7]}", + "lock_behavior": "none", + "name": "some-pipeline", + "template": null, + "group": "dev", + "origin": { + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/config_repos/some-pipeline" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#config-repos" + }, + "find": { + "href": "https://gocd.com/go/api/admin/config_repos/:id" + } + }, + "type": "config_repo", + "id": "some-pipeline" + }, + "parameters": [], + "environment_variables": [ + { + "secure": false, + "name": "OIDC_TOKEN", + "value": "{{SECRET:[vault][some-pipeline]}}" + } + ], + "materials": [ + { + "type": "plugin", + "attributes": { + "ref": "some-repository-pr", + "filter": null, + "destination": null, + "invert_filter": false + } + } + ], + "stages": [ + { + "name": "test", + "fetch_materials": true, + "clean_working_directory": false, + "never_cleanup_artifacts": false, + "approval": { + "type": "success", + "allow_only_on_success": false, + "authorization": { + "roles": [], + "users": [] + } + }, + "environment_variables": [], + "jobs": [ + { + "name": "test", + "run_instance_count": null, + "timeout": null, + "environment_variables": [], + "resources": [ + "build" + ], + "tasks": [ + { + "type": "exec", + "attributes": { + "run_if": [ + "passed" + ], + "command": "/opt/script.sh", + "args": "" + } + } + ], + "tabs": [], + "artifacts": [] + } + ] + } + ], + "tracking_tool": null, + "timer": null +} \ No newline at end of file diff --git a/src/test/resources/mocks/gocd/pipeline-config.json b/src/test/resources/mocks/gocd/pipeline-config.json new file mode 100644 index 0000000..ee4a624 --- /dev/null +++ b/src/test/resources/mocks/gocd/pipeline-config.json @@ -0,0 +1,101 @@ +{ + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/pipelines/some-pipeline" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#pipeline-config" + }, + "find": { + "href": "https://gocd.com/go/api/admin/pipelines/:pipeline_name" + } + }, + "label_template": "${COUNT}-${git[:7]}", + "lock_behavior": "none", + "name": "some-pipeline", + "template": null, + "group": "dev", + "origin": { + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/config_repos/some-pipeline" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#config-repos" + }, + "find": { + "href": "https://gocd.com/go/api/admin/config_repos/:id" + } + }, + "type": "config_repo", + "id": "some-pipeline" + }, + "parameters": [], + "environment_variables": [ + { + "secure": false, + "name": "OIDC_TOKEN", + "value": "{{SECRET:[vault][some-pipeline]}}" + } + ], + "materials": [ + { + "type": "git", + "attributes": { + "url": "git@github.com:important-organization/some-repository.git", + "destination": null, + "filter": null, + "invert_filter": false, + "name": "git", + "auto_update": false, + "branch": "main", + "submodule_folder": null, + "shallow_clone": false + } + } + ], + "stages": [ + { + "name": "test", + "fetch_materials": true, + "clean_working_directory": false, + "never_cleanup_artifacts": false, + "approval": { + "type": "success", + "allow_only_on_success": false, + "authorization": { + "roles": [], + "users": [] + } + }, + "environment_variables": [], + "jobs": [ + { + "name": "test", + "run_instance_count": null, + "timeout": null, + "environment_variables": [], + "resources": [ + "build" + ], + "tasks": [ + { + "type": "exec", + "attributes": { + "run_if": [ + "passed" + ], + "command": "/opt/script.sh", + "args": "" + } + } + ], + "tabs": [], + "artifacts": [] + } + ] + } + ], + "tracking_tool": null, + "timer": null +} \ No newline at end of file diff --git a/src/test/resources/mocks/gocd/scm-response.json b/src/test/resources/mocks/gocd/scm-response.json new file mode 100644 index 0000000..a5f45aa --- /dev/null +++ b/src/test/resources/mocks/gocd/scm-response.json @@ -0,0 +1,37 @@ +{ + "_links": { + "doc": { + "href": "https://api.gocd.org/22.1.0/#scms" + }, + "self": { + "href": "https://ci.dev.s-cloud.net/go/api/admin/scms/some-pipeline-pr" + }, + "find": { + "href": "https://ci.dev.s-cloud.net/go/api/admin/scms/:material_name" + } + }, + "id": "some-repository-pr", + "name": "some-repository-pr", + "auto_update": false, + "origin": { + "_links": { + "self": { + "href": "https://ci.dev.s-cloud.net/go/admin/config_xml" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#get-configuration" + } + }, + "type": "gocd" + }, + "plugin_metadata": { + "id": "github.pr", + "version": "1" + }, + "configuration": [ + { + "key": "url", + "value": "git@github.com:important-organization/some-repository" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/secret-config-metadata.json b/src/test/resources/secret-config-metadata.json index dafbb3a..b9e54c8 100644 --- a/src/test/resources/secret-config-metadata.json +++ b/src/test/resources/secret-config-metadata.json @@ -134,5 +134,29 @@ "required": false, "secure": true } + }, + { + "key": "GoCDServerUrl", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } + }, + { + "key": "GoCDUsername", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } + }, + { + "key": "GoCDPassword", + "metadata": { + "display_name": "", + "required": false, + "secure": true + } } ] diff --git a/src/test/resources/secret-config-oidc.json b/src/test/resources/secret-config-oidc.json index a7bf649..c9a9433 100644 --- a/src/test/resources/secret-config-oidc.json +++ b/src/test/resources/secret-config-oidc.json @@ -7,5 +7,8 @@ "AuthMethod": "token", "SecretEngine": "oidc", "PipelineTokenAuthBackendRole": "some-backend-role", - "PipelinePolicy": "some-policy" + "PipelinePolicy": "some-policy", + "GoCDServerUrl": "someURL", + "GoCDUsername": "username", + "GoCDPassword": "supersecret" } From 8bba83212ae6ab0b08879ae53bb7f49a2209d7ae Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Fri, 25 Mar 2022 17:51:06 +0100 Subject: [PATCH 05/11] moved to entity identities with metadata instead of entity alias --- build.gradle | 2 +- .../vault/{gocd => api}/GoCDPipelineApi.java | 5 +- .../secretmanager/vault/api/VaultApi.java | 50 +++++ .../secretmanager/vault/api/VaultAuthApi.java | 75 +++++++ .../vault/api/VaultIdentityApi.java | 149 +++++++++++++ .../secretmanager/vault/api/VaultSysApi.java | 62 ++++++ .../vault/models/SecretConfig.java | 19 +- ...elineConfigMaterialAttributesResponse.java | 42 ++++ .../gocd/PipelineConfigMaterialResponse.java | 42 ++++ .../request/gocd/PipelineConfigResponse.java | 43 ---- .../vault/request/gocd/SCMConfiguration.java | 42 ++++ .../vault/request/gocd/SCMResponse.java | 22 -- .../request/vault/EntityAliasRequest.java | 7 +- .../request/vault/EntityDataResponse.java | 42 ++++ .../vault/request/vault/EntityRequest.java | 44 ++++ .../vault/request/vault/EntityResponse.java | 34 +++ ...adataRequest.java => MetadataRequest.java} | 6 +- .../OIDCPipelineIdentityProvider.java | 176 +++------------ src/main/resources/secrets.template.html | 8 +- .../gocd/secretmanager/vault/TestUtils.java | 34 +++ .../{gocd => api}/GoCDPipelineApiTest.java | 4 +- .../vault/api/VaultAuthApiTest.java | 92 ++++++++ .../vault/api/VaultIdentityApiTest.java | 201 ++++++++++++++++++ .../vault/api/VaultSysApiTest.java | 85 ++++++++ src/test/resources/mocks/vault/entity.json | 32 +++ .../resources/mocks/vault/lookup-self.json | 23 -- .../resources/secret-config-metadata.json | 8 + 27 files changed, 1096 insertions(+), 253 deletions(-) rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/{gocd => api}/GoCDPipelineApi.java (95%) create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultApi.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApi.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApi.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApi.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMConfiguration.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityDataResponse.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityRequest.java create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityResponse.java rename src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/{CustomMetadataRequest.java => MetadataRequest.java} (88%) create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/TestUtils.java rename src/test/java/com/thoughtworks/gocd/secretmanager/vault/{gocd => api}/GoCDPipelineApiTest.java (97%) create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApiTest.java create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApiTest.java create mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApiTest.java create mode 100644 src/test/resources/mocks/vault/entity.json delete mode 100644 src/test/resources/mocks/vault/lookup-self.json diff --git a/build.gradle b/build.gradle index 2442739..ab6e04b 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ gocdPlugin { goCdVersion = '20.9.0' name = 'Vault secret manager plugin' description = 'The plugin allows to use hashicorp vault as secret manager for the GoCD server' - vendorName = 'ThoughtWorks, Inc.' + vendorName = 'asdThoughtWorks, Inc.' vendorUrl = 'https://github.com/gocd-private/gocd-vault-secret-plugin' githubRepo { diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java similarity index 95% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java index c84e1cd..bc62d6a 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApi.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java @@ -14,13 +14,14 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.gocd; +package com.thoughtworks.gocd.secretmanager.vault.api; import cd.go.plugin.base.GsonTransformer; import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.gocd.PipelineConfigMaterialResponse; import com.thoughtworks.gocd.secretmanager.vault.request.gocd.PipelineConfigResponse; import com.thoughtworks.gocd.secretmanager.vault.request.gocd.SCMResponse; import okhttp3.MediaType; @@ -61,7 +62,7 @@ public PipelineMaterial fetchPipelineMaterial(String pipeline) throws APIExcepti } // Even if there are multiple pipeline definitions, we will always use the first found material. - PipelineConfigResponse.PipelineConfigMaterialResponse pipelineConfigMaterialResponse = pipelineConfigResponse.getMaterials().get(0); + PipelineConfigMaterialResponse pipelineConfigMaterialResponse = pipelineConfigResponse.getMaterials().get(0); String materialType = pipelineConfigMaterialResponse.getType(); String repositoryUrl = pipelineConfigMaterialResponse.getAttributes().getUrl(); if(materialType.equalsIgnoreCase("plugin")) { diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultApi.java new file mode 100644 index 0000000..a5bc6c4 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultApi.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import com.bettercloud.vault.VaultConfig; +import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import okhttp3.OkHttpClient; + +public class VaultApi { + + private final VaultConfig vaultConfig; + private final SecretConfig secretConfig; + private final OkHttpClient vaultClient; + + public static final String X_VAULT_TOKEN = "X-Vault-Token"; + + + public VaultApi(VaultConfig vaultConfig, SecretConfig secretConfig) { + this.vaultConfig = vaultConfig; + this.secretConfig = secretConfig; + this.vaultClient = new OkHTTPClientFactory().vault(secretConfig); + } + + public VaultAuthApi auth() { + return new VaultAuthApi(secretConfig, vaultConfig, vaultClient); + } + + public VaultIdentityApi identity() { + return new VaultIdentityApi(vaultConfig, vaultClient); + } + + public VaultSysApi sys() { + return new VaultSysApi(vaultConfig, vaultClient); + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApi.java new file mode 100644 index 0000000..b7cae10 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApi.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.VaultConfig; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.CreateTokenRequest; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.TokenResponse; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import java.io.IOException; +import java.util.List; + +import static cd.go.plugin.base.GsonTransformer.toJson; +import static com.thoughtworks.gocd.secretmanager.vault.api.VaultApi.X_VAULT_TOKEN; +import static com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory.CONTENT_TYPE_JSON; + +public class VaultAuthApi { + + private final SecretConfig secretConfig; + private final VaultConfig vaultConfig; + private final OkHttpClient client; + + public VaultAuthApi(SecretConfig secretConfig, VaultConfig vaultConfig, OkHttpClient client) { + this.secretConfig = secretConfig; + this.vaultConfig = vaultConfig; + this.client = client; + } + + public String assumePipeline(String pipelineTokenAuthBackendRole, List pipelinePolicies, String entityAliasName) throws APIException { + CreateTokenRequest createTokenRequest = new CreateTokenRequest( + pipelineTokenAuthBackendRole, + pipelinePolicies.isEmpty() ? null : secretConfig.getPipelinePolicy(), + entityAliasName + ); + + RequestBody body = RequestBody.create(toJson(createTokenRequest), CONTENT_TYPE_JSON); + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/auth/token/create/" + secretConfig.getPipelineTokenAuthBackendRole()) + .post(body) + .build(); + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException("Could not create pipeline token. Due to: " + response.body().string(), response.code()); + } + + TokenResponse tokenResponse = GsonTransformer.fromJson(response.body().string(), TokenResponse.class); + return tokenResponse.getAuth().getClientToken(); + } catch (IOException e) { + throw new APIException(e); + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApi.java new file mode 100644 index 0000000..3a0e936 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApi.java @@ -0,0 +1,149 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.VaultConfig; +import com.google.gson.reflect.TypeToken; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.*; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; + +import static cd.go.plugin.base.GsonTransformer.toJson; +import static com.thoughtworks.gocd.secretmanager.vault.api.VaultApi.X_VAULT_TOKEN; +import static com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory.CONTENT_TYPE_JSON; + +public class VaultIdentityApi { + + private final VaultConfig vaultConfig; + private final OkHttpClient client; + + public VaultIdentityApi(VaultConfig vaultConfig, OkHttpClient client) { + this.vaultConfig = vaultConfig; + this.client = client; + } + + public void createPipelineEntityAlias(String entityId, String mountAccessor, String entityAliasName) throws APIException { + + EntityAliasRequest entityAliasRequest = new EntityAliasRequest( + entityAliasName, + entityId, + mountAccessor + ); + + RequestBody body = RequestBody.create(toJson(entityAliasRequest), CONTENT_TYPE_JSON); + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/identity/entity-alias") + .post(body) + .build(); + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException("Could not create entity alias. Due to: " + response.body().string(), response.code()); + } + } catch (IOException e) { + throw new APIException(e); + } + } + + public String oidcToken(String pipelineAuthToken, String path) throws APIException { + + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, pipelineAuthToken) + .url(vaultConfig.getAddress() + path) + .get() + .build(); + + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException("Could not read OIDC token. Due to: " + response.body().string(), response.code()); + } + + Type type = new TypeToken>() {}.getType(); + DataResponse dataResponse = GsonTransformer.fromJson(response.body().string(), type); + return dataResponse.getData().getToken(); + } catch (IOException e) { + throw new APIException(e); + } + } + + public Optional createPipelineEntity(String entityName, List policies, PipelineMaterial pipelineMaterial) throws APIException { + EntityRequest entityRequest = new EntityRequest( + entityName, + policies, + new MetadataRequest(pipelineMaterial) + ); + + RequestBody body = RequestBody.create(toJson(entityRequest), CONTENT_TYPE_JSON); + + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/identity/entity/name/" + entityName) + .post(body) + .build(); + + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException(String.format("Could not create entity [%s]. Due to: %s", entityName, response.body().string()), response.code()); + } + + if(response.code() == 200) { + return Optional.of(GsonTransformer.fromJson(response.body().string(), EntityResponse.class)); + } else { + // In case the entity already existed it was now updated + return Optional.empty(); + } + } catch (IOException e) { + throw new APIException(e); + } + } + + public EntityResponse fetchPipelineEntity(String pipelineEntityName) throws APIException { + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/identity/entity/name/" + pipelineEntityName) + .get() + .build(); + + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException(String.format("Could not create entity [%s]. Due to: %s", pipelineEntityName, response.body().string()), response.code()); + } + + return GsonTransformer.fromJson(response.body().string(), EntityResponse.class); + } catch (IOException e) { + throw new APIException(e); + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApi.java new file mode 100644 index 0000000..2e6323d --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApi.java @@ -0,0 +1,62 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.VaultConfig; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.AuthMountsResponse; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +import java.io.IOException; + +import static com.thoughtworks.gocd.secretmanager.vault.api.VaultApi.X_VAULT_TOKEN; + +public class VaultSysApi { + + private final VaultConfig vaultConfig; + private final OkHttpClient client; + + public VaultSysApi(VaultConfig vaultConfig, OkHttpClient client) { + this.vaultConfig = vaultConfig; + this.client = client; + } + + public String getAuthMountAccessor() throws APIException { + Request request = new Request.Builder() + .header(X_VAULT_TOKEN, vaultConfig.getToken()) + .url(vaultConfig.getAddress() + "/v1/sys/auth") + .get() + .build(); + + try { + Response response = client.newCall(request).execute(); + + if (response.code() < 200 || response.code() >= 300) { + throw new APIException("Could not fetch auth mounts own token. Due to: " + response.body().string(), response.code()); + } + + AuthMountsResponse authMountsResponse = GsonTransformer.fromJson(response.body().string(), AuthMountsResponse.class); + return authMountsResponse.getToken().getAccessor(); + } catch (IOException e) { + throw new APIException(e); + } + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java index fc8d086..b822b99 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/models/SecretConfig.java @@ -46,6 +46,7 @@ public class SecretConfig { public static final int DEFAULT_MAX_RETRIES = 0; public static final int DEFAULT_RETRY_INTERVAL_MS = 100; public static final String DEFAULT_SECRET_ENGINE = SECRET_ENGINE; + public static final String DEFAULT_ENTITY_NAME_PREFIX = "pipeline-identity"; @Expose @SerializedName("VaultUrl") @@ -67,6 +68,11 @@ public class SecretConfig { @Property(name = "PipelinePolicy") private String pipelinePolicy; + @Expose + @SerializedName("CustomEntityNamePrefix") + @Property(name = "CustomEntityNamePrefix") + private String customEntityNamePrefix; + @Expose @SerializedName("VaultPath") @Property(name = "VaultPath", required = true) @@ -230,7 +236,7 @@ public List getPipelinePolicy() { if (isBlank(pipelinePolicy)) { return new ArrayList<>(); } - return Arrays.asList(pipelinePolicy.split(",\\s*")); + return asList(pipelinePolicy.split(",\\s*")); } public String getGocdServerURL() { @@ -240,6 +246,13 @@ public String getGocdServerURL() { return gocdServerURL; } + public String getCustomEntityNamePrefix() { + if (isBlank(customEntityNamePrefix)) { + return DEFAULT_ENTITY_NAME_PREFIX; + } + return customEntityNamePrefix; + } + public String getGoCDUsername() { return gocdUsername; } @@ -284,7 +297,8 @@ public boolean equals(Object o) { Objects.equals(pipelineTokenAuthBackendRole, that.pipelineTokenAuthBackendRole) && Objects.equals(pipelinePolicy, that.pipelinePolicy) && Objects.equals(gocdUsername, that.gocdUsername) && - Objects.equals(gocdPassword, that.gocdPassword); + Objects.equals(gocdPassword, that.gocdPassword) && + Objects.equals(customEntityNamePrefix, that.customEntityNamePrefix); } @@ -308,4 +322,5 @@ public boolean isCertAuthentication() { public boolean isOIDCSecretEngine() { return OIDC_ENGINE.equalsIgnoreCase(secretEngine); } + } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java new file mode 100644 index 0000000..b512124 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.gocd; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class PipelineConfigMaterialAttributesResponse { + + @Expose + @SerializedName("url") + private String url; + + @Expose + @SerializedName("branch") + private String branch; + + public PipelineConfigMaterialAttributesResponse() { + } + + public String getUrl() { + return url; + } + + public String getBranch() { + return branch; + } + } \ No newline at end of file diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialResponse.java new file mode 100644 index 0000000..fd641df --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.gocd; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class PipelineConfigMaterialResponse { + + @Expose + @SerializedName("type") + private String type; + + @Expose + @SerializedName("attributes") + private PipelineConfigMaterialAttributesResponse attributes; + + public PipelineConfigMaterialResponse() { + } + + public String getType() { + return type; + } + + public PipelineConfigMaterialAttributesResponse getAttributes() { + return attributes; + } + } \ No newline at end of file diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java index 5629b54..7859fb7 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java @@ -50,47 +50,4 @@ public List getMaterials() { return materials; } - public class PipelineConfigMaterialResponse { - - @Expose - @SerializedName("type") - private String type; - - @Expose - @SerializedName("attributes") - private PipelineConfigMaterialAttributesResponse attributes; - - public PipelineConfigMaterialResponse() { - } - - public String getType() { - return type; - } - - public PipelineConfigMaterialAttributesResponse getAttributes() { - return attributes; - } - } - - public class PipelineConfigMaterialAttributesResponse { - - @Expose - @SerializedName("url") - private String url; - - @Expose - @SerializedName("branch") - private String branch; - - public PipelineConfigMaterialAttributesResponse() { - } - - public String getUrl() { - return url; - } - - public String getBranch() { - return branch; - } - } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMConfiguration.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMConfiguration.java new file mode 100644 index 0000000..1da6195 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.gocd; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class SCMConfiguration { + + @Expose + @SerializedName("key") + private String key; + + @Expose + @SerializedName("value") + private String value; + + public SCMConfiguration() { + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } + } \ No newline at end of file diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java index cc2a229..ae4177d 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.java @@ -32,26 +32,4 @@ public SCMResponse() { public List getConfigurations() { return configurations; } - - public class SCMConfiguration { - - @Expose - @SerializedName("key") - private String key; - - @Expose - @SerializedName("value") - private String value; - - public SCMConfiguration() { - } - - public String getKey() { - return key; - } - - public String getValue() { - return value; - } - } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java index 3aa384a..e90daf9 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.java @@ -33,14 +33,9 @@ public class EntityAliasRequest { @SerializedName("mount_accessor") private String mountAccessor; - @Expose - @SerializedName("custom_metadata") - private CustomMetadataRequest customMetadata; - - public EntityAliasRequest(String name, String canonicalId, String mountAccessor, CustomMetadataRequest customMetadata) { + public EntityAliasRequest(String name, String canonicalId, String mountAccessor) { this.name = name; this.canonicalId = canonicalId; this.mountAccessor = mountAccessor; - this.customMetadata = customMetadata; } } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityDataResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityDataResponse.java new file mode 100644 index 0000000..38868d7 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityDataResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.vault; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class EntityDataResponse { + + @Expose + @SerializedName("id") + private String id; + + @Expose + @SerializedName("name") + private String name; + + public EntityDataResponse() { + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityRequest.java new file mode 100644 index 0000000..28ac349 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.vault; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.Arrays; +import java.util.List; + +public class EntityRequest { + + @Expose + @SerializedName("name") + private String name; + + @Expose + @SerializedName("policies") + private List policies; + + @Expose + @SerializedName("metadata") + private MetadataRequest metadata; + + public EntityRequest(String name, List policies, MetadataRequest metadata) { + this.name = name; + this.policies = policies; + this.metadata = metadata; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityResponse.java new file mode 100644 index 0000000..2bd0a39 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.vault; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class EntityResponse { + + @Expose + @SerializedName("data") + private EntityDataResponse data; + + public EntityResponse() { + } + + public EntityDataResponse getData() { + return data; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CustomMetadataRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/MetadataRequest.java similarity index 88% rename from src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CustomMetadataRequest.java rename to src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/MetadataRequest.java index 1a28f16..34266e7 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CustomMetadataRequest.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/MetadataRequest.java @@ -20,7 +20,7 @@ import com.google.gson.annotations.SerializedName; import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; -public class CustomMetadataRequest { +public class MetadataRequest { @Expose @SerializedName("group") @@ -42,7 +42,7 @@ public class CustomMetadataRequest { @SerializedName("branch") private String branch; - public CustomMetadataRequest(String group, String pipeline, String repository, String organization, String branch) { + public MetadataRequest(String group, String pipeline, String repository, String organization, String branch) { this.group = group; this.pipeline = pipeline; this.repository = repository; @@ -50,7 +50,7 @@ public CustomMetadataRequest(String group, String pipeline, String repository, S this.branch = branch; } - public CustomMetadataRequest(PipelineMaterial pipelineMaterial) { + public MetadataRequest(PipelineMaterial pipelineMaterial) { this( pipelineMaterial.getGroup(), pipelineMaterial.getName(), diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java index 596d98f..9aab60a 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java @@ -16,179 +16,67 @@ package com.thoughtworks.gocd.secretmanager.vault.secretengines; -import cd.go.plugin.base.GsonTransformer; import com.bettercloud.vault.Vault; import com.bettercloud.vault.VaultConfig; -import com.google.gson.reflect.TypeToken; -import com.thoughtworks.gocd.secretmanager.vault.gocd.GoCDPipelineApi; -import com.thoughtworks.gocd.secretmanager.vault.http.DataResponseExtractor; -import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.api.GoCDPipelineApi; +import com.thoughtworks.gocd.secretmanager.vault.api.VaultApi; import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; -import com.thoughtworks.gocd.secretmanager.vault.request.vault.*; -import okhttp3.*; -import java.io.IOException; -import java.lang.reflect.Type; import java.util.Optional; -import static cd.go.plugin.base.GsonTransformer.toJson; -import static com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory.CONTENT_TYPE_JSON; - public class OIDCPipelineIdentityProvider extends SecretEngine { - private final OkHttpClient client; - private final DataResponseExtractor dataResponseExtractor; - private final GoCDPipelineApi gocdPipelineApi; - private VaultConfig vaultConfig; + private final GoCDPipelineApi gocd; + private final VaultApi vault; private SecretConfig secretConfig; - private static final String X_VAULT_TOKEN = "X-Vault-Token"; - public OIDCPipelineIdentityProvider(Vault vault, VaultConfig vaultConfig, SecretConfig secretConfig) { super(vault); - this.vaultConfig = vaultConfig; this.secretConfig = secretConfig; - this.gocdPipelineApi = new GoCDPipelineApi(secretConfig); - this.dataResponseExtractor = new DataResponseExtractor(); + this.gocd = new GoCDPipelineApi(secretConfig); + this.vault = new VaultApi(vaultConfig, secretConfig); + } - OkHTTPClientFactory okHTTPClientFactory = new OkHTTPClientFactory(); - this.client = okHTTPClientFactory.vault(secretConfig); + // Test usage + public OIDCPipelineIdentityProvider(Vault vault, SecretConfig secretConfig, GoCDPipelineApi gocd, VaultApi vaultAPI) { + super(vault); + this.secretConfig = secretConfig; + this.gocd = gocd; + this.vault = vaultAPI; } + @Override public Optional getSecret(String path, String pipelineName) throws APIException { - PipelineMaterial pipelineMaterial = gocdPipelineApi.fetchPipelineMaterial(pipelineName); - CustomMetadataRequest customMetadataRequest = new CustomMetadataRequest(pipelineMaterial); + PipelineMaterial pipelineMaterial = gocd.fetchPipelineMaterial(pipelineName); - createPipelineEntityAlias(customMetadataRequest); - - String pipelineAuthToken = assumePipeline(customMetadataRequest.getPipeline()); - return Optional.of(oidcToken(pipelineAuthToken, path)); - } + String entityId = vault.identity().createPipelineEntity(entityName(pipelineName), secretConfig.getPipelinePolicy(), pipelineMaterial) + .orElse(vault.identity().fetchPipelineEntity(entityName(pipelineName))) + .getData() + .getId(); - protected void createPipelineEntityAlias(CustomMetadataRequest customMetadataRequest) throws APIException { - String entityId = getEntityId(); - String mountAccessor = getMountAccessor(); + String authMountAccessor = vault.sys().getAuthMountAccessor(); + vault.identity().createPipelineEntityAlias(entityId, authMountAccessor, entityAliasName(pipelineName)); - EntityAliasRequest entityAliasRequest = new EntityAliasRequest( - entityAliasName(customMetadataRequest.getPipeline()), - entityId, - mountAccessor, - customMetadataRequest - ); - - RequestBody body = RequestBody.create(toJson(entityAliasRequest), CONTENT_TYPE_JSON); - Request request = new Request.Builder() - .header(X_VAULT_TOKEN, vaultConfig.getToken()) - .url(vaultConfig.getAddress() + "/v1/identity/entity-alias") - .post(body) - .build(); - try { - Response response = client.newCall(request).execute(); - - if (response.code() < 200 || response.code() >= 300) { - throw new APIException("Could not create entity alias. Due to: " + response.body().string(), response.code()); - } - } catch (IOException e) { - throw new APIException(e); - } + String pipelineAuthToken = vault.auth().assumePipeline(secretConfig.getPipelineTokenAuthBackendRole(), secretConfig.getPipelinePolicy(), entityAliasName(pipelineName)); + return Optional.of(vault.identity().oidcToken(pipelineAuthToken, path)); } - private String getMountAccessor() throws APIException { - Request request = new Request.Builder() - .header(X_VAULT_TOKEN, vaultConfig.getToken()) - .url(vaultConfig.getAddress() + "/v1/sys/auth") - .get() - .build(); - - try { - Response response = client.newCall(request).execute(); - - if (response.code() < 200 || response.code() >= 300) { - throw new APIException("Could not fetch auth mounts own token. Due to: " + response.body().string(), response.code()); - } - - AuthMountsResponse authMountsResponse = GsonTransformer.fromJson(response.body().string(), AuthMountsResponse.class); - return authMountsResponse.getToken().getAccessor(); - } catch (IOException e) { - throw new APIException(e); - } + private String entityAliasName(String pipelineName) { + return entityName(pipelineName, "-entity-alias"); } - private String getEntityId() throws APIException { - Request request = new Request.Builder() - .header(X_VAULT_TOKEN, vaultConfig.getToken()) - .url(vaultConfig.getAddress() + "/v1/auth/token/lookup-self") - .get() - .build(); - - try { - Response response = client.newCall(request).execute(); - - if (response.code() < 200 || response.code() >= 300) { - throw new APIException("Could not lookup own token. Due to: " + response.body().string(), response.code()); - } - - LookupResponse lookupResponse = dataResponseExtractor.extract(response, LookupResponse.class); - return lookupResponse.getEntityId(); - } catch (IOException e) { - throw new APIException(e); - } + private String entityName(String pipelineName) { + return entityName(pipelineName, ""); } - protected String assumePipeline(String pipelineName) throws APIException { - CreateTokenRequest createTokenRequest = new CreateTokenRequest( - secretConfig.getPipelineTokenAuthBackendRole(), - secretConfig.getPipelinePolicy().isEmpty() ? null : secretConfig.getPipelinePolicy(), - entityAliasName(pipelineName) + private String entityName(String pipelineName, String additionalPrefix) { + return String.format("%s%s-%s", + secretConfig.getCustomEntityNamePrefix(), + additionalPrefix, + pipelineName.toLowerCase().replaceAll("\\s+", "-") ); - - RequestBody body = RequestBody.create(toJson(createTokenRequest), CONTENT_TYPE_JSON); - Request request = new Request.Builder() - .header(X_VAULT_TOKEN, vaultConfig.getToken()) - .url(vaultConfig.getAddress() + "/v1/auth/token/create/" + secretConfig.getPipelineTokenAuthBackendRole()) - .post(body) - .build(); - try { - Response response = client.newCall(request).execute(); - - if (response.code() < 200 || response.code() >= 300) { - throw new APIException("Could not create pipeline token. Due to: " + response.body().string(), response.code()); - } - - TokenResponse tokenResponse = GsonTransformer.fromJson(response.body().string(), TokenResponse.class); - return tokenResponse.getAuth().getClientToken(); - } catch (IOException e) { - throw new APIException(e); - } - } - - protected String oidcToken(String pipelineAuthToken, String path) throws APIException { - - Request request = new Request.Builder() - .header(X_VAULT_TOKEN, pipelineAuthToken) - .url(vaultConfig.getAddress() + path) - .get() - .build(); - - try { - Response response = client.newCall(request).execute(); - - if (response.code() < 200 || response.code() >= 300) { - throw new APIException("Could not read OIDC token. Due to: " + response.body().string(), response.code()); - } - - Type type = new TypeToken>() {}.getType(); - DataResponse dataResponse = GsonTransformer.fromJson(response.body().string(), type); - return dataResponse.getData().getToken(); - } catch (IOException e) { - throw new APIException(e); - } - } - - private String entityAliasName(String pipelineName) { - return String.format("gocd-pipeline-dev-test-%s", pipelineName.toLowerCase().replaceAll("\\s+", "-")); } } diff --git a/src/main/resources/secrets.template.html b/src/main/resources/secrets.template.html index 8f5efdf..8749ab1 100755 --- a/src/main/resources/secrets.template.html +++ b/src/main/resources/secrets.template.html @@ -147,7 +147,7 @@
- @@ -162,13 +162,13 @@
- + {{ GOINPUTNAME[VaultPath].$error.server }} -

+

This should be the path which holds the secrets. The plugin reads secrets from a single path.

-

+

This should be the path which holds the OIDC token. Usually this starts with '/v1/identity/oidc/token/...'

diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/TestUtils.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/TestUtils.java new file mode 100644 index 0000000..29316f4 --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/TestUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault; + +import okhttp3.mockwebserver.RecordedRequest; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + +public class TestUtils { + + public static String extractBodyAsString(RecordedRequest request) { + return new BufferedReader(new InputStreamReader(request.getBody().inputStream(), StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.joining("")); + } +} diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java similarity index 97% rename from src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java rename to src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java index e112924..7b613e0 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/gocd/GoCDPipelineApiTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.thoughtworks.gocd.secretmanager.vault.gocd; +package com.thoughtworks.gocd.secretmanager.vault.api; import cd.go.plugin.base.GsonTransformer; import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; @@ -27,14 +27,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; -import org.mockito.Mockito; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; class GoCDPipelineApiTest { diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApiTest.java new file mode 100644 index 0000000..fbe9e83 --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultAuthApiTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.OIDCPipelineIdentityProvider; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +import static com.thoughtworks.gocd.secretmanager.vault.TestUtils.extractBodyAsString; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.*; + +class VaultAuthApiTest { + + private MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + } + + @AfterEach + public void cleanup() throws IOException { + mockWebServer.shutdown(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/auth-token.json" + }) + public void assumePipelineTest(String secretConfigJson, String authTokenResponse) throws VaultException, IOException, InterruptedException, APIException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + + mockWebServer.enqueue(new MockResponse().setBody(authTokenResponse)); + mockWebServer.start(); + + VaultConfig vaultConfig = new VaultConfig() + .address(mockWebServer.url("").url().toString()) + .token("some-token") + .build(); + VaultAuthApi vaultAuthApi = new VaultAuthApi( + secretConfig, + vaultConfig, + new OkHTTPClientFactory().vault(secretConfig) + ); + + String pipelineToken = vaultAuthApi.assumePipeline("some-backend-role", Lists.list("some-policy"), "some_pipelinename"); + assertThat(pipelineToken).isEqualTo("s.wOrq9dO9kzOcuvB06CMviJhZ"); + + RecordedRequest tokenCreation = mockWebServer.takeRequest(); + assertThat(tokenCreation.getPath()).isEqualTo("/v1/auth/token/create/some-backend-role"); + assertThat(tokenCreation.getMethod()).isEqualTo("POST"); + + String body = extractBodyAsString(tokenCreation); + assertThat(body).isEqualTo("{\"role_name\":\"some-backend-role\",\"policies\":[\"some-policy\"],\"entity_alias\":\"some_pipelinename\"}"); + } + +} \ No newline at end of file diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApiTest.java new file mode 100644 index 0000000..5a21278 --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultIdentityApiTest.java @@ -0,0 +1,201 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.EntityDataResponse; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.EntityResponse; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.MetadataRequest; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.OIDCPipelineIdentityProvider; +import kotlin.Metadata; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.assertj.core.util.Lists; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; + +import java.io.IOException; +import java.util.Optional; + +import static com.thoughtworks.gocd.secretmanager.vault.TestUtils.extractBodyAsString; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.Mockito.mock; + +public class VaultIdentityApiTest { + + private MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + } + + @AfterEach + public void cleanup() throws IOException { + mockWebServer.shutdown(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json" + }) + public void createPipelineEntityAliasTest(String secretConfigJson) throws VaultException, IOException, InterruptedException, APIException { + mockWebServer.enqueue(new MockResponse().setResponseCode(204)); + + VaultIdentityApi vaultIdentityApi = initVaultIdentityApi(secretConfigJson); + + vaultIdentityApi.createPipelineEntityAlias("7d2e3179-f69b-450c-7179-ac8ee8bd8ca9", "auth_token_12ac23", "some-entity-alias-name"); + + RecordedRequest createEntityAlias = mockWebServer.takeRequest(); + assertThat(createEntityAlias.getPath()).isEqualTo("/v1/identity/entity-alias"); + assertThat(createEntityAlias.getMethod()).isEqualTo("POST"); + + String body = extractBodyAsString(createEntityAlias); + + assertThat(body).isEqualToNormalizingWhitespace("{\"name\":\"some-entity-alias-name\",\"canonical_id\":\"7d2e3179-f69b-450c-7179-ac8ee8bd8ca9\",\"mount_accessor\":\"auth_token_12ac23\"}"); + } + + + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/oidc-token.json" + }) + public void oidcTokenTest(String secretConfigJson, String oidcTokenResponse) throws VaultException, IOException, InterruptedException, APIException { + mockWebServer.enqueue(new MockResponse().setBody(oidcTokenResponse)); + + VaultIdentityApi vaultIdentityApi = initVaultIdentityApi(secretConfigJson); + + String oidcToken = vaultIdentityApi.oidcToken("some_token", "/v1/identity/oidc/token/some-oidc-backend"); + assertThat(oidcToken).isEqualTo("eyJhbGciOiJSUzI1NiIsImtpZCI6IjJkMGI4YjlkLWYwNGQtNzFlYy1iNjc0LWM3MzU4NDMyYmM1YiJ9.eyJhdWQiOiJQNkNmQ3p5SHNRWTRwTWNBNmtXQU9DSXRBNyIsImV4cCI6MTU2MTQ4ODQxMiwiaWF0IjoxNTYxNDAyMDEyLCJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tOjEyMzQiLCJzdWIiOiI2YzY1ZWFmNy1kNGY0LTEzMzMtMDJiYy0xYzc1MjE5YzMxMDIifQ.IcbWTmks7P5eVtwmIBl5rL1B88MI55a9JJuYVLIlwE9aP_ilXpX5fE38CDm5PixDDVJb8TI2Q_FO4GMMH0ymHDO25ZvA917WcyHCSBGaQlgcS-WUL2fYTqFjSh-pezszaYBgPuGvH7hJjlTZO6g0LPCyUWat3zcRIjIQdXZum-OyhWAelQlveEL8sOG_ldyZ8v7fy7GXDxfJOK1kpw5AX9DXJKylbwZTBS8tLb-7edq8uZ0lNQyWy9VPEW_EEIZvGWy0AHua-Loa2l59GRRP8mPxuMYxH_c88x1lsSw0vH9E3rU8AXLyF3n4d40PASXEjZ-7dnIf4w4hf2P4L0xs_g"); + + RecordedRequest tokenCreation = mockWebServer.takeRequest(); + assertThat(tokenCreation.getPath()).isEqualTo("/v1/identity/oidc/token/some-oidc-backend"); + assertThat(tokenCreation.getMethod()).isEqualTo("GET"); + assertThat(tokenCreation.getHeader("X-Vault-Token")).isEqualTo("some_token"); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/entity.json" + }) + public void createPipelineEntityTestOnNewEntity(String secretConfig, String entityResponse) throws VaultException, IOException, APIException, InterruptedException { + PipelineMaterial metadata = new PipelineMaterial( + "some_pipelinename", + "some_group", + "some_organization", + "some_repository", + "some_branch" + ); + + + mockWebServer.enqueue(new MockResponse().setBody(entityResponse)); + + VaultIdentityApi vaultIdentityApi = initVaultIdentityApi(secretConfig); + + Optional pipelineEntity = vaultIdentityApi.createPipelineEntity("pipeline-identity-dev-test-pipeline", Lists.list("gocd-pipeline-policy-dev-test"), metadata); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo("POST"); + assertThat(recordedRequest.getPath()).isEqualTo("/v1/identity/entity/name/pipeline-identity-dev-test-pipeline"); + String recordedRequestString = extractBodyAsString(recordedRequest); + assertThat(recordedRequestString).isEqualTo("{\"name\":\"pipeline-identity-dev-test-pipeline\",\"policies\":[\"gocd-pipeline-policy-dev-test\"],\"metadata\":{\"group\":\"some_group\",\"pipeline\":\"some_pipelinename\",\"repository\":\"some_repository\",\"organization\":\"some_organization\",\"branch\":\"some_branch\"}}"); + + assertThat(pipelineEntity).isPresent(); + EntityDataResponse data = pipelineEntity.get().getData(); + assertThat(data.getName()).isEqualTo("pipeline-identity-dev-test-pipeline"); + assertThat(data.getId()).isEqualTo("1ab2dbd4-ff87-8291-5e08-56a0083424e1"); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json" + }) + public void createPipelineEntityTestOnExistingEntity(String secretConfig) throws VaultException, IOException, APIException, InterruptedException { + PipelineMaterial metadata = new PipelineMaterial( + "some_pipelinename", + "some_group", + "some_organization", + "some_repository", + "some_branch" + ); + + + mockWebServer.enqueue(new MockResponse().setResponseCode(204)); + + VaultIdentityApi vaultIdentityApi = initVaultIdentityApi(secretConfig); + + Optional pipelineEntity = vaultIdentityApi.createPipelineEntity("pipeline-identity-dev-test-pipeline", Lists.list("gocd-pipeline-policy-dev-test"), metadata); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo("POST"); + assertThat(recordedRequest.getPath()).isEqualTo("/v1/identity/entity/name/pipeline-identity-dev-test-pipeline"); + String recordedRequestString = extractBodyAsString(recordedRequest); + assertThat(recordedRequestString).isEqualTo("{\"name\":\"pipeline-identity-dev-test-pipeline\",\"policies\":[\"gocd-pipeline-policy-dev-test\"],\"metadata\":{\"group\":\"some_group\",\"pipeline\":\"some_pipelinename\",\"repository\":\"some_repository\",\"organization\":\"some_organization\",\"branch\":\"some_branch\"}}"); + + assertThat(pipelineEntity).isNotPresent(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/entity.json" + }) + public void getPipelineEntityTest(String secretConfig, String entityResponse) throws VaultException, IOException, APIException, InterruptedException { + mockWebServer.enqueue(new MockResponse().setBody(entityResponse)); + + VaultIdentityApi vaultIdentityApi = initVaultIdentityApi(secretConfig); + + EntityResponse pipelineEntity = vaultIdentityApi.fetchPipelineEntity("pipeline-identity-dev-test-pipeline"); + + RecordedRequest recordedRequest = mockWebServer.takeRequest(); + assertThat(recordedRequest.getMethod()).isEqualTo("GET"); + assertThat(recordedRequest.getPath()).isEqualTo("/v1/identity/entity/name/pipeline-identity-dev-test-pipeline"); + + assertThat(pipelineEntity.getData().getName()).isEqualTo("pipeline-identity-dev-test-pipeline"); + assertThat(pipelineEntity.getData().getId()).isEqualTo("1ab2dbd4-ff87-8291-5e08-56a0083424e1"); + } + + private VaultIdentityApi initVaultIdentityApi(String secretConfigJson) throws IOException, VaultException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + + mockWebServer.start(); + VaultConfig vaultConfig = new VaultConfig() + .address(mockWebServer.url("").url().toString()) + .token("some-token") + .build(); + + VaultIdentityApi vaultIdentityApi = new VaultIdentityApi( + vaultConfig, + new OkHTTPClientFactory().vault(secretConfig) + ); + return vaultIdentityApi; + } +} diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApiTest.java new file mode 100644 index 0000000..a77320a --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/VaultSysApiTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.api; + +import cd.go.plugin.base.GsonTransformer; +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.thoughtworks.gocd.secretmanager.vault.TestUtils; +import com.thoughtworks.gocd.secretmanager.vault.annotations.JsonSource; +import com.thoughtworks.gocd.secretmanager.vault.http.OkHTTPClientFactory; +import com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; +import com.thoughtworks.gocd.secretmanager.vault.models.SecretConfig; +import com.thoughtworks.gocd.secretmanager.vault.request.vault.MetadataRequest; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.OIDCPipelineIdentityProvider; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; + +import java.io.IOException; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class VaultSysApiTest { + + private MockWebServer mockWebServer; + + @BeforeEach + public void setup() { + mockWebServer = new MockWebServer(); + } + + @AfterEach + public void cleanup() throws IOException { + mockWebServer.shutdown(); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/vault/auth-mounts.json" + }) + public void createPipelineEntityAliasTest(String secretConfigJson, String authMountsResponse) throws VaultException, IOException, InterruptedException, APIException { + SecretConfig secretConfig = GsonTransformer.fromJson(secretConfigJson, SecretConfig.class); + + mockWebServer.enqueue(new MockResponse().setBody(authMountsResponse)); + mockWebServer.start(); + + VaultConfig vaultConfig = new VaultConfig() + .address(mockWebServer.url("").url().toString()) + .token("some-token") + .build(); + + VaultSysApi vaultSysApi = new VaultSysApi( + vaultConfig, + new OkHTTPClientFactory().vault(secretConfig) + ); + + String authMountAccessor = vaultSysApi.getAuthMountAccessor(); + + RecordedRequest authList = mockWebServer.takeRequest(); + assertThat(authList.getPath()).isEqualTo("/v1/sys/auth"); + assertThat(authList.getMethod()).isEqualTo("GET"); + + assertThat(authMountAccessor).isEqualTo("auth_token_12ac23"); + + } +} diff --git a/src/test/resources/mocks/vault/entity.json b/src/test/resources/mocks/vault/entity.json new file mode 100644 index 0000000..83e76c1 --- /dev/null +++ b/src/test/resources/mocks/vault/entity.json @@ -0,0 +1,32 @@ +{ + "request_id": "9fec9dfc-f5c1-303e-e73c-4520f38699b5", + "lease_id": "", + "renewable": false, + "lease_duration": 0, + "data": { + "aliases": [], + "creation_time": "2022-03-25T12:57:29.360263666Z", + "direct_group_ids": [], + "disabled": false, + "group_ids": [], + "id": "1ab2dbd4-ff87-8291-5e08-56a0083424e1", + "inherited_group_ids": [], + "last_update_time": "2022-03-25T12:58:48.788584567Z", + "merged_entity_ids": null, + "metadata": { + "branch": "some_branch", + "group": "some_group", + "organization": "some_org", + "pipeline": "some_pipeline", + "repository": "some_repository" + }, + "name": "pipeline-identity-dev-test-pipeline", + "namespace_id": "root", + "policies": [ + "gocd-pipeline-policy-dev-test" + ] + }, + "wrap_info": null, + "warnings": null, + "auth": null +} \ No newline at end of file diff --git a/src/test/resources/mocks/vault/lookup-self.json b/src/test/resources/mocks/vault/lookup-self.json deleted file mode 100644 index bfb8cfc..0000000 --- a/src/test/resources/mocks/vault/lookup-self.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "data": { - "accessor":"8609694a-cdbc-db9b-d345-e782dbb562ed", - "creation_time": 1523979354, - "creation_ttl": 2764800, - "display_name":"ldap2-tesla", - "entity_id":"7d2e3179-f69b-450c-7179-ac8ee8bd8ca9", - "expire_time":"2018-05-19T11:35:54.466476215-04:00", - "explicit_max_ttl": 0, - "id":"cf64a70f-3a12-3f6c-791d-6cef6d390eed", - "identity_policies": ["dev-group-policy"], - "issue_time":"2018-04-17T11:35:54.466476078-04:00", - "meta": { - "username":"tesla" - }, - "num_uses": 0, - "orphan": true, - "path":"auth/ldap2/login/tesla", - "policies": ["default","testgroup2-policy"], - "renewable": true, - "ttl": 2764790 - } -} \ No newline at end of file diff --git a/src/test/resources/secret-config-metadata.json b/src/test/resources/secret-config-metadata.json index b9e54c8..8d615e2 100644 --- a/src/test/resources/secret-config-metadata.json +++ b/src/test/resources/secret-config-metadata.json @@ -30,6 +30,14 @@ "required": false, "secure": false } + }, + { + "key": "CustomEntityNamePrefix", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } }, { "key": "VaultPath", From a1db58f87d4f07f152536e8de3cc38180e7c56d2 Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Tue, 29 Mar 2022 10:53:46 +0300 Subject: [PATCH 06/11] Final fixes. Added new option to secrets.template --- .../request/vault/AuthTokenResponse.java | 33 ++++ .../vault/request/vault/TokenResponse.java | 13 -- src/main/resources/secrets.template.html | 14 ++ .../OIDCPipelineIdentityProviderTest.java | 182 ------------------ 4 files changed, 47 insertions(+), 195 deletions(-) create mode 100644 src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthTokenResponse.java delete mode 100644 src/test/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProviderTest.java diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthTokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthTokenResponse.java new file mode 100644 index 0000000..c275449 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthTokenResponse.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 ThoughtWorks, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.thoughtworks.gocd.secretmanager.vault.request.vault; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class AuthTokenResponse { + @Expose + @SerializedName("client_token") + private String clientToken; + + public AuthTokenResponse() { + } + + public String getClientToken() { + return clientToken; + } + } \ No newline at end of file diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java index 79c0706..2655249 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java @@ -31,17 +31,4 @@ public TokenResponse() { public AuthTokenResponse getAuth() { return auth; } - - public class AuthTokenResponse { - @Expose - @SerializedName("client_token") - private String clientToken; - - public AuthTokenResponse() { - } - - public String getClientToken() { - return clientToken; - } - } } diff --git a/src/main/resources/secrets.template.html b/src/main/resources/secrets.template.html index 8749ab1..43d64bb 100755 --- a/src/main/resources/secrets.template.html +++ b/src/main/resources/secrets.template.html @@ -201,6 +201,20 @@
+
+
+ + + {{ GOINPUTNAME[CustomEntityNamePrefix].$error.server }} +

+ Use this optional parameter to namespace your CI environments. When specified each pipeline entity created in Vault will have the syntax {CustomEntityNamePrefix}-{PipelineName}. Default is pipeline-identity +

+
+
+
From 0f865b25d306f6d143458d5ad1cc52afdd1bece8 Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Wed, 30 Mar 2022 12:31:34 +0300 Subject: [PATCH 08/11] Added README and updated plugin --- README.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++- build.gradle | 2 +- 2 files changed, 134 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e83aec9..46f4345 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Alternatively, the configuration can be added directly to the config.xml using t `` 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) #### Vault Configuration + | Field | Required | Description | |-----------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | VaultUrl | Yes | The url of the Vault server instance. If no address is explicitly set, the plugin will look to the `VAULT_ADDR` environment variable. | @@ -117,7 +118,138 @@ To configure secret engines, set the value `SecretEngine` to either `secret` for | GoCDUsername | Yes | GoCD username which will be used by this plugin to authenticate against the GoCD API. | | GoCDPassword | Yes | GoCD credentials which will be used by this plugin to authenticate against the GoCD API. | -TODO: Describe how to use the OIDC provider +### OpenID Connect + +The GoCD Vault Secret Plugin can provide the calling pipeline with an own pipeline identity token, issued by Vault. It does so +by fetching GoCD API to retrieve information about the pipeline and then to create an entity in Vault which contains +these pipeline information as metadata. Finally, this newly created entity is used to create an OIDC Identity token (JWT) which +is signed by Vault and contains the following pipeline information: + +| Field | Description | +|--------------|----------------------------------------------------------------------------------------------------------------| +| pipeline | Pipeline name. | +| group | Pipeline group. | +| organization | Github organization or owning user account name (Other git servers are currently not supported). | +| repository | Repository name. | +| branch | (Optional) Can be null. The current git branch. This is null if the SCM material is provided by a GoCD plugin. | + +Example OIDC Identity Token Body: + +```json +{ + "aud": "https://some.gocd.domain.com", + "branch": "main", + "exp": 1648541403, + "group": "defaultGroup", + "iat": 1648455003, + "iss": "https://some.vault.domain.com/v1/identity/oidc", + "namespace": "root", + "organization": "anrock-sc", + "pipeline": "deploy-gocd-vault-plugin", + "repository": "gocd-vault-secret-plugin", + "sub": "52349e30-cff8-7959-b61c-e9f9280d6233" +} +``` + +#### Vault Configuration + +To enable the Vault OIDC Provider follow the [Vault OIDC Provider documentation](https://www.vaultproject.io/docs/concepts/oidc-provider#oidc-provider). +The plugin will create for each new pipeline a new [entity](https://www.vaultproject.io/api-docs/secret/identity/entity) in vault with respective pipeline metadata +(requires `read`, `write` and `update` permission to `/identity/entity/*`; policy should be restricted to entity name and attached policy name). +These metadata can be attached to the scopes of the OIDC Token using this template as an example: + +```plain + { + "pipeline": {{identity.entity.metadata.pipeline}}, + "group": {{identity.entity.metadata.group}}, + "repository": {{identity.entity.metadata.repository}}, + "organization": {{identity.entity.metadata.organization}}, + "branch": {{identity.entity.metadata.branch}} + } +``` + +This plugin needs to assume the newly crated pipeline entity in order to retrieve a pipeline identity token. For that the plugin +logs in as a pipeline via the token authentication endpoint. In order to do so, a [token auth backend role](https://www.vaultproject.io/api-docs/auth/token#create-update-token-role) +needs to be created in Vault, which is bound to a policy that allows to retrieve an OIDC Identity token. + +Example terraform definition for this auth token backend role: +```hcl +resource "vault_policy" "" { + name = "" + policy = <" { + capabilities = ["read"] + } + EOT +} + +resource "vault_token_auth_backend_role" "" { + role_name = "" + allowed_policies = [vault_policy..name] + allowed_entity_aliases = ["entity-alias-*"] +} +``` + +In addition to the pipeline entity the plugin will also create an [entity alias](https://www.vaultproject.io/api-docs/secret/identity/entity-alias) +(requires `create` and `update` permission to `/identity/entity-alias`) bound to the newly created entity, +as well as the token authentication endpoint (requires `read` permission to the `/sys/auth` path). +To assume a certain pipeline, a new vault token is created (requires `update` permission to `auth/token/create/`). +Using the new Vault token a request to fetch the OIDC Identity Token is done and returned to the pipeline. + + +#### Required Vault Policies + +**Required policy for the vault plugin** + +```hcl +# Used to read token accessor +path "sys/auth" { + capabilities = ["read"] +} + +# Assume identity of pipeline +path "auth/token/create/" { + capabilities = ["update"] +} + +# Create entity alias for pipelines +path "identity/entity-alias" { + capabilities = ["create", "update"] + allowed_parameters = { + "mount_accessor" = ["auth_token_*"] + "name" = ["-entity-alias-*"] + "*" = [] + } +} + +# Create entity for pipelines +path "identity/entity/*" { + capabilities = ["create", "update", "read"] + allowed_parameters = { + "name" = ["-*"] + "policies" = [""] + "*" = [] + } +} +``` + +**Required policy for a pipeline:** + +```hcl +path "identity/oidc/token/" { + capabilities = ["read"] +} +``` + +#### Usage + +To use this plugin add this SECRET reference to your pipeline configuration: +```plain +ENV_NAME={{SECRET:[][]}} +``` + +The pipeline name as a secret key is important for the plugin to know which pipeline identity token should be returned. +This value is trusted by the plugin and should be set by a trusted party in order to prevent pipeline privilege escalation. ### Building the code base To build the jar, run `./gradlew clean test assemble` diff --git a/build.gradle b/build.gradle index ab6e04b..fd381d5 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ repositories { } dependencies { - compileOnly group: 'cd.go.plugin', name: 'go-plugin-api', version: '21.4.0' + implementation group: 'cd.go.plugin', name: 'go-plugin-api', version: '22.1.0' implementation group: 'cd.go.plugin.base', name: 'gocd-plugin-base', version: '0.0.2' implementation group: 'com.bettercloud', name: 'vault-java-driver', version: '5.1.0' implementation group: 'com.google.code.gson', name: 'gson', version: '2.9.0' From e804abff241d42fd93c085a8cbdb3375edb5cee4 Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Thu, 31 Mar 2022 18:25:44 +0300 Subject: [PATCH 09/11] Finalized the plugin settings --- build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index fd381d5..8cf869c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,11 +19,11 @@ apply plugin: 'java' gocdPlugin { id = 'com.thoughtworks.gocd.secretmanager.vault' - pluginVersion = '1.2.1-SNAPSHOT' - goCdVersion = '20.9.0' + pluginVersion = '1.3.0' + goCdVersion = '22.1.0' name = 'Vault secret manager plugin' - description = 'The plugin allows to use hashicorp vault as secret manager for the GoCD server' - vendorName = 'asdThoughtWorks, Inc.' + description = 'The plugin allows to use hashicorp vault as secret manager and OIDC provider for the GoCD server' + vendorName = 'ThoughtWorks, Inc.' vendorUrl = 'https://github.com/gocd-private/gocd-vault-secret-plugin' githubRepo { From f89c5c576a3170fd5574961d9e3022a635952c8f Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Fri, 22 Apr 2022 14:47:37 +0200 Subject: [PATCH 10/11] Fixed bugs in SCM plugin and processing of pipeline dependencies --- .../vault/api/GoCDPipelineApi.java | 44 +++++-- ...elineConfigMaterialAttributesResponse.java | 56 ++++++--- .../vault/api/GoCDPipelineApiTest.java | 79 ++++++++++++- ...ipeline-config-dependency-wrong-order.json | 111 ++++++++++++++++++ .../gocd/pipeline-config-dependency.json | 97 +++++++++++++++ 5 files changed, 356 insertions(+), 31 deletions(-) create mode 100644 src/test/resources/mocks/gocd/pipeline-config-dependency-wrong-order.json create mode 100644 src/test/resources/mocks/gocd/pipeline-config-dependency.json diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java index bc62d6a..1e47d15 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java @@ -61,20 +61,42 @@ public PipelineMaterial fetchPipelineMaterial(String pipeline) throws APIExcepti throw new APIException(String.format("Could not fetch pipeline configuration for pipeline %s. Due to: %s", pipeline, response.body().string()), response.code()); } - // Even if there are multiple pipeline definitions, we will always use the first found material. - PipelineConfigMaterialResponse pipelineConfigMaterialResponse = pipelineConfigResponse.getMaterials().get(0); + // If possible we use git materials, otherwise we use first found material + PipelineConfigMaterialResponse pipelineConfigMaterialResponse = pipelineConfigResponse.getMaterials() + .stream() + .filter(materials -> materials.getType().equalsIgnoreCase("git") || materials.getType().equalsIgnoreCase("plugin")) + .findFirst() + .orElse(pipelineConfigResponse.getMaterials().get(0)); + String materialType = pipelineConfigMaterialResponse.getType(); - String repositoryUrl = pipelineConfigMaterialResponse.getAttributes().getUrl(); - if(materialType.equalsIgnoreCase("plugin")) { - repositoryUrl = fetchSCMRepositoryUrl(pipelineConfigResponse.getName()); + switch (materialType) { + case "plugin": + return new PipelineMaterial( + pipelineConfigResponse.getName(), + pipelineConfigResponse.getGroup(), + pipelineConfigMaterialResponse.getAttributes().getBranch(), + fetchSCMRepositoryUrl(pipelineConfigMaterialResponse.getAttributes().getRef()) + ); + case "dependency": + PipelineMaterial pipelineMaterial = fetchPipelineMaterial(pipelineConfigMaterialResponse.getAttributes().getPipeline()); + return new PipelineMaterial( + pipelineConfigResponse.getName(), + pipelineConfigResponse.getGroup(), + pipelineMaterial.getOrganization(), + pipelineMaterial.getRepositoryName(), + pipelineMaterial.getBranch() + ); + case "git": + return new PipelineMaterial( + pipelineConfigResponse.getName(), + pipelineConfigResponse.getGroup(), + pipelineConfigMaterialResponse.getAttributes().getBranch(), + pipelineConfigMaterialResponse.getAttributes().getUrl() + ); + default: + throw new IllegalStateException(String.format("Unexpected material type %s", materialType)); } - return new PipelineMaterial( - pipelineConfigResponse.getName(), - pipelineConfigResponse.getGroup(), - pipelineConfigMaterialResponse.getAttributes().getBranch(), - repositoryUrl - ); } catch (IOException e) { throw new APIException(e); } diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java index b512124..e0affb8 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java @@ -21,22 +21,40 @@ public class PipelineConfigMaterialAttributesResponse { - @Expose - @SerializedName("url") - private String url; - - @Expose - @SerializedName("branch") - private String branch; - - public PipelineConfigMaterialAttributesResponse() { - } - - public String getUrl() { - return url; - } - - public String getBranch() { - return branch; - } - } \ No newline at end of file + // set if material type is git + @Expose + @SerializedName("url") + private String url; + @Expose + @SerializedName("branch") + private String branch; + + // set if pipeline type is plugin + @Expose + @SerializedName("ref") + private String ref; + + // set if material type is dependency + @Expose + @SerializedName("pipeline") + private String pipeline; + + public PipelineConfigMaterialAttributesResponse() { + } + + public String getUrl() { + return url; + } + + public String getBranch() { + return branch; + } + + public String getRef() { + return ref; + } + + public String getPipeline() { + return pipeline; + } +} \ No newline at end of file diff --git a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java index 7b613e0..a42f59e 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java @@ -111,7 +111,7 @@ public void fetchPipelineMaterialTestSucceedsForSCMMaterial(String secretConfigJ assertThat(scmConfigRequest.getHeader("Authorization")).isEqualTo(basicAuth); assertThat(scmConfigRequest.getMethod()).isEqualTo("GET"); assertThat(scmConfigRequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V4_JSON.toString()); - assertThat(scmConfigRequest.getPath()).isEqualTo("/go/api/admin/scms/some-pipeline"); + assertThat(scmConfigRequest.getPath()).isEqualTo("/go/api/admin/scms/some-repository-pr"); assertThat(pipelineMaterial).isEqualTo( @@ -125,6 +125,83 @@ public void fetchPipelineMaterialTestSucceedsForSCMMaterial(String secretConfigJ ); } + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/gocd/pipeline-config-dependency.json", + "/mocks/gocd/pipeline-config.json" + }) + public void fetchPipelineMaterialTestSucceedsForDependencyMaterial(String secretConfigJson, String pipelineConfigResponse, String pipelineConfig) throws IOException, InterruptedException, APIException { + SecretConfig secretConfig = spy(GsonTransformer.fromJson(secretConfigJson, SecretConfig.class)); + + mockWebServer.enqueue(new MockResponse().setBody(pipelineConfigResponse)); + mockWebServer.enqueue(new MockResponse().setBody(pipelineConfig)); + mockWebServer.start(); + + doReturn(getMockServerAddress()).when(secretConfig).getGocdServerURL(); + GoCDPipelineApi goCDPipelineApi = new GoCDPipelineApi(secretConfig); + + PipelineMaterial pipelineMaterial = goCDPipelineApi.fetchPipelineMaterial("some-pipeline-with-dependencies"); + RecordedRequest pipelineConfigrequest = mockWebServer.takeRequest(); + + String basicAuth = "Basic " + Base64.getEncoder().encodeToString("username:supersecret".getBytes(StandardCharsets.UTF_8)); + assertThat(pipelineConfigrequest.getHeader("Authorization")).isEqualTo(basicAuth); + assertThat(pipelineConfigrequest.getMethod()).isEqualTo("GET"); + assertThat(pipelineConfigrequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V11_JSON.toString()); + assertThat(pipelineConfigrequest.getPath()).isEqualTo("/go/api/admin/pipelines/some-pipeline-with-dependencies"); + + RecordedRequest scmConfigRequest = mockWebServer.takeRequest(); + assertThat(scmConfigRequest.getHeader("Authorization")).isEqualTo(basicAuth); + assertThat(scmConfigRequest.getMethod()).isEqualTo("GET"); + assertThat(scmConfigRequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V11_JSON.toString()); + assertThat(scmConfigRequest.getPath()).isEqualTo("/go/api/admin/pipelines/some-pipeline"); + + + assertThat(pipelineMaterial).isEqualTo( + new PipelineMaterial( + "some-pipeline-with-dependencies", + "dev", + "important-organization", + "some-repository", + "main" + ) + ); + } + + @ParameterizedTest + @JsonSource(jsonFiles = { + "/secret-config-oidc.json", + "/mocks/gocd/pipeline-config-dependency-wrong-order.json" + }) + public void fetchPipelineMaterialTestSucceedsForDependencyMaterialWrongOrder(String secretConfigJson, String pipelineConfigResponse) throws IOException, InterruptedException, APIException { + SecretConfig secretConfig = spy(GsonTransformer.fromJson(secretConfigJson, SecretConfig.class)); + + mockWebServer.enqueue(new MockResponse().setBody(pipelineConfigResponse)); + mockWebServer.start(); + + doReturn(getMockServerAddress()).when(secretConfig).getGocdServerURL(); + GoCDPipelineApi goCDPipelineApi = new GoCDPipelineApi(secretConfig); + + PipelineMaterial pipelineMaterial = goCDPipelineApi.fetchPipelineMaterial("some-pipeline-with-dependencies-wrong-order"); + RecordedRequest pipelineConfigrequest = mockWebServer.takeRequest(); + + String basicAuth = "Basic " + Base64.getEncoder().encodeToString("username:supersecret".getBytes(StandardCharsets.UTF_8)); + assertThat(pipelineConfigrequest.getHeader("Authorization")).isEqualTo(basicAuth); + assertThat(pipelineConfigrequest.getMethod()).isEqualTo("GET"); + assertThat(pipelineConfigrequest.getHeader("Accept")).isEqualTo(GoCDPipelineApi.ACCEPT_GOCD_V11_JSON.toString()); + assertThat(pipelineConfigrequest.getPath()).isEqualTo("/go/api/admin/pipelines/some-pipeline-with-dependencies-wrong-order"); + + assertThat(pipelineMaterial).isEqualTo( + new PipelineMaterial( + "some-pipeline-with-dependencies-wrong-order", + "dev", + "important-organization", + "some-repository", + "main" + ) + ); + } + private String getMockServerAddress() { String address = mockWebServer.url("").url().toString(); return address.substring(0, address.length() - 1); diff --git a/src/test/resources/mocks/gocd/pipeline-config-dependency-wrong-order.json b/src/test/resources/mocks/gocd/pipeline-config-dependency-wrong-order.json new file mode 100644 index 0000000..63bdb6d --- /dev/null +++ b/src/test/resources/mocks/gocd/pipeline-config-dependency-wrong-order.json @@ -0,0 +1,111 @@ +{ + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/pipelines/some-pipeline-pr" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#pipeline-config" + }, + "find": { + "href": "https://gocd.com/go/api/admin/pipelines/:pipeline_name" + } + }, + "label_template": "${COUNT}-${git[:7]}", + "lock_behavior": "none", + "name": "some-pipeline-with-dependencies-wrong-order", + "template": null, + "group": "dev", + "origin": { + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/config_repos/some-pipeline" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#config-repos" + }, + "find": { + "href": "https://gocd.com/go/api/admin/config_repos/:id" + } + }, + "type": "config_repo", + "id": "some-pipeline-with-dependencies-wrong-order" + }, + "parameters": [], + "environment_variables": [ + { + "secure": false, + "name": "OIDC_TOKEN", + "value": "{{SECRET:[vault][some-pipeline]}}" + } + ], + "materials": [ + { + "type": "dependency", + "attributes": { + "pipeline": "some-pipeline", + "stage": "deploy", + "name": "some-pipeline", + "auto_update": true, + "ignore_for_scheduling": false + } + }, + { + "type": "git", + "attributes": { + "url": "git@github.com:important-organization/some-repository.git", + "destination": "some-repository", + "filter": null, + "invert_filter": true, + "name": "some-pipeline", + "auto_update": false, + "branch": "main", + "submodule_folder": null, + "shallow_clone": false + } + } + ], + "stages": [ + { + "name": "test", + "fetch_materials": true, + "clean_working_directory": false, + "never_cleanup_artifacts": false, + "approval": { + "type": "success", + "allow_only_on_success": false, + "authorization": { + "roles": [], + "users": [] + } + }, + "environment_variables": [], + "jobs": [ + { + "name": "test", + "run_instance_count": null, + "timeout": null, + "environment_variables": [], + "resources": [ + "build" + ], + "tasks": [ + { + "type": "exec", + "attributes": { + "run_if": [ + "passed" + ], + "command": "/opt/script.sh", + "args": "" + } + } + ], + "tabs": [], + "artifacts": [] + } + ] + } + ], + "tracking_tool": null, + "timer": null +} \ No newline at end of file diff --git a/src/test/resources/mocks/gocd/pipeline-config-dependency.json b/src/test/resources/mocks/gocd/pipeline-config-dependency.json new file mode 100644 index 0000000..65920e6 --- /dev/null +++ b/src/test/resources/mocks/gocd/pipeline-config-dependency.json @@ -0,0 +1,97 @@ +{ + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/pipelines/some-pipeline-pr" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#pipeline-config" + }, + "find": { + "href": "https://gocd.com/go/api/admin/pipelines/:pipeline_name" + } + }, + "label_template": "${COUNT}-${git[:7]}", + "lock_behavior": "none", + "name": "some-pipeline-with-dependencies", + "template": null, + "group": "dev", + "origin": { + "_links": { + "self": { + "href": "https://gocd.com/go/api/admin/config_repos/some-pipeline" + }, + "doc": { + "href": "https://api.gocd.org/22.1.0/#config-repos" + }, + "find": { + "href": "https://gocd.com/go/api/admin/config_repos/:id" + } + }, + "type": "config_repo", + "id": "some-pipeline-with-dependencies" + }, + "parameters": [], + "environment_variables": [ + { + "secure": false, + "name": "OIDC_TOKEN", + "value": "{{SECRET:[vault][some-pipeline]}}" + } + ], + "materials": [ + { + "type": "dependency", + "attributes": { + "pipeline": "some-pipeline", + "stage": "deploy", + "name": "some-pipeline", + "auto_update": true, + "ignore_for_scheduling": false + } + } + ], + "stages": [ + { + "name": "test", + "fetch_materials": true, + "clean_working_directory": false, + "never_cleanup_artifacts": false, + "approval": { + "type": "success", + "allow_only_on_success": false, + "authorization": { + "roles": [], + "users": [] + } + }, + "environment_variables": [], + "jobs": [ + { + "name": "test", + "run_instance_count": null, + "timeout": null, + "environment_variables": [], + "resources": [ + "build" + ], + "tasks": [ + { + "type": "exec", + "attributes": { + "run_if": [ + "passed" + ], + "command": "/opt/script.sh", + "args": "" + } + } + ], + "tabs": [], + "artifacts": [] + } + ] + } + ], + "tracking_tool": null, + "timer": null +} \ No newline at end of file From 1c600be1bc61ed28e06f29538fb82fe821583254 Mon Sep 17 00:00:00 2001 From: Marvin Petzolt Date: Tue, 17 May 2022 13:04:55 +0200 Subject: [PATCH 11/11] Updated Readme --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46f4345..1697907 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,26 @@ Example OIDC Identity Token Body: "iat": 1648455003, "iss": "https://some.vault.domain.com/v1/identity/oidc", "namespace": "root", - "organization": "anrock-sc", + "organization": "anroc", "pipeline": "deploy-gocd-vault-plugin", "repository": "gocd-vault-secret-plugin", "sub": "52349e30-cff8-7959-b61c-e9f9280d6233" } ``` +Identity tokens can be used to authenticate to Vault to use Vaults rich API to fetch more then just static key-value secrets, +as well as authenticating to other services such as [Google Cloud Project](https://cloud.google.com/iam/docs/workload-identity-federation). + +##### Claim building + +As GoCD supports multiple material configuration, it highly depends on the order of the multiple definition as well as +the type of material configured. The supported cases are defined below. + +1. If multiple materials are defined, the first Git or Git Plugin material will be used. +2. If only pipeline dependency materials are defined, the first dependency will be used to resolve the referenced configuration. + +Gor Git plugin materials, no branch information can be fetched and the branch claim remains empty. + #### Vault Configuration To enable the Vault OIDC Provider follow the [Vault OIDC Provider documentation](https://www.vaultproject.io/docs/concepts/oidc-provider#oidc-provider). @@ -245,7 +258,7 @@ path "identity/oidc/token/" { To use this plugin add this SECRET reference to your pipeline configuration: ```plain -ENV_NAME={{SECRET:[][]}} +IDENTITY_TOKEN={{SECRET:[][]}} ``` The pipeline name as a secret key is important for the plugin to know which pipeline identity token should be returned.