Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Path only #32806

Merged
merged 5 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 21 additions & 0 deletions container-disc/src/main/java/ai/vespa/secret/model/VaultId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package ai.vespa.secret.model;

/**
* Internally created id for a vault. Usually a UUID.
*
* @author gjoranv
*/
public record VaultId(String value) {

public VaultId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Version id cannot be null or empty");
}
}

public static VaultId of(String value) {
return new VaultId(value);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
*/
public class VaultName extends PatternedStringWrapper<VaultName> {

private static final Pattern namePattern = Pattern.compile("[.a-zA-Z0-9_-]{1,64}");
private static final Pattern namePattern = Pattern.compile("[.a-zA-Z0-9_-]{1,64}");

private VaultName(String name) {
private VaultName(String name) {
super(name, namePattern, "Vault name");
}

public static VaultName of(String name) {
public static VaultName of(String name) {
return new VaultName(name);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AwsRole;
import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient;
import com.yahoo.vespa.athenz.client.zts.ZtsClient;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
Expand Down Expand Up @@ -64,7 +63,7 @@ private AsmSecretReader(ZtsClient ztsClient, AthenzDomain domain) {
}

// For testing
public AsmSecretReader(Function<AwsRole, SecretsManagerClient> clientAndCredentialsSupplier) {
public AsmSecretReader(Function<AwsRolePath, SecretsManagerClient> clientAndCredentialsSupplier) {
super(clientAndCredentialsSupplier);
cache = initCache();
ztsClientCloser = () -> {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import ai.vespa.secret.model.VaultName;
import com.yahoo.component.AbstractComponent;
import com.yahoo.vespa.athenz.api.AthenzDomain;
import com.yahoo.vespa.athenz.api.AwsRole;
import com.yahoo.vespa.athenz.api.AwsTemporaryCredentials;
import com.yahoo.vespa.athenz.aws.AwsCredentials;
import com.yahoo.vespa.athenz.client.zts.ZtsClient;
Expand All @@ -13,11 +12,11 @@
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* Base class for AWS Secrets Manager read or write clients.
Expand All @@ -28,9 +27,9 @@ public abstract class AsmSecretStoreBase extends AbstractComponent implements Au

public static final String AWSCURRENT = "AWSCURRENT";

private final Function<AwsRole, SecretsManagerClient> clientAndCredentialsSupplier;
private final Function<AwsRolePath, SecretsManagerClient> clientAndCredentialsSupplier;

private final ConcurrentMap<AwsRole, SecretsManagerClient> clientMap = new ConcurrentHashMap<>();
private final ConcurrentMap<AwsRolePath, SecretsManagerClient> clientMap = new ConcurrentHashMap<>();


public AsmSecretStoreBase(ZtsClient ztsClient, AthenzDomain athenzDomain) {
Expand All @@ -41,12 +40,12 @@ public AsmSecretStoreBase(ZtsClient ztsClient, AthenzDomain athenzDomain) {
}

// For testing
protected AsmSecretStoreBase(Function<AwsRole, SecretsManagerClient> clientAndCredentialsSupplier) {
protected AsmSecretStoreBase(Function<AwsRolePath, SecretsManagerClient> clientAndCredentialsSupplier) {
this.clientAndCredentialsSupplier = clientAndCredentialsSupplier;
}

/** Returns the AWS role associated with the given vault. */
protected abstract AwsRole awsRole(VaultName vault);
protected abstract AwsRolePath awsRole(VaultName vault);


protected SecretsManagerClient getClient(VaultName vault) {
Expand All @@ -55,11 +54,11 @@ protected SecretsManagerClient getClient(VaultName vault) {
return clientMap.get(awsRole);
}

private static AwsCredentialsProvider getAwsSessionCredsProvider(AwsRole role,
private static AwsCredentialsProvider getAwsSessionCredsProvider(AwsRolePath role,
ZtsClient ztsClient,
AthenzDomain athenzDomain) {

AwsCredentials credentials = new AwsCredentials(ztsClient, athenzDomain, role);
AwsCredentials credentials = new AwsCredentials(ztsClient, athenzDomain, role.athenzAwsRole());
return () -> {
AwsTemporaryCredentials temporary = credentials.get();
return AwsSessionCredentials.create(temporary.accessKeyId(),
Expand All @@ -86,8 +85,8 @@ public void deconstruct() {
}

// Only for testing
public Set<String> clientRoleNames() {
return clientMap.keySet().stream().map(AwsRole::name).collect(Collectors.toSet());
public Set<AwsRolePath> clientRoleNames() {
return new HashSet<>(clientMap.keySet());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import ai.vespa.secret.model.Key;
import ai.vespa.secret.model.VaultName;
import com.yahoo.component.annotation.Inject;
import com.yahoo.vespa.athenz.api.AwsRole;
import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;

Expand All @@ -29,16 +28,16 @@ public AsmTenantSecretReader(AsmSecretConfig config, ServiceIdentityProvider ide
}

// For testing
AsmTenantSecretReader(Function<AwsRole, SecretsManagerClient> clientAndCredentialsSupplier,
AsmTenantSecretReader(Function<AwsRolePath, SecretsManagerClient> clientAndCredentialsSupplier,
String system, String tenant) {
super(clientAndCredentialsSupplier);
this.system = system;
this.tenant = tenant;
}

@Override
protected AwsRole awsRole(VaultName vault) {
return new AwsRole(AthenzUtil.resourceEntityName(system, tenant, vault));
protected AwsRolePath awsRole(VaultName vault) {
return AthenzUtil.awsReaderRole(system, tenant, vault);
}

@Override
Expand Down
32 changes: 27 additions & 5 deletions jdisc-cloud-aws/src/main/java/ai/vespa/secret/aws/AthenzUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,53 @@

import ai.vespa.secret.model.Role;
import ai.vespa.secret.model.VaultName;
import com.yahoo.vespa.athenz.api.AwsRole;


/**
* Tenant secret store constants and functions used across modules and repos.
* Note that we cannot use SystemName and TenantName from config-provision here,
* as that bundle is not available in tenant containers.
*
* @author gjoranv
*/
public class AthenzUtil {

// Serves as a namespace for resources in athenz and AWS
public static final String PREFIX = "tenant-secret";

/* tenant-secret.<system>.<tenant> */
public static String roleAndPolicyPrefix(String systemName, String tenantName) {
return "%s.%s.%s".formatted(PREFIX, systemName, tenantName).toLowerCase();
return String.join(".", PREFIX, systemName, tenantName).toLowerCase();
}

/* tenant-secret.<system>.<tenant>.<vaultName>.reader */
public static String resourceEntityName(String system, String tenant, VaultName vault) {
// Note that the domain name is added by AthenzDomainName, such that actual resource name will be
// e.g. vespa.external.tenant-secret:<aws-role-name>
// The aws role name is: tenant-secret.<system>.<tenant>.<vault>.reader
return "%s.%s.%s".formatted(roleAndPolicyPrefix(system, tenant),
vault.value(),
Role.READER.value())
return "%s.%s".formatted(roleAndPolicyPrefix(system, tenant),
athenzReaderRoleName(vault))
.toLowerCase();
}

/* Path: /tenant-secret/<system>/<tenant>/ */
public static AwsPath awsPath(String systemName, String tenantName) {
return AwsPath.of(PREFIX, systemName, tenantName);
}

/*
* Path: /tenant-secret/<system>/<tenant>/ + Role: <vaultId>.reader
*
* We use vaultId instead of vaultName because vaultName is not unique across tenants,
* and role names must be unique across paths within an account.
*/
public static AwsRolePath awsReaderRole(String systemName, String tenantName, VaultName vault) {
return new AwsRolePath(awsPath(systemName, tenantName), new AwsRole(athenzReaderRoleName(vault)));
}

/* <vaultName>.reader */
private static String athenzReaderRoleName(VaultName vault) {
return "%s.%s".formatted(vault.value(), Role.READER.value());
}

}
52 changes: 52 additions & 0 deletions jdisc-cloud-aws/src/main/java/ai/vespa/secret/aws/AwsPath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* // Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
*
*/

package ai.vespa.secret.aws;

import ai.vespa.validation.PatternedStringWrapper;

import java.util.regex.Pattern;
import java.util.stream.Stream;

import static java.util.stream.Collectors.joining;

/**
* @author gjoranv
*/
public class AwsPath extends PatternedStringWrapper<AwsPath> {

// The limit for a path in AWS is 512 chars, but we can limit ourselves to
// "/tenant-secret.<system>.<tenant>/".
// Here, we assume 128 chars tenant name and max 30 chars system name
private static final Pattern namePattern = Pattern.compile("/[/.a-zA-Z0-9_-]{0,173}");


private AwsPath(String name) {
super(name, namePattern, "AWS path");
}

/** Creates a new path from the given elements, adding the leading and trailing slashes */
public static AwsPath of(String... elements) {
if (elements != null && elements.length > 0 && elements[0] != null && ! elements[0].isEmpty()) {
return new AwsPath(
Stream.of(elements)
.map(String::toLowerCase)
// TODO: consider using '.' as delimiter, to prevent deep paths
.collect(joining("/", "/", "/")));

}
return new AwsPath("/");
}

public static AwsPath root() {
return new AwsPath("/");
}

/** Creates a new instance from the given string, that is assumed to contain the leading and trailing slashes */
public static AwsPath fromAwsPathString(String path) {
return new AwsPath(path);
}

}
55 changes: 55 additions & 0 deletions jdisc-cloud-aws/src/main/java/ai/vespa/secret/aws/AwsRolePath.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* // Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
*
*/

package ai.vespa.secret.aws;

import com.yahoo.vespa.athenz.api.AwsRole;

import java.util.Objects;

/**
* An AWS role with path. We use paths because AWS roles can only be up to 64 chars long.
* Note that AWS role names must be unique across paths within an account.
*
* @author gjoranv
*/
public record AwsRolePath(AwsPath path, AwsRole role) {

public AwsRolePath {
Objects.requireNonNull(path, "path cannot be null");
Objects.requireNonNull(role, "role cannot be null");
}

public static AwsRolePath fromStrings(String path, String roleName) {
if (roleName == null || roleName.isEmpty()) {
throw new IllegalArgumentException("roleName cannot be null or empty");
}
return new AwsRolePath(AwsPath.fromAwsPathString(path), new AwsRole(roleName));
}

public static AwsRolePath atRoot(String roleName) {
return new AwsRolePath(AwsPath.of(), new AwsRole(roleName));
}

// When used as an Athenz resource name, the leading '/' must be removed
public String athenzResourceName() {
return fullName().substring(1);
}

// Only for compatibility with existing APIs in AwsCredentials
public AwsRole athenzAwsRole() {
return new AwsRole(athenzResourceName());
}

@Override
public String toString() {
return "AwsRolePath{" + path + ", " + role.name() + '}';
}

private String fullName() {
return "%s%s".formatted(path.value(), role.name());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

package ai.vespa.secret.aws.testutil;

import ai.vespa.secret.aws.AwsRolePath;
import ai.vespa.secret.model.Key;
import ai.vespa.secret.model.SecretVersionState;
import com.yahoo.vespa.athenz.api.AwsRole;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueRequest;
import software.amazon.awssdk.services.secretsmanager.model.GetSecretValueResponse;
import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException;
Expand All @@ -30,14 +30,14 @@ public void put(Key key, SecretVersion... versions) {
secrets.put(awsSecretIdMapper.apply(key), List.of(versions));
}

public MockSecretsReader newClient(AwsRole awsRole) {
public MockSecretsReader newClient(AwsRolePath awsRole) {
return new MockSecretsReader(awsRole);
}


public class MockSecretsReader extends MockSecretsManagerClient {

MockSecretsReader(AwsRole awsRole) {
MockSecretsReader(AwsRolePath awsRole) {
super(awsRole);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

package ai.vespa.secret.aws.testutil;

import ai.vespa.secret.aws.AwsRolePath;
import ai.vespa.secret.model.Key;
import ai.vespa.secret.model.SecretVersionState;
import com.yahoo.vespa.athenz.api.AwsRole;
import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
Expand Down Expand Up @@ -58,10 +58,10 @@ public List<MockSecretsManagerClient> clients() {

public abstract class MockSecretsManagerClient implements SecretsManagerClient {

public final AwsRole awsRole;
public final AwsRolePath awsRole;
public boolean isClosed = false;

protected MockSecretsManagerClient(AwsRole awsRole) {
protected MockSecretsManagerClient(AwsRolePath awsRole) {
this.awsRole = awsRole;
clients.add(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ AsmTenantSecretReader secretReader() {
@Test
void it_creates_one_credentials_and_client_per_vault_and_closes_them() {
var vault1 = VaultName.of("vault1");
var awsRole1 = "tenant-secret.publiccd.tenant1.vault1.reader";
var awsRole1 = AwsRolePath.fromStrings("/tenant-secret/publiccd/tenant1/", "vault1.reader");
var vault2 = VaultName.of("vault2");
var awsRole2 = "tenant-secret.publiccd.tenant1.vault2.reader";
var awsRole2 = AwsRolePath.fromStrings("/tenant-secret/publiccd/tenant1/", "vault2.reader");

var secret1 = new SecretVersion("1", SecretVersionState.CURRENT, "secret1");
var secret2 = new SecretVersion("2", SecretVersionState.CURRENT, "secret2");
Expand All @@ -59,7 +59,7 @@ void it_creates_one_credentials_and_client_per_vault_and_closes_them() {
tester.put(key1, secret1);
tester.put(key2, secret2);

try (var reader = secretReader()){
try (var reader = secretReader()) {
reader.getSecret(key1);
reader.getSecret(key2);

Expand Down
Loading