Skip to content

feat: make HashiCorp vault authentication extensible #4822

New issue

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

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

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ plugins {
dependencies {
api(project(":spi:common:core-spi"))
api(project(":spi:common:http-spi"))
api(project(":extensions:common:vault:hashicorp:vault-hashicorp-spi"))
api(project(":spi:common:vault-hashicorp-spi"))

implementation(project(":core:common:lib:util-lib"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,22 @@

package org.eclipse.edc.vault.hashicorp.auth;

import org.eclipse.edc.runtime.metamodel.annotation.Configuration;
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.vault.hashicorp.client.HashicorpVaultSettings;
import org.eclipse.edc.vault.hashicorp.spi.auth.HashicorpVaultTokenProvider;

@Extension(value = HashicorpVaultAuthenticationExtension.NAME)
public class HashicorpVaultAuthenticationExtension implements ServiceExtension {

public static final String NAME = "Hashicorp Vault Authentication";

@Configuration
private HashicorpVaultSettings config;
@Setting(description = "The token used to access the Hashicorp Vault", key = "edc.vault.hashicorp.token")
private String token;

@Provider(isDefault = true)
public HashicorpVaultTokenProvider tokenProvider() {
return new HashicorpVaultTokenProviderImpl(config.token());
return new HashicorpVaultTokenProviderImpl(token);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ public class HashicorpVaultSettings {
private String healthCheckPath;
@Setting(description = "Specifies if being a standby should still return the active status code instead of the standby status code", defaultValue = VAULT_HEALTH_CHECK_STANDBY_OK_DEFAULT + "", key = "edc.vault.hashicorp.health.check.standby.ok")
private boolean healthStandbyOk;
@Setting(description = "The token used to access the Hashicorp Vault", key = "edc.vault.hashicorp.token")
private String token;
@Setting(description = "Whether the automatic token renewal process will be triggered or not. Should be disabled only for development and testing purposes",
defaultValue = VAULT_TOKEN_SCHEDULED_RENEW_ENABLED_DEFAULT + "", key = "edc.vault.hashicorp.token.scheduled-renew-enabled")
private boolean scheduledTokenRenewEnabled;
Expand Down Expand Up @@ -79,10 +77,6 @@ public boolean healthStandbyOk() {
return healthStandbyOk;
}

public String token() {
return token;
}

public boolean scheduledTokenRenewEnabled() {
return scheduledTokenRenewEnabled;
}
Expand Down Expand Up @@ -135,11 +129,6 @@ public Builder healthStandbyOk(boolean healthStandbyOk) {
return this;
}

public Builder token(String token) {
values.token = token;
return this;
}

public Builder scheduledTokenRenewEnabled(boolean scheduledTokenRenewEnabled) {
values.scheduledTokenRenewEnabled = scheduledTokenRenewEnabled;
return this;
Expand Down Expand Up @@ -168,7 +157,6 @@ public Builder folderPath(String folderPath) {
public HashicorpVaultSettings build() {
requireNonNull(values.url, "Vault url must be valid");
requireNonNull(values.healthCheckPath, "Vault health check path must not be null");
requireNonNull(values.token, "Vault token must not be null");

if (values.ttl < 5) {
throw new IllegalArgumentException("Vault token ttl minimum value is 5");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
private Request httpGet(HttpUrl requestUri) {
return new Request.Builder()
.url(requestUri)
.headers(getHeaders(tokenProvider.vaultToken()))
.headers(getHeaders())
.get()
.build();
}
Expand All @@ -170,14 +170,14 @@
private Request httpPost(HttpUrl requestUri, Object requestBody) {
return new Request.Builder()
.url(requestUri)
.headers(getHeaders(tokenProvider.vaultToken()))
.headers(getHeaders())
.post(createRequestBody(requestBody))
.build();
}

private Headers getHeaders(String token) {
private Headers getHeaders() {
var headersBuilder = new Headers.Builder().add(VAULT_REQUEST_HEADER, Boolean.toString(true));
headersBuilder.add(VAULT_TOKEN_HEADER, token);
headersBuilder.add(VAULT_TOKEN_HEADER, tokenProvider.vaultToken());
return headersBuilder.build();
}

Expand All @@ -203,7 +203,7 @@
return Result.success(isRenewable);
} catch (IllegalArgumentException e) {
var errMsgFormat = "Failed to parse renewable flag from token look up response %s with reason: %s";
monitor.warning(errMsgFormat.formatted(map, e.getStackTrace()));
return Result.failure(errMsgFormat.formatted(map, e.getMessage()));
}
}
Expand All @@ -216,7 +216,7 @@
return Result.success(ttl);
} catch (IllegalArgumentException e) {
var errMsgFormat = "Failed to parse ttl from token renewal response %s with reason: %s";
monitor.warning(errMsgFormat.formatted(map, e.getStackTrace()));
return Result.failure(errMsgFormat.formatted(map, e.getMessage()));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ class HashicorpVaultIntegrationTest {
void setUp() {
var settings = HashicorpVaultSettings.Builder.newInstance()
.url("http://localhost:%d".formatted(getPort()))
.token(TOKEN)
.ttl(100)
.healthCheckPath(VAULT_API_HEALTH_PATH_DEFAULT)
.secretPath(VAULT_API_SECRET_PATH_DEFAULT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ static void prepare() throws IOException {
.url("http://localhost:%s".formatted(getPort()))
.healthCheckPath(VAULT_API_HEALTH_PATH_DEFAULT)
.ttl(24 * 60)
.token(TOKEN)
.build();
httpClient = new EdcHttpClientImpl(new OkHttpClient.Builder().build(), RetryPolicy.ofDefaults(), monitor);
tokenProvider = new HashicorpVaultTokenProviderImpl(TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ class HashicorpVaultHealthServiceTest {
.url(VAULT_URL)
.healthCheckPath(HEALTH_PATH)
.healthStandbyOk(false)
.token(VAULT_TOKEN)
.ttl(VAULT_TOKEN_TTL)
.renewBuffer(RENEW_BUFFER)
.secretPath(CUSTOM_SECRET_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,12 @@ class HashicorpVaultSettingsTest {
void createSettings_withDefaultValues_shouldSucceed() {
var settings = assertDoesNotThrow(() -> createSettings(
URL,
TOKEN,
HEALTH_CHECK_PATH, VAULT_TOKEN_TTL_DEFAULT,
VAULT_TOKEN_RENEW_BUFFER_DEFAULT));
assertThat(settings.url()).isEqualTo(URL);
assertThat(settings.healthCheckEnabled()).isEqualTo(true);
assertThat(settings.healthCheckPath()).isEqualTo(HEALTH_CHECK_PATH);
assertThat(settings.healthStandbyOk()).isEqualTo(true);
assertThat(settings.token()).isEqualTo(TOKEN);
assertThat(settings.ttl()).isEqualTo(VAULT_TOKEN_TTL_DEFAULT);
assertThat(settings.renewBuffer()).isEqualTo(VAULT_TOKEN_RENEW_BUFFER_DEFAULT);
assertThat(settings.secretPath()).isEqualTo(SECRET_PATH);
Expand All @@ -53,7 +51,6 @@ void createSettings_withDefaultValues_shouldSucceed() {
void createSettings_withVaultUrlNull_shouldThrowException() {
var throwable = assertThrows(Exception.class, () -> createSettings(
null,
TOKEN,
HEALTH_CHECK_PATH, VAULT_TOKEN_TTL_DEFAULT,
VAULT_TOKEN_RENEW_BUFFER_DEFAULT));
assertThat(throwable.getMessage()).isEqualTo("Vault url must not be null");
Expand All @@ -63,29 +60,16 @@ void createSettings_withVaultUrlNull_shouldThrowException() {
void createSettings_withHealthCheckPathNull_shouldThrowException() {
var throwable = assertThrows(Exception.class, () -> createSettings(
URL,
TOKEN,
null,
VAULT_TOKEN_TTL_DEFAULT,
VAULT_TOKEN_RENEW_BUFFER_DEFAULT));
assertThat(throwable.getMessage()).isEqualTo("Vault health check path must not be null");
}

@Test
void createSettings_withVaultTokenNull_shouldThrowException() {
var throwable = assertThrows(Exception.class, () -> createSettings(
URL,
null,
HEALTH_CHECK_PATH,
VAULT_TOKEN_TTL_DEFAULT,
VAULT_TOKEN_RENEW_BUFFER_DEFAULT));
assertThat(throwable.getMessage()).isEqualTo("Vault token must not be null");
}

@Test
void createSettings_withVaultTokenTtlLessThan5_shouldThrowException() {
var throwable = assertThrows(Exception.class, () -> createSettings(
URL,
TOKEN,
HEALTH_CHECK_PATH,
4,
VAULT_TOKEN_RENEW_BUFFER_DEFAULT));
Expand All @@ -97,15 +81,13 @@ void createSettings_withVaultTokenTtlLessThan5_shouldThrowException() {
void createSettings_withVaultTokenRenewBufferEqualOrGreaterThanTtl_shouldThrowException(long value) {
var throwable = assertThrows(Exception.class, () -> createSettings(
URL,
TOKEN,
HEALTH_CHECK_PATH,
VAULT_TOKEN_TTL_DEFAULT,
value));
assertThat(throwable.getMessage()).isEqualTo("Vault token renew buffer value must be less than ttl value");
}

private HashicorpVaultSettings createSettings(String url,
String token,
String healthCheckPath,
long ttl,
long renewBuffer) {
Expand All @@ -114,7 +96,6 @@ private HashicorpVaultSettings createSettings(String url,
.healthCheckEnabled(true)
.healthCheckPath(healthCheckPath)
.healthStandbyOk(true)
.token(token)
.ttl(ttl)
.renewBuffer(renewBuffer)
.secretPath(SECRET_PATH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.json.Json;
import org.assertj.core.api.Assertions;
import org.eclipse.edc.junit.annotations.ComponentTest;
import org.eclipse.edc.spi.monitor.ConsoleMonitor;
import org.eclipse.edc.vault.hashicorp.auth.HashicorpVaultTokenProviderImpl;
Expand All @@ -34,6 +33,7 @@
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.edc.http.client.testfixtures.HttpTestUtils.testHttpClient;
import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat;
import static org.testcontainers.shaded.org.awaitility.Awaitility.await;
Expand All @@ -59,7 +59,7 @@ abstract static class Tests {

@BeforeEach
void beforeEach() throws IOException, InterruptedException {
Assertions.assertThat(CREATION_TTL).isGreaterThan(TTL);
assertThat(CREATION_TTL).isGreaterThan(TTL);
}

@Test
Expand All @@ -77,15 +77,15 @@ void lookUpToken_whenTokenExpired_shouldFail() {
.untilAsserted(() -> {
var tokenLookUpResult = tokenRenewService.isTokenRenewable();
assertThat(tokenLookUpResult).isFailed();
Assertions.assertThat(tokenLookUpResult.getFailureDetail()).isEqualTo("Token look up failed with status 403");
assertThat(tokenLookUpResult.getFailureDetail()).isEqualTo("Token look up failed with status 403");
});
}

@Test
void renewToken_whenTokenNotExpired_shouldSucceed() {
var tokenRenewResult = tokenRenewService.renewToken();

assertThat(tokenRenewResult).isSucceeded().satisfies(ttl -> Assertions.assertThat(ttl).isEqualTo(TTL));
assertThat(tokenRenewResult).isSucceeded().satisfies(ttl -> assertThat(ttl).isEqualTo(TTL));
}

@Test
Expand All @@ -96,7 +96,7 @@ void renewToken_whenTokenExpired_shouldFail() {
.untilAsserted(() -> {
var tokenRenewResult = tokenRenewService.renewToken();
assertThat(tokenRenewResult).isFailed();
Assertions.assertThat(tokenRenewResult.getFailureDetail()).isEqualTo("Token renew failed with status: 403");
assertThat(tokenRenewResult.getFailureDetail()).isEqualTo("Token renew failed with status: 403");
});
}
}
Expand All @@ -109,15 +109,16 @@ class LastKnownFoss extends Tests {
static final VaultContainer<?> VAULT_CONTAINER = new VaultContainer<>("vault:1.9.6")
.withVaultToken(UUID.randomUUID().toString());

public static HashicorpVaultSettings getSettings() throws IOException, InterruptedException {
@BeforeEach
void beforeEach() throws IOException, InterruptedException {
var execResult = VAULT_CONTAINER.execInContainer(
"vault",
"token",
"create",
"-policy=root",
"-ttl=%d".formatted(CREATION_TTL),
"-format=json");

var jsonParser = Json.createParser(new StringReader(execResult.getStdout()));
jsonParser.next();
var auth = jsonParser.getObjectStream().filter(e -> e.getKey().equals(AUTH_KEY))
Expand All @@ -126,24 +127,19 @@ public static HashicorpVaultSettings getSettings() throws IOException, Interrupt
.orElseThrow()
.asJsonObject();
var clientToken = auth.getString(CLIENT_TOKEN_KEY);

return HashicorpVaultSettings.Builder.newInstance()
var settings = HashicorpVaultSettings.Builder.newInstance()
.url(HTTP_URL_FORMAT.formatted(VAULT_CONTAINER.getHost(), VAULT_CONTAINER.getFirstMappedPort()))
.healthCheckPath(HEALTH_CHECK_PATH)
.token(clientToken)
.ttl(TTL)
.renewBuffer(RENEW_BUFFER)
.build();
}

@BeforeEach
void beforeEach() throws IOException, InterruptedException {
var settings = getSettings();

tokenRenewService = new HashicorpVaultTokenRenewService(
testHttpClient(),
mapper,
settings,
new HashicorpVaultTokenProviderImpl(settings.token()),
new HashicorpVaultTokenProviderImpl(clientToken),
monitor
);
}
Expand All @@ -157,15 +153,16 @@ class Latest extends Tests {
static final VaultContainer<?> VAULT_CONTAINER = new VaultContainer<>("hashicorp/vault:1.18.3")
.withVaultToken(UUID.randomUUID().toString());

public static HashicorpVaultSettings getSettings() throws IOException, InterruptedException {
@BeforeEach
void beforeEach() throws IOException, InterruptedException {
var execResult = VAULT_CONTAINER.execInContainer(
"vault",
"token",
"create",
"-policy=root",
"-ttl=%d".formatted(CREATION_TTL),
"-format=json");

var jsonParser = Json.createParser(new StringReader(execResult.getStdout()));
jsonParser.next();
var auth = jsonParser.getObjectStream().filter(e -> e.getKey().equals(AUTH_KEY))
Expand All @@ -174,24 +171,19 @@ public static HashicorpVaultSettings getSettings() throws IOException, Interrupt
.orElseThrow()
.asJsonObject();
var clientToken = auth.getString(CLIENT_TOKEN_KEY);

return HashicorpVaultSettings.Builder.newInstance()
var settings = HashicorpVaultSettings.Builder.newInstance()
.url(HTTP_URL_FORMAT.formatted(VAULT_CONTAINER.getHost(), VAULT_CONTAINER.getFirstMappedPort()))
.healthCheckPath(HEALTH_CHECK_PATH)
.token(clientToken)
.ttl(TTL)
.renewBuffer(RENEW_BUFFER)
.build();
}

@BeforeEach
void beforeEach() throws IOException, InterruptedException {
var settings = getSettings();

tokenRenewService = new HashicorpVaultTokenRenewService(
testHttpClient(),
mapper,
settings,
new HashicorpVaultTokenProviderImpl(settings.token()),
new HashicorpVaultTokenProviderImpl(clientToken),
monitor
);
}
Expand Down
Loading