| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.knox.gateway.backend.hashicorp.vault; |
| |
| import org.apache.knox.gateway.backend.hashicorp.vault.authentication.HashicorpVaultClientAuthenticationProvider; |
| import org.apache.knox.gateway.backend.hashicorp.vault.authentication.TokenHashicorpVaultClientAuthenticationProvider; |
| import org.apache.knox.gateway.config.GatewayConfig; |
| import org.apache.knox.gateway.services.security.AliasService; |
| import org.apache.knox.gateway.services.security.AliasServiceException; |
| import org.apache.knox.test.category.VerifyTest; |
| import org.easymock.EasyMock; |
| import org.junit.After; |
| import org.junit.AfterClass; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| import org.junit.experimental.categories.Category; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.testcontainers.containers.Container; |
| import org.testcontainers.containers.GenericContainer; |
| import org.testcontainers.containers.wait.strategy.Wait; |
| import org.testcontainers.images.builder.Transferable; |
| import org.testcontainers.vault.VaultContainer; |
| |
| import java.nio.charset.StandardCharsets; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Random; |
| |
| import static org.apache.knox.gateway.backend.hashicorp.vault.HashicorpVaultAliasService.VAULT_SEPARATOR; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.junit.Assert.fail; |
| import static org.junit.Assume.assumeNoException; |
| |
| @Category(VerifyTest.class) |
| public class TestHashicorpVaultAliasService { |
| private static final Logger LOG = LoggerFactory.getLogger(TestHashicorpVaultAliasService.class); |
| |
| private static final Random RANDOM = new Random(); |
| private static final String vaultVersion = "1.2.1"; |
| private static final String vaultImage = "vault:" + vaultVersion; |
| private static final Integer vaultPort = 8200; |
| private static final String vaultToken = "myroot"; |
| private String vaultSecretsEngine; |
| |
| private static GenericContainer vaultContainer; |
| private static String vaultAddress; |
| |
| @BeforeClass |
| public static void setUpClass() { |
| try { |
| vaultContainer = new VaultContainer(vaultImage) |
| .withVaultToken(vaultToken) |
| .waitingFor(Wait.forListeningPort()); |
| vaultContainer.addExposedPort(vaultPort); |
| } catch (Exception e) { |
| assumeNoException(e); |
| } |
| |
| vaultContainer.start(); |
| vaultAddress = String.format(Locale.ROOT, |
| "http://%s:%s", |
| vaultContainer.getContainerIpAddress(), |
| vaultContainer.getMappedPort(vaultPort)); |
| |
| assertTrue(vaultContainer.isRunning()); |
| } |
| |
| @Before |
| public void setUp() throws Exception { |
| vaultSecretsEngine = "knox-secret-" + RANDOM.nextInt(100); |
| |
| setupVaultSecretsEngine(); |
| } |
| |
| private void setupVaultSecretsEngine() throws Exception { |
| Container.ExecResult execResult = vaultContainer.execInContainer("vault", "secrets", |
| "enable", "-path=" + vaultSecretsEngine, "-version=2", "kv"); |
| assertEquals(0, execResult.getExitCode()); |
| LOG.debug("created KV secrets engine {}", vaultSecretsEngine); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| cleanupVaultPolicy(); |
| cleanupVaultSecretsEngine(); |
| |
| vaultSecretsEngine = null; |
| } |
| |
| private void cleanupVaultSecretsEngine() throws Exception { |
| vaultContainer.execInContainer("vault", "secrets", "disable", vaultSecretsEngine); |
| LOG.debug("deleted KV secrets engine {}", vaultSecretsEngine); |
| } |
| |
| @AfterClass |
| public static void tearDownClass() { |
| if(vaultContainer != null) { |
| vaultContainer.stop(); |
| } |
| } |
| |
| private String getKnoxToken(boolean forceKnoxSpecifcToken) throws Exception { |
| String token; |
| if(forceKnoxSpecifcToken) { |
| LOG.info("Using Knox specific token"); |
| Container.ExecResult tokenCreationExecResult = vaultContainer.execInContainer("vault", "token", |
| "create", "-policy=" + getVaultPolicy(), "-field=token"); |
| token = tokenCreationExecResult.getStdout().replaceAll("\\s", "").trim(); |
| } else { |
| LOG.info("Using root token"); |
| token = vaultToken; |
| } |
| return token; |
| } |
| |
| private String getVaultPolicy() { |
| return vaultSecretsEngine + "-policy"; |
| } |
| |
| @Test |
| public void testVaultIntegration() throws Exception { |
| String vaultPathPrefix = generatePathPrefix(); |
| setupVaultPolicy(VAULT_SEPARATOR); |
| |
| GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class); |
| |
| Map<String, String> remoteAliasServiceConfiguration = new HashMap<>(); |
| remoteAliasServiceConfiguration.put(HashicorpVaultAliasService.VAULT_ADDRESS_KEY, |
| vaultAddress); |
| remoteAliasServiceConfiguration.put(HashicorpVaultAliasService.VAULT_SECRETS_ENGINE_KEY, |
| vaultSecretsEngine); |
| remoteAliasServiceConfiguration.put(HashicorpVaultAliasService.VAULT_PATH_PREFIX_KEY, |
| vaultPathPrefix); |
| remoteAliasServiceConfiguration.put(HashicorpVaultClientAuthenticationProvider.AUTHENTICATION_TYPE_KEY, |
| TokenHashicorpVaultClientAuthenticationProvider.TYPE); |
| remoteAliasServiceConfiguration.put(TokenHashicorpVaultClientAuthenticationProvider.TOKEN_KEY, |
| getKnoxToken(RANDOM.nextBoolean())); |
| |
| EasyMock.expect(gatewayConfig.getRemoteAliasServiceConfiguration()) |
| .andReturn(remoteAliasServiceConfiguration).anyTimes(); |
| EasyMock.replay(gatewayConfig); |
| |
| AliasService localAliasService = EasyMock.createNiceMock(AliasService.class); |
| |
| AliasService aliasService = new HashicorpVaultAliasService(localAliasService); |
| aliasService.init(gatewayConfig, Collections.emptyMap()); |
| aliasService.start(); |
| |
| String clusterName = "test-" + RANDOM.nextInt(100); |
| String alias = "abc-" + RANDOM.nextInt(100); |
| String aliasPassword = "def-" + RANDOM.nextInt(100); |
| |
| assertEquals(0, aliasService.getAliasesForCluster(clusterName).size()); |
| |
| aliasService.addAliasForCluster(clusterName, alias, aliasPassword); |
| |
| assertEquals(1, aliasService.getAliasesForCluster(clusterName).size()); |
| |
| char[] vaultAliasPassword = aliasService.getPasswordFromAliasForCluster(clusterName, alias); |
| assertEquals(aliasPassword, String.valueOf(vaultAliasPassword)); |
| |
| aliasService.removeAliasForCluster(clusterName, alias); |
| assertNull(aliasService.getPasswordFromAliasForCluster(clusterName, alias)); |
| assertEquals(0, aliasService.getAliasesForCluster(clusterName).size()); |
| |
| aliasService.stop(); |
| } |
| |
| @Test |
| public void testVaultIntegrationPermissions() throws Exception { |
| String vaultPathPrefix = generatePathPrefix(); |
| setupVaultPolicy("/invalidPrefix/"); |
| |
| GatewayConfig gatewayConfig = EasyMock.createNiceMock(GatewayConfig.class); |
| |
| Map<String, String> remoteAliasServiceConfiguration = new HashMap<>(); |
| remoteAliasServiceConfiguration.put(HashicorpVaultAliasService.VAULT_ADDRESS_KEY, vaultAddress); |
| remoteAliasServiceConfiguration.put(HashicorpVaultAliasService.VAULT_SECRETS_ENGINE_KEY, |
| vaultSecretsEngine); |
| remoteAliasServiceConfiguration.put(HashicorpVaultAliasService.VAULT_PATH_PREFIX_KEY, |
| vaultPathPrefix); |
| remoteAliasServiceConfiguration.put(HashicorpVaultClientAuthenticationProvider.AUTHENTICATION_TYPE_KEY, |
| TokenHashicorpVaultClientAuthenticationProvider.TYPE); |
| remoteAliasServiceConfiguration.put(TokenHashicorpVaultClientAuthenticationProvider.TOKEN_KEY, |
| getKnoxToken(true)); |
| |
| EasyMock.expect(gatewayConfig.getRemoteAliasServiceConfiguration()) |
| .andReturn(remoteAliasServiceConfiguration).anyTimes(); |
| EasyMock.replay(gatewayConfig); |
| |
| AliasService localAliasService = EasyMock.createNiceMock(AliasService.class); |
| |
| AliasService aliasService = new HashicorpVaultAliasService(localAliasService); |
| aliasService.init(gatewayConfig, Collections.emptyMap()); |
| aliasService.start(); |
| |
| String clusterName = "test-" + RANDOM.nextInt(100); |
| String alias = "abc-" + RANDOM.nextInt(100); |
| String aliasPassword = "def-" + RANDOM.nextInt(100); |
| |
| try { |
| aliasService.getAliasesForCluster(clusterName); |
| fail("Should have gotten a 403"); |
| } catch (AliasServiceException e) { |
| assertTrue(e.getMessage().contains("Status 403 Forbidden")); |
| } |
| |
| try { |
| aliasService.addAliasForCluster(clusterName, alias, aliasPassword); |
| fail("Should have gotten a 403"); |
| } catch (AliasServiceException e) { |
| assertTrue(e.getMessage().contains("Status 403 Forbidden")); |
| } |
| |
| try { |
| aliasService.getPasswordFromAliasForCluster(clusterName, alias); |
| fail("Should have gotten a 403"); |
| } catch (AliasServiceException e) { |
| assertTrue(e.getMessage().contains("Status 403 Forbidden")); |
| } |
| |
| try { |
| aliasService.removeAliasForCluster(clusterName, alias); |
| fail("Should have gotten a 403"); |
| } catch (AliasServiceException e) { |
| assertTrue(e.getMessage().contains("Status 403 Forbidden")); |
| } |
| |
| aliasService.stop(); |
| } |
| |
| private String generatePathPrefix() { |
| StringBuilder pathPrefix = new StringBuilder(); |
| int numParts = RANDOM.nextInt(10); |
| for(int i = 0; i < numParts; i++) { |
| pathPrefix.append(VAULT_SEPARATOR).append(RANDOM.nextInt(10)); |
| } |
| pathPrefix.append(VAULT_SEPARATOR); |
| String result = pathPrefix.toString(); |
| LOG.info("Using path prefix: '{}'", result); |
| return result; |
| } |
| |
| private void setupVaultPolicy(String pathPrefix) throws Exception { |
| String policy = "path \"" + vaultSecretsEngine + pathPrefix + "*\" {\n" + |
| " capabilities = [\"create\", \"read\", \"update\", \"delete\", \"list\"]\n" + |
| "}"; |
| LOG.info("policy: {}", policy); |
| String policyFilePath = "/tmp/" + getVaultPolicy() + ".hcl"; |
| vaultContainer.copyFileToContainer(Transferable.of(policy.getBytes(StandardCharsets.UTF_8)), |
| policyFilePath); |
| vaultContainer.execInContainer("vault", "policy", "write", getVaultPolicy(), policyFilePath); |
| LOG.debug("created policy {}", getVaultPolicy()); |
| vaultContainer.execInContainer("rm", "-f", policyFilePath); |
| } |
| |
| private void cleanupVaultPolicy() { |
| try { |
| vaultContainer.execInContainer("vault", "policy", "delete", getVaultPolicy()); |
| LOG.debug("deleted policy {}", getVaultPolicy()); |
| } catch (Exception ignore) { |
| // ignore |
| } |
| } |
| } |