diff options
author | Morten Tokle <mortent@oath.com> | 2018-08-17 14:35:59 +0200 |
---|---|---|
committer | Morten Tokle <mortent@oath.com> | 2018-08-17 14:47:21 +0200 |
commit | f7015e9c2d4614797f20672da2ac89f31f8ed37a (patch) | |
tree | 2492ecb3cdea4d3a3a46b93adb6a84b3a4f2a942 /athenz-identity-provider-service | |
parent | 748725984486ddc14eeeb54c71c3017e445ef5c2 (diff) |
Validate refresh requests
Diffstat (limited to 'athenz-identity-provider-service')
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; + } } |