diff --git a/README.md b/README.md index 9b7430f..1697907 100644 --- a/README.md +++ b/README.md @@ -72,23 +72,197 @@ 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. | -| VaultPath | Yes | The vault path which holds the secrets as key-value pair (e.g. `secret/gocd`) | | ConnectionTimeout | No | The number of seconds to wait before giving up on establishing an HTTP(s) connection to the Vault server. If no openTimeout is explicitly set, then the object will look to the `VAULT_OPEN_TIMEOUT` environment variable. Defaults to `5 seconds`. | | ReadTimeout | No | Once connection has already been established, this is the number of seconds to wait for all data to finish downloading. If no readTimeout is explicitly set, then the object will look to the `VAULT_READ_TIMEOUT` environment variable. Defaults to `30 seconds`. | | ServerPem | No | An X.509 certificate, in unencrypted PEM format with UTF-8 encoding to use when communicating with Vault over HTTPS | +| Max Retries | No | Number of times to attempt to gather secrets from Vault. Defaults to `0`. | +| Retry Interval Milliseconds | No | Duration between retry attempts (set by `Max Retries`). Defaults to `100 milliseconds`. | + +#### Authentication + +| Field | Required | Description | +|-----------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | AuthMethod | Yes | The auth method to use to authenticate with the Vault server, can be one of `token`, `approle` or `cert` | | Token | No | Required if using `token` auth method. This is the token used to read secrets from Vault. Ensure this token has a longer ttl, the plugin will not be renewing the token. | | RoleId | No | Required if using `approle` auth method. The plugins will use the configured `RoleId` and `SecretId` to authenticate with Vault. | | SecretId | No | Required if using `approle` auth method. | | ClientKeyPem | No | Required if using `cert` auth method. An RSA private key, in unencrypted PEM format with UTF-8 encoding. | | ClientPem | No | Required if using `cert` auth method. An X.509 client certificate, in unencrypted PEM format with UTF-8 encoding. | -| Max Retries | No | Number of times to attempt to gather secrets from Vault. Defaults to `0`. | -| Retry Interval Milliseconds | No | Duration between retry attempts (set by `Max Retries`). Defaults to `100 milliseconds`. | +#### Secret Engines + +To configure secret engines, set the value `SecretEngine` to either `secret` for the key-value secret storage or to `oidc` to use Vault with GoCD with pipeline identity tokens. + +##### Key-Value Secret Engine + +| Field | Required | Description | +|-----------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| SecretEngine | No | This defines the [secret engine type](https://www.vaultproject.io/docs/secrets). Either `secert` or `oidc`. Defaults to `secret`. | +| VaultPath | Yes | The vault path which holds the secrets as key-value pair (e.g. `secret/gocd`) | + +##### OIDC Provider + +| Field | Required | Description | +|------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| SecretEngine | Yes | This defines the [secret engine type](https://www.vaultproject.io/docs/secrets). Either `secert` or `oidc`. Defaults to `secret`. | +| VaultPath | Yes | Path which returns the OIDC token from Vault. Usually this starts with `/v1/identity/oidc/token/...` | +| PipelineTokenAuthBackendRole | Yes | The Token-Auth Backend Role which is used by this plugin to assume a certain pipeline entity. | +| PipelinePolicy | No | An comma separated list of optional [pipeline policy names](https://www.vaultproject.io/api-docs/auth/token#policies) to restrict the permissions this assumed pipeline entity will have. | +| CustomEntityNamePrefix | No | 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` | +| GoCDServerUrl | Yes | GoCD server base URL to issue API calls. | +| 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. | + +### 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": "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). +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 +IDENTITY_TOKEN={{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 b0fd1bb..404d21c 100644 --- a/build.gradle +++ b/build.gradle @@ -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' @@ -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' @@ -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 { 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..7351cb2 100644 --- a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutor.java @@ -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; @@ -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 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)); @@ -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); 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..9c86ed0 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,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(); } 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/api/GoCDPipelineApi.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java new file mode 100644 index 0000000..1e47d15 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApi.java @@ -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); + } + + + } +} 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/builders/SecretEngineBuilder.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.java new file mode 100644 index 0000000..8c7ade1 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/builders/SecretEngineBuilder.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.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; +import com.thoughtworks.gocd.secretmanager.vault.secretengines.SecretEngine; + +public class SecretEngineBuilder { + + private Vault vault; + private VaultConfig vaultConfig; + private SecretConfig secretConfig; + + + public SecretEngineBuilder secretConfig(SecretConfig secretConfig) { + this.secretConfig = secretConfig; + return this; + } + + public SecretEngineBuilder vault(Vault vault) { + this.vault = vault; + return this; + } + + public SecretEngine build() { + switch (secretConfig.getSecretEngine()) { + case SecretConfig.OIDC_ENGINE: + 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/http/DataResponseExtractor.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/DataResponseExtractor.java new file mode 100644 index 0000000..e53b828 --- /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.vault.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..4bc40ef --- /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", 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 new file mode 100644 index 0000000..4f8ec00 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/http/OkHTTPClientFactory.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.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 CONTENT_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); + + 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(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/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/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 477c6ae..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 @@ -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; @@ -38,18 +36,43 @@ 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; + public static final String DEFAULT_ENTITY_NAME_PREFIX = "pipeline-identity"; @Expose @SerializedName("VaultUrl") @Property(name = "VaultUrl", required = true) private String vaultUrl; + @Expose + @SerializedName("SecretEngine") + @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("CustomEntityNamePrefix") + @Property(name = "CustomEntityNamePrefix") + private String customEntityNamePrefix; + @Expose @SerializedName("VaultPath") @Property(name = "VaultPath", required = true) @@ -115,6 +138,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; } @@ -183,10 +221,54 @@ public String getServerPem() { return serverPem; } + public String getSecretEngine() { + if (isBlank(secretEngine)) { + return DEFAULT_SECRET_ENGINE; + } + return secretEngine; + } + + public String getPipelineTokenAuthBackendRole() { + return pipelineTokenAuthBackendRole; + } + + public List getPipelinePolicy() { + if (isBlank(pipelinePolicy)) { + return new ArrayList<>(); + } + return asList(pipelinePolicy.split(",\\s*")); + } + + public String getGocdServerURL() { + if (gocdServerURL.endsWith("/")) { + return gocdServerURL.substring(0, gocdServerURL.length() - 1); + } + return gocdServerURL; + } + + public String getCustomEntityNamePrefix() { + if (isBlank(customEntityNamePrefix)) { + return DEFAULT_ENTITY_NAME_PREFIX; + } + return customEntityNamePrefix; + } + + public String getGoCDUsername() { + return gocdUsername; + } + + public String getGoCDPassword() { + return gocdPassword; + } + 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 +292,19 @@ 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) && + Objects.equals(pipelineTokenAuthBackendRole, that.pipelineTokenAuthBackendRole) && + Objects.equals(pipelinePolicy, that.pipelinePolicy) && + Objects.equals(gocdUsername, that.gocdUsername) && + Objects.equals(gocdPassword, that.gocdPassword) && + Objects.equals(customEntityNamePrefix, that.customEntityNamePrefix); + } @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() { @@ -229,4 +318,9 @@ 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/gocd/PipelineConfigMaterialAttributesResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java new file mode 100644 index 0000000..e0affb8 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigMaterialAttributesResponse.java @@ -0,0 +1,60 @@ +/* + * 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 { + + // 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/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 new file mode 100644 index 0000000..7859fb7 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/PipelineConfigResponse.java @@ -0,0 +1,53 @@ +/* + * 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; + } + +} 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 new file mode 100644 index 0000000..ae4177d --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/gocd/SCMResponse.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.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; + } +} diff --git a/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthMountsResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/AuthMountsResponse.java new file mode 100644 index 0000000..e65b29a --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/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.vault; + +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/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/CreateTokenRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/CreateTokenRequest.java new file mode 100644 index 0000000..81aeb03 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/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.vault; + +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/vault/DataResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/DataResponse.java new file mode 100644 index 0000000..d050d09 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/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.vault; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class DataResponse { + + @Expose + @SerializedName("data") + private T data; + + public DataResponse() { + } + + public T getData() { + return data; + } +} 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 new file mode 100644 index 0000000..e90daf9 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/EntityAliasRequest.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.request.vault; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class EntityAliasRequest { + + @Expose + @SerializedName("name") + private String name; + + @Expose + @SerializedName("canonical_id") + private String canonicalId; + + @Expose + @SerializedName("mount_accessor") + private String mountAccessor; + + public EntityAliasRequest(String name, String canonicalId, String mountAccessor) { + this.name = name; + this.canonicalId = canonicalId; + this.mountAccessor = mountAccessor; + } +} 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/LookupResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/LookupResponse.java new file mode 100644 index 0000000..c2bc966 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/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.vault; + +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/vault/MetadataRequest.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/MetadataRequest.java new file mode 100644 index 0000000..34266e7 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/MetadataRequest.java @@ -0,0 +1,66 @@ +/* + * 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 com.thoughtworks.gocd.secretmanager.vault.models.PipelineMaterial; + +public class MetadataRequest { + + @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 MetadataRequest(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 MetadataRequest(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/vault/OICDTokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/OICDTokenResponse.java new file mode 100644 index 0000000..6a45915 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/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.vault; + +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/request/vault/TokenAuthMountResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenAuthMountResponse.java new file mode 100644 index 0000000..a2ce905 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/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.vault; + +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/vault/TokenResponse.java b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.java new file mode 100644 index 0000000..2655249 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/request/vault/TokenResponse.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 TokenResponse { + + @Expose + @SerializedName("auth") + private AuthTokenResponse auth; + + public TokenResponse() { + } + + public AuthTokenResponse getAuth() { + return auth; + } +} 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..64490c1 --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/KVSecretEngine.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.secretengines; + +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; + +public class KVSecretEngine extends SecretEngine { + + private Map secretsFromVault; + + public KVSecretEngine(Vault vault) { + super(vault); + } + + @Override + public Optional getSecret(String path, String key) throws APIException { + if (secretsFromVault == null) { + try { + secretsFromVault = getSecretData(path); + } catch (VaultException vaultException) { + throw new APIException(vaultException); + } + } + + 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..9aab60a --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/OIDCPipelineIdentityProvider.java @@ -0,0 +1,82 @@ +/* + * 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.VaultConfig; +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 java.util.Optional; + +public class OIDCPipelineIdentityProvider extends SecretEngine { + + private final GoCDPipelineApi gocd; + private final VaultApi vault; + private SecretConfig secretConfig; + + public OIDCPipelineIdentityProvider(Vault vault, VaultConfig vaultConfig, SecretConfig secretConfig) { + super(vault); + this.secretConfig = secretConfig; + this.gocd = new GoCDPipelineApi(secretConfig); + this.vault = new VaultApi(vaultConfig, 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 = gocd.fetchPipelineMaterial(pipelineName); + + String entityId = vault.identity().createPipelineEntity(entityName(pipelineName), secretConfig.getPipelinePolicy(), pipelineMaterial) + .orElse(vault.identity().fetchPipelineEntity(entityName(pipelineName))) + .getData() + .getId(); + + String authMountAccessor = vault.sys().getAuthMountAccessor(); + vault.identity().createPipelineEntityAlias(entityId, authMountAccessor, entityAliasName(pipelineName)); + + String pipelineAuthToken = vault.auth().assumePipeline(secretConfig.getPipelineTokenAuthBackendRole(), secretConfig.getPipelinePolicy(), entityAliasName(pipelineName)); + return Optional.of(vault.identity().oidcToken(pipelineAuthToken, path)); + } + + private String entityAliasName(String pipelineName) { + return entityName(pipelineName, "-entity-alias"); + } + + private String entityName(String pipelineName) { + return entityName(pipelineName, ""); + } + + private String entityName(String pipelineName, String additionalPrefix) { + return String.format("%s%s-%s", + secretConfig.getCustomEntityNamePrefix(), + additionalPrefix, + pipelineName.toLowerCase().replaceAll("\\s+", "-") + ); + } +} 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..9ff010f --- /dev/null +++ b/src/main/java/com/thoughtworks/gocd/secretmanager/vault/secretengines/SecretEngine.java @@ -0,0 +1,38 @@ +/* + * 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 com.thoughtworks.gocd.secretmanager.vault.http.exceptions.APIException; + +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 APIException; + + public Vault getVault() { + return vault; + } +} 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/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..cac140d 100755 --- a/src/main/resources/secrets.template.html +++ b/src/main/resources/secrets.template.html @@ -143,14 +143,109 @@ 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. +

