Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support OpenID Connect #102

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
180 changes: 177 additions & 3 deletions README.md

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ apply plugin: 'java'

gocdPlugin {
id = 'com.thoughtworks.gocd.secretmanager.vault'
pluginVersion = '1.2.0'
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'
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'

Expand Down Expand Up @@ -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.9.0'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.9.0'
Expand All @@ -62,6 +63,7 @@ dependencies {
testImplementation group: 'org.jsoup', name: 'jsoup', version: '1.15.2'
testImplementation group: 'cd.go.plugin', name: 'go-plugin-api', version: '21.4.0'
testImplementation group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.1'
testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '4.9.3'
}

test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
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 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;
Expand All @@ -46,17 +48,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<String, String> secretsFromVault = vault.logical()
.read(request.getConfiguration().getVaultPath())
.getData();
SecretEngine secretEngine = buildSecretEngine(request, vault, vaultProvider.getVaultConfig());

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));
Expand All @@ -66,6 +65,14 @@ protected GoPluginApiResponse execute(SecretConfigRequest request) {
}
}

protected SecretEngine buildSecretEngine(SecretConfigRequest request, Vault vault, VaultConfig vaultConfig) {
return new SecretEngineBuilder()
.secretConfig(request.getConfiguration())
.vault(vault)
.vaultConfig(vaultConfig)
.build();
}

@Override
protected SecretConfigRequest parseRequest(String body) {
return fromJson(body, SecretConfigRequest.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -49,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 AppRoleAuthMethodValidator(), new TokenAuthMethodValidator(),
new SecretEngineValidator(), new OIDCSecretEngineValidator())
.lookup(new SecretConfigLookupExecutor())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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.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;
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());
}

// 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();
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));
}

} 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);
}


}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading