summaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service
diff options
context:
space:
mode:
authorMorten Tokle <mortent@oath.com>2018-08-17 14:35:59 +0200
committerMorten Tokle <mortent@oath.com>2018-08-17 14:47:21 +0200
commitf7015e9c2d4614797f20672da2ac89f31f8ed37a (patch)
tree2492ecb3cdea4d3a3a46b93adb6a84b3a4f2a942 /athenz-identity-provider-service
parent748725984486ddc14eeeb54c71c3017e445ef5c2 (diff)
Validate refresh requests
Diffstat (limited to 'athenz-identity-provider-service')
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java4
-rw-r--r--athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java82
-rw-r--r--athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java115
3 files changed, 182 insertions, 19 deletions
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java
index 4f70a7b9a10..e6dd40faaca 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceConfirmation.java
@@ -34,7 +34,7 @@ public class InstanceConfirmation {
@JsonProperty("attestationData") @JsonSerialize(using = SignedIdentitySerializer.class)
public final SignedIdentityDocumentEntity signedIdentityDocument;
- @JsonUnwrapped public final Map<String, Object> attributes = new HashMap<>(); // optional attributes that Athenz may provide
+ @JsonUnwrapped public final Map<String, String> attributes = new HashMap<>(); // optional attributes that Athenz may provide
@JsonCreator
public InstanceConfirmation(@JsonProperty("provider") String provider,
@@ -49,7 +49,7 @@ public class InstanceConfirmation {
}
@JsonAnySetter
- public void set(String name, Object value) {
+ public void set(String name, String value) {
attributes.put(name, value);
}
diff --git a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java
index dcaf50c1c04..3d575bddcf8 100644
--- a/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java
+++ b/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidator.java
@@ -1,6 +1,7 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation;
+import com.google.common.net.InetAddresses;
import com.google.inject.Inject;
import com.yahoo.config.model.api.ApplicationInfo;
import com.yahoo.config.model.api.ServiceInfo;
@@ -13,10 +14,17 @@ import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
import com.yahoo.vespa.athenz.identityprovider.client.IdentityDocumentSigner;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import java.net.InetAddress;
import java.security.PublicKey;
+import java.util.Arrays;
+import java.util.List;
import java.util.Optional;
import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* Verifies that the instance's identity document is valid
@@ -29,15 +37,20 @@ public class InstanceValidator {
private static final Logger log = Logger.getLogger(InstanceValidator.class.getName());
static final String SERVICE_PROPERTIES_DOMAIN_KEY = "identity.domain";
static final String SERVICE_PROPERTIES_SERVICE_KEY = "identity.service";
+ static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz.";
private final IdentityDocumentSigner signer = new IdentityDocumentSigner();
private final KeyProvider keyProvider;
private final SuperModelProvider superModelProvider;
+ private final NodeRepository nodeRepository;
@Inject
- public InstanceValidator(KeyProvider keyProvider, SuperModelProvider superModelProvider) {
+ public InstanceValidator(KeyProvider keyProvider,
+ SuperModelProvider superModelProvider,
+ NodeRepository nodeRepository) {
this.keyProvider = keyProvider;
this.superModelProvider = superModelProvider;
+ this.nodeRepository = nodeRepository;
}
public boolean isValidInstance(InstanceConfirmation instanceConfirmation) {
@@ -68,8 +81,71 @@ public class InstanceValidator {
log.log(LogLevel.INFO, () -> String.format("Accepting refresh for instance with identity '%s', provider '%s', instanceId '%s'.",
new AthenzService(confirmation.domain, confirmation.service).getFullName(),
confirmation.provider,
- confirmation.attributes.get("sanDNS").toString()));
- return true;
+ confirmation.attributes.get("sanDNS")));
+ try {
+ return validateAttributes(confirmation);
+ } catch (Exception e) {
+ log.log(LogLevel.INFO, "Encountered exception while refreshing certificate for confirmation: " + confirmation, e);
+ return true;
+ }
+ }
+
+ private boolean validateAttributes(InstanceConfirmation confirmation) {
+ // Find a list of SAN DNS
+ List<String> sanDNS = Optional.ofNullable(confirmation.attributes.get("sanDNS"))
+ .map(s -> s.split(","))
+ .map(Arrays::asList)
+ .map(List::stream)
+ .orElse(Stream.empty())
+ .collect(Collectors.toList());
+
+ VespaUniqueInstanceId vespaUniqueInstanceId = sanDNS.stream()
+ .filter(dns -> dns.contains(INSTANCE_ID_DELIMITER))
+ .findFirst()
+ .map(s -> s.replaceAll(INSTANCE_ID_DELIMITER + ".*", ""))
+ .map(VespaUniqueInstanceId::fromDottedString)
+ .orElse(null);
+ if(vespaUniqueInstanceId == null) {
+ log.log(LogLevel.WARNING, "Unabe to find unique instance ID in refresh request: " + confirmation.toString());
+ return false;
+ }
+
+ // Find node matching vespa unique id
+ Node node = nodeRepository.getNodes().stream()
+ .filter(n -> n.allocation().isPresent())
+ .filter(n -> nodeMatchesVespaUniqueId(n, vespaUniqueInstanceId))
+ .findFirst() // Should be only one
+ .orElse(null);
+ if(node == null) {
+ log.log(LogLevel.WARNING, "Invalid InstanceConfirmation, No nodes matching uniqueId: " + vespaUniqueInstanceId);
+ return false;
+ }
+
+ // Find list of ipaddresses
+ List<InetAddress> ips = Optional.ofNullable(confirmation.attributes.get("sanIP"))
+ .map(s -> s.split(","))
+ .map(Arrays::asList)
+ .map(List::stream)
+ .orElse(Stream.empty())
+ .map(InetAddresses::forString)
+ .collect(Collectors.toList());
+
+ List<InetAddress> nodeIpAddresses = node.ipAddresses().stream()
+ .map(InetAddresses::forString)
+ .collect(Collectors.toList());
+
+ // Validate that ipaddresses in request are valid for node
+ return nodeIpAddresses.containsAll(ips);
+ }
+
+ private boolean nodeMatchesVespaUniqueId(Node node, VespaUniqueInstanceId vespaUniqueInstanceId) {
+ return node.allocation().map(allocation ->
+ allocation.membership().index() == vespaUniqueInstanceId.clusterIndex() &&
+ allocation.membership().cluster().id().value().equals(vespaUniqueInstanceId.clusterId()) &&
+ allocation.owner().instance().value().equals(vespaUniqueInstanceId.instance()) &&
+ allocation.owner().application().value().equals(vespaUniqueInstanceId.application()) &&
+ allocation.owner().tenant().value().equals(vespaUniqueInstanceId.tenant()))
+ .orElse(false);
}
// If/when we dont care about logging exactly whats wrong, this can be simplified
diff --git a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java
index 56777325231..8beb8bda99f 100644
--- a/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java
+++ b/athenz-identity-provider-service/src/test/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/instanceconfirmation/InstanceValidatorTest.java
@@ -1,6 +1,9 @@
// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.athenz.instanceproviderservice.instanceconfirmation;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.yahoo.component.Version;
import com.yahoo.config.model.api.ApplicationInfo;
import com.yahoo.config.model.api.HostInfo;
import com.yahoo.config.model.api.Model;
@@ -8,12 +11,22 @@ import com.yahoo.config.model.api.ServiceInfo;
import com.yahoo.config.model.api.SuperModel;
import com.yahoo.config.model.api.SuperModelProvider;
import com.yahoo.config.provision.ApplicationId;
+import com.yahoo.config.provision.ClusterMembership;
+import com.yahoo.config.provision.NodeType;
+import com.yahoo.vespa.athenz.identityprovider.api.IdentityType;
+import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
+import com.yahoo.vespa.hosted.provision.Node;
+import com.yahoo.vespa.hosted.provision.NodeRepository;
+import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors;
import org.junit.Test;
+import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@@ -29,6 +42,7 @@ import static org.mockito.Mockito.when;
/**
* @author valerijf
* @author bjorncs
+ * @author mortent
*/
public class InstanceValidatorTest {
@@ -36,11 +50,10 @@ public class InstanceValidatorTest {
private final String domain = "domain";
private final String service = "service";
-
@Test
public void application_does_not_exist() {
SuperModelProvider superModelProvider = mockSuperModelProvider();
- InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider);
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null);
assertFalse(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service));
}
@@ -49,7 +62,7 @@ public class InstanceValidatorTest {
public void application_does_not_have_domain_set() {
SuperModelProvider superModelProvider = mockSuperModelProvider(
mockApplicationInfo(applicationId, 5, Collections.emptyList()));
- InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider);
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null);
assertFalse(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service));
}
@@ -57,11 +70,11 @@ public class InstanceValidatorTest {
@Test
public void application_has_wrong_domain() {
ServiceInfo serviceInfo = new ServiceInfo("serviceName", "type", Collections.emptyList(),
- Collections.singletonMap(SERVICE_PROPERTIES_DOMAIN_KEY, "not-domain"), "confId", "hostName");
+ Collections.singletonMap(SERVICE_PROPERTIES_DOMAIN_KEY, "not-domain"), "confId", "hostName");
SuperModelProvider superModelProvider = mockSuperModelProvider(
mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo)));
- InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider);
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null);
assertFalse(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service));
}
@@ -73,24 +86,82 @@ public class InstanceValidatorTest {
properties.put(SERVICE_PROPERTIES_SERVICE_KEY, service);
ServiceInfo serviceInfo = new ServiceInfo("serviceName", "type", Collections.emptyList(),
- properties, "confId", "hostName");
+ properties, "confId", "hostName");
SuperModelProvider superModelProvider = mockSuperModelProvider(
mockApplicationInfo(applicationId, 5, Collections.singletonList(serviceInfo)));
- InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider);
+ InstanceValidator instanceValidator = new InstanceValidator(null, superModelProvider, null);
assertTrue(instanceValidator.isSameIdentityAsInServicesXml(applicationId, domain, service));
}
+ @Test
+ public void accepts_valid_refresh_requests() {
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository);
+
+ List<Node> nodeList = createNodes(10);
+ Node node = nodeList.get(0);
+ nodeList = allocateNode(nodeList, node, applicationId);
+ when(nodeRepository.getNodes()).thenReturn(nodeList);
+ String nodeIp = node.ipAddresses().stream().findAny().orElseThrow(() -> new RuntimeException("No ipaddress for mocked node"));
+ InstanceConfirmation instanceConfirmation = createRefreshInstanceConfirmation(ImmutableList.of(nodeIp), applicationId);
+
+ assertTrue(instanceValidator.isValidRefresh(instanceConfirmation));
+ }
+
+ @Test
+ public void rejects_refresh_on_ip_mismatch() {
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository);
+
+ List<Node> nodeList = createNodes(10);
+ Node node = nodeList.get(0);
+ nodeList = allocateNode(nodeList, node, applicationId);
+ when(nodeRepository.getNodes()).thenReturn(nodeList);
+ String nodeIp = node.ipAddresses().stream().findAny().orElseThrow(() -> new RuntimeException("No ipaddress for mocked node"));
+
+ // Add invalid ip to list of ip addresses
+ InstanceConfirmation instanceConfirmation = createRefreshInstanceConfirmation(ImmutableList.of(nodeIp, "::ff"), applicationId);
+
+ assertFalse(instanceValidator.isValidRefresh(instanceConfirmation));
+ }
+
+ @Test
+ public void rejects_refresh_when_node_is_not_allocated() {
+ NodeRepository nodeRepository = mock(NodeRepository.class);
+ InstanceValidator instanceValidator = new InstanceValidator(null, null, nodeRepository);
+
+ List<Node> nodeList = createNodes(10);
+ when(nodeRepository.getNodes()).thenReturn(nodeList);
+ InstanceConfirmation instanceConfirmation = createRefreshInstanceConfirmation(ImmutableList.of("::11"), applicationId);
+
+ assertFalse(instanceValidator.isValidRefresh(instanceConfirmation));
+
+ }
+
+ private InstanceConfirmation createRefreshInstanceConfirmation(List<String> ips, ApplicationId applicationId) {
+ InstanceConfirmation instanceConfirmation = new InstanceConfirmation(
+ "vespa.vespa.cd.provider_dev_us-north-1",
+ "vespa.vespa.cd",
+ "tenant",
+ null);
+
+ instanceConfirmation.set("sanIP", String.join(",", ips));
+ VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(0, "default", applicationId.instance().value(), applicationId.application().value(), applicationId.tenant().value(), "us-north-1", "dev", IdentityType.NODE);
+ instanceConfirmation.set("sanDNS", vespaUniqueInstanceId.asDottedString() + ".instanceid.athenz.dev-us-north-1.vespa.yahoo.cloud");
+ return instanceConfirmation;
+ }
+
private SuperModelProvider mockSuperModelProvider(ApplicationInfo... appInfos) {
SuperModel superModel = new SuperModel(Stream.of(appInfos)
- .collect(Collectors.groupingBy(
- appInfo -> appInfo.getApplicationId().tenant(),
- Collectors.toMap(
- ApplicationInfo::getApplicationId,
- Function.identity()
- )
- )));
+ .collect(Collectors.groupingBy(
+ appInfo -> appInfo.getApplicationId().tenant(),
+ Collectors.toMap(
+ ApplicationInfo::getApplicationId,
+ Function.identity()
+ )
+ )));
SuperModelProvider superModelProvider = mock(SuperModelProvider.class);
when(superModelProvider.getSuperModel()).thenReturn(superModel);
@@ -107,4 +178,20 @@ public class InstanceValidatorTest {
return new ApplicationInfo(appId, 0, model);
}
+
+ private List<Node> createNodes(int num) {
+ MockNodeFlavors flavors = new MockNodeFlavors();
+ List<Node> nodeList = new ArrayList<>();
+ for (int i = 0; i < num; i++) {
+ Node node = Node.create("foo" + i, ImmutableSet.of("::1" + i, "::2" + i, "::3" + i), Collections.emptySet(), "foo" + i, Optional.empty(), flavors.getFlavorOrThrow("default"), NodeType.tenant);
+ nodeList.add(node);
+ }
+ return nodeList;
+ }
+
+ private List<Node> allocateNode(List<Node> nodeList, Node node, ApplicationId applicationId) {
+ nodeList.removeIf(n -> n.openStackId().equals(node.openStackId()));
+ nodeList.add(node.allocate(applicationId, ClusterMembership.from("container/default/0/0", Version.fromString("6.123.4")), Instant.now()));
+ return nodeList;
+ }
}