+
+
+
+
{{ 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. +

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

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

+
+
+ +
+
+ + + {{ 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 +

+
+
+ +
+
+ + + {{ 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 af85c73..54a142e 100644 --- a/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/SecretConfigLookupExecutorTest.java @@ -17,12 +17,14 @@ 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.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; import org.json.JSONException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,12 +32,11 @@ 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 +48,33 @@ 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); + void shouldReturnLookupResponse() throws APIException, JSONException { 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); + final VaultConfig vaultConfig = mock(VaultConfig.class); + + when(vaultProvider.getVaultConfig()).thenReturn(vaultConfig); when(request.getConfiguration()).thenReturn(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;")); + 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/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/api/GoCDPipelineApiTest.java b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java new file mode 100644 index 0000000..a42f59e --- /dev/null +++ b/src/test/java/com/thoughtworks/gocd/secretmanager/vault/api/GoCDPipelineApiTest.java @@ -0,0 +1,210 @@ +/* + * 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.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 java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +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-repository-pr"); + + + assertThat(pipelineMaterial).isEqualTo( + new PipelineMaterial( + "some-pipeline", + "dev", + "important-organization", + "some-repository", + null + ) + ); + } + + @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); + } + +} \ No newline at end of file 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/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..061521e --- /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.vault(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.CONTENT_TYPE_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.vault(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.vault(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.vault(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.vault(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/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 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/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/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/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 3933113..8d615e2 100644 --- a/src/test/resources/secret-config-metadata.json +++ b/src/test/resources/secret-config-metadata.json @@ -6,6 +6,38 @@ "required": true, "secure": false } + }, + { + "key": "SecretEngine", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } + }, + { + "key": "PipelineTokenAuthBackendRole", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } + }, + { + "key": "PipelinePolicy", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } + }, + { + "key": "CustomEntityNamePrefix", + "metadata": { + "display_name": "", + "required": false, + "secure": false + } }, { "key": "VaultPath", @@ -14,7 +46,7 @@ "required": true, "secure": false } -}, + }, { "key": "NameSpace", "metadata": { @@ -110,5 +142,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-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..c9a9433 --- /dev/null +++ b/src/test/resources/secret-config-oidc.json @@ -0,0 +1,14 @@ +{ + "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", + "GoCDServerUrl": "someURL", + "GoCDUsername": "username", + "GoCDPassword": "supersecret" +}