diff options
12 files changed, 0 insertions, 1292 deletions
diff --git a/node-repository/pom.xml b/node-repository/pom.xml index 6349993a3dc..86a56c14c52 100644 --- a/node-repository/pom.xml +++ b/node-repository/pom.xml @@ -79,18 +79,6 @@ </dependency> <dependency> <groupId>com.yahoo.vespa</groupId> - <artifactId>vespa-athenz</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>com.yahoo.vespa</groupId> - <artifactId>jdisc-security-filters</artifactId> - <version>${project.version}</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>com.yahoo.vespa</groupId> <artifactId>flags</artifactId> <version>${project.version}</version> <scope>provided</scope> diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java deleted file mode 100644 index 9934c343092..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilter.java +++ /dev/null @@ -1,72 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.google.inject.Inject; -import com.yahoo.config.provisioning.ConfigServerSecurityConfig; -import com.yahoo.jdisc.handler.ResponseHandler; -import com.yahoo.jdisc.http.filter.DiscFilterRequest; -import com.yahoo.jdisc.http.filter.SecurityRequestFilter; -import com.yahoo.vespa.hosted.provision.NodeRepository; -import com.yahoo.vespa.hosted.provision.restapi.v2.ErrorResponse; -import com.yahoo.yolean.chain.After; - -import java.net.URI; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.BiPredicate; -import java.util.logging.Logger; - -/** - * Authorization filter for all paths in config server. It assumes that {@link NodeIdentifierFilter} is part of filter chain. - * - * @author mpolden - * @author bjorncs - */ -@After("NodeIdentifierFilter") -public class AuthorizationFilter implements SecurityRequestFilter { - - private static final Logger log = Logger.getLogger(AuthorizationFilter.class.getName()); - - private final BiPredicate<NodePrincipal, URI> authorizer; - private final BiConsumer<ErrorResponse, ResponseHandler> rejectAction; - - @Inject - public AuthorizationFilter(NodeRepository nodeRepository, ConfigServerSecurityConfig securityConfig) { - this.authorizer = new Authorizer(nodeRepository, securityConfig); - this.rejectAction = AuthorizationFilter::logAndReject; - } - - @Override - public void filter(DiscFilterRequest request, ResponseHandler handler) { - validateAccess(request) - .ifPresent(errorResponse -> rejectAction.accept(errorResponse, handler)); - } - - private Optional<ErrorResponse> validateAccess(DiscFilterRequest request) { - try { - NodePrincipal hostIdentity = (NodePrincipal) request.getUserPrincipal(); - if (hostIdentity == null) - return Optional.of(ErrorResponse.internalServerError(createErrorMessage(request, "Principal is missing. NodeIdentifierFilter has not been applied."))); - if (!authorizer.test(hostIdentity, request.getUri())) - return Optional.of(ErrorResponse.forbidden(createErrorMessage(request, "Invalid credentials: " + hostIdentity.toString()))); - request.setUserPrincipal(hostIdentity); - return Optional.empty(); - } catch (NodeIdentifier.NodeIdentifierException e) { - return Optional.of(ErrorResponse.forbidden(createErrorMessage(request, "Invalid credentials: " + e.getMessage()))); - } - } - - private static String createErrorMessage(DiscFilterRequest request, String message) { - return String.format("%s %s denied for %s: %s", - request.getMethod(), - request.getUri().getPath(), - request.getRemoteAddr(), - message); - } - - private static void logAndReject(ErrorResponse response, ResponseHandler handler) { - log.warning(response.message()); - FilterUtils.write(response, handler); - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/Authorizer.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/Authorizer.java deleted file mode 100644 index 062a9c32afb..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/Authorizer.java +++ /dev/null @@ -1,211 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.config.provision.NodeType; -import com.yahoo.config.provisioning.ConfigServerSecurityConfig; -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.utils.AthenzIdentities; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.NodeRepository; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; - -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiPredicate; -import java.util.stream.Collectors; - -/** - * Authorizer for config server REST APIs. This contains the rules for all API paths where the authorization process - * may require information from the node-repository to make a decision - * - * @author mpolden - * @author bjorncs - */ -public class Authorizer implements BiPredicate<NodePrincipal, URI> { - private final NodeRepository nodeRepository; - private final String athenzProviderHostname; - private final AthenzIdentity controllerHostIdentity; - private final Set<AthenzIdentity> trustedIdentities; - private final Set<AthenzIdentity> hostAdminIdentities; - - Authorizer(NodeRepository nodeRepository, ConfigServerSecurityConfig securityConfig) { - AthenzIdentity configServerHostIdentity = AthenzIdentities.from(securityConfig.configServerHostIdentity()); - - this.nodeRepository = nodeRepository; - this.athenzProviderHostname = securityConfig.athenzProviderHostname(); - this.controllerHostIdentity = AthenzIdentities.from(securityConfig.controllerHostIdentity()); - this.trustedIdentities = Set.of(controllerHostIdentity, configServerHostIdentity); - this.hostAdminIdentities = Set.of( - controllerHostIdentity, - configServerHostIdentity, - AthenzIdentities.from(securityConfig.tenantHostIdentity()), - AthenzIdentities.from(securityConfig.proxyHostIdentity())); - } - - /** Returns whether principal is authorized to access given URI */ - @Override - public boolean test(NodePrincipal principal, URI uri) { - if (principal.getAthenzIdentityName().isPresent()) { - // All host admins can retrieve flags data - if (uri.getPath().equals("/flags/v1/data") || uri.getPath().equals("/flags/v1/data/")) { - return hostAdminIdentities.contains(principal.getAthenzIdentityName().get()); - } - - // Only controller can access everything else in flags - if (uri.getPath().startsWith("/flags/v1/")) { - return principal.getAthenzIdentityName().get().equals(controllerHostIdentity); - } - - // Trusted services can access everything - if (trustedIdentities.contains(principal.getAthenzIdentityName().get())) { - return true; - } - } - - if (principal.getHostname().isPresent()) { - String hostname = principal.getHostname().get(); - if (isAthenzProviderApi(uri)) { - return athenzProviderHostname.equals(hostname); - } - - // Individual nodes can only access their own resources - if (canAccessAll(hostnamesFrom(uri), principal, this::isSelfOrParent)) { - return true; - } - - // Nodes can access this resource if its type matches any of the valid node types - if (canAccessAny(nodeTypesFor(uri), principal, this::isNodeType)) { - return true; - } - } - return false; - } - - private static boolean isAthenzProviderApi(URI uri) { - return "/athenz/v1/provider/instance".equals(uri.getPath()) || - "/athenz/v1/provider/refresh".equals(uri.getPath()); - } - - /** Returns whether principal is the node itself or the parent of the node */ - private boolean isSelfOrParent(String hostname, NodePrincipal principal) { - // Node can always access itself - if (principal.getHostname().get().equals(hostname)) { - return true; - } - - // Parent node can access its children - return getNode(hostname).flatMap(Node::parentHostname) - .map(parentHostname -> principal.getHostname().get().equals(parentHostname)) - .orElse(false); - } - - /** Returns whether principal is a node of the given node type */ - private boolean isNodeType(NodeType type, NodePrincipal principal) { - return getNode(principal.getHostname().get()).map(node -> node.type() == type) - .orElse(false); - } - - /** Returns whether principal can access all given resources */ - private <T> boolean canAccessAll(List<T> resources, NodePrincipal principal, BiPredicate<T, NodePrincipal> predicate) { - return !resources.isEmpty() && resources.stream().allMatch(resource -> predicate.test(resource, principal)); - } - - /** Returns whether principal can access any of the given resources */ - private <T> boolean canAccessAny(List<T> resources, NodePrincipal principal, BiPredicate<T, NodePrincipal> predicate) { - return !resources.isEmpty() && resources.stream().anyMatch(resource -> predicate.test(resource, principal)); - } - - private Optional<Node> getNode(String hostname) { - // Ignore potential path traversal. Node repository happily passes arguments unsanitized all the way down to - // curator... - if (hostname.chars().allMatch(c -> c == '.')) { - return Optional.empty(); - } - return nodeRepository.getNode(hostname); - } - - /** Returns hostnames contained in query parameters of given URI */ - private static List<String> hostnamesFromQuery(URI uri) { - return URLEncodedUtils.parse(uri, StandardCharsets.UTF_8.name()) - .stream() - .filter(pair -> "hostname".equals(pair.getName()) || - "parentHost".equals(pair.getName())) - .map(NameValuePair::getValue) - .filter(hostname -> !hostname.isEmpty()) - .collect(Collectors.toList()); - } - - /** Returns hostnames from a URI if any, e.g. /nodes/v2/node/node1.fqdn */ - private static List<String> hostnamesFrom(URI uri) { - if (isChildOf("/nodes/v2/acl/", uri.getPath()) || - isChildOf("/nodes/v2/node/", uri.getPath()) || - isChildOf("/nodes/v2/state/", uri.getPath())) { - return Collections.singletonList(lastChildOf(uri.getPath())); - } - if (isChildOf("/orchestrator/v1/hosts/", uri.getPath())) { - return firstChildOf("/orchestrator/v1/hosts/", uri.getPath()) - .map(Collections::singletonList) - .orElseGet(Collections::emptyList); - } - if (isChildOf("/orchestrator/v1/suspensions/hosts/", uri.getPath())) { - List<String> hostnames = new ArrayList<>(); - hostnames.add(lastChildOf(uri.getPath())); - hostnames.addAll(hostnamesFromQuery(uri)); - return hostnames; - } - if (isChildOf("/nodes/v2/command/", uri.getPath()) || - "/nodes/v2/node/".equals(uri.getPath())) { - return hostnamesFromQuery(uri); - } - if (isChildOf("/athenz/v1/provider/identity-document", uri.getPath())) { - return Collections.singletonList(lastChildOf(uri.getPath())); - } - return Collections.emptyList(); - } - - /** Returns node types which can access given URI */ - private static List<NodeType> nodeTypesFor(URI uri) { - if (isChildOf("/routing/v1/", uri.getPath())) { - return Arrays.asList(NodeType.proxy, NodeType.proxyhost); - } - return Collections.emptyList(); - } - - /** Returns whether child is a sub-path of parent */ - private static boolean isChildOf(String parent, String child) { - return child.startsWith(parent) && child.length() > parent.length(); - } - - /** Returns the first component of path relative to root */ - private static Optional<String> firstChildOf(String root, String path) { - if (!isChildOf(root, path)) { - return Optional.empty(); - } - path = path.substring(root.length()); - int firstSeparator = path.indexOf('/'); - if (firstSeparator == -1) { - return Optional.of(path); - } - return Optional.of(path.substring(0, firstSeparator)); - } - - /** Returns the last component of the given path */ - private static String lastChildOf(String path) { - if (path.endsWith("/")) { - path = path.substring(0, path.length() - 1); - } - int lastSeparator = path.lastIndexOf("/"); - if (lastSeparator == -1) { - return path; - } - return path.substring(lastSeparator + 1); - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterUtils.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterUtils.java deleted file mode 100644 index 82d82f4694e..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.container.jdisc.HttpResponse; -import com.yahoo.jdisc.handler.FastContentWriter; -import com.yahoo.jdisc.handler.ResponseDispatch; -import com.yahoo.jdisc.handler.ResponseHandler; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; - -/** - * @author mpolden - */ -public class FilterUtils { - - private FilterUtils() {} - - /** Write HTTP response using given handler */ - public static void write(HttpResponse response, ResponseHandler handler) { - response.headers().put("Content-Type", response.getContentType()); - try (FastContentWriter writer = ResponseDispatch.newInstance(response.getJdiscResponse()) - .connectFastWriter(handler)) { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - try { - response.render(out); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - writer.write(out.toByteArray()); - } - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifier.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifier.java deleted file mode 100644 index ecc3f84f86c..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifier.java +++ /dev/null @@ -1,137 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.Zone; -import com.yahoo.config.provisioning.ConfigServerSecurityConfig; -import com.yahoo.security.SubjectAlternativeName; -import com.yahoo.security.X509CertificateUtils; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.NodeRepository; - -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import static com.yahoo.security.SubjectAlternativeName.Type.DNS_NAME; - -/** - * Resolve node from various types of x509 identity certificates. - * - * @author bjorncs - */ -class NodeIdentifier { - - private static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz."; - - private final Zone zone; - private final NodeRepository nodeRepository; - private final String athenzProviderHostname; - private final Set<String> configServerLikeIdentities; - private final Set<String> tenantAndProxyHostIndentities; - private final String tenantIdentity; - - private final Supplier<List<Node>> nodeCache; - - NodeIdentifier(Zone zone, NodeRepository nodeRepository, ConfigServerSecurityConfig securityConfig) { - this.zone = zone; - this.nodeRepository = nodeRepository; - this.athenzProviderHostname = securityConfig.athenzProviderHostname(); - this.configServerLikeIdentities = Set.of(securityConfig.controllerHostIdentity(), securityConfig.configServerHostIdentity()); - this.tenantAndProxyHostIndentities = Set.of(securityConfig.tenantHostIdentity(), securityConfig.proxyHostIdentity()); - this.tenantIdentity = securityConfig.tenantIdentity(); - nodeCache = Suppliers.memoizeWithExpiration(nodeRepository::getNodes, 1, TimeUnit.MINUTES); - } - - NodePrincipal resolveNode(List<X509Certificate> certificateChain) throws NodeIdentifierException { - X509Certificate clientCertificate = certificateChain.get(0); - String subjectCommonName = X509CertificateUtils.getSubjectCommonNames(clientCertificate).stream() - .findFirst() - .orElseThrow(() -> new NodeIdentifierException("Certificate subject common name is missing!")); - if (isAthenzIssued(clientCertificate)) { - List<SubjectAlternativeName> sans = X509CertificateUtils.getSubjectAlternativeNames(clientCertificate); - if (configServerLikeIdentities.contains(subjectCommonName)) { - return NodePrincipal.withAthenzIdentity(subjectCommonName, certificateChain); - } else if (tenantAndProxyHostIndentities.contains(subjectCommonName)) { - return NodePrincipal.withAthenzIdentity(subjectCommonName, getHostFromCalypsoCertificate(sans), certificateChain); - } else if (subjectCommonName.equals(tenantIdentity)) { - return NodePrincipal.withAthenzIdentity(subjectCommonName, getHostFromVespaCertificate(sans), certificateChain); - } - - throw new NodeIdentifierException(String.format( - "Subject common name (%s) does not match any expected identity", subjectCommonName)); - } else if (subjectCommonName.contains(athenzProviderHostname)) { - // ZTS treated as a node principal even though its not a Vespa node - return NodePrincipal.withLegacyIdentity(subjectCommonName, certificateChain); - } else { - throw new NodeIdentifierException(String.format("Unknown certificate (subject=%s, issuer=%s)", - subjectCommonName, - X509CertificateUtils.getIssuerCommonNames(clientCertificate))); - } - } - - private boolean isAthenzIssued(X509Certificate certificate) { - String issuerCommonName = X509CertificateUtils.getIssuerCommonNames(certificate).stream() - .findFirst() - .orElseThrow(() -> new NodeIdentifierException("Certificate issuer common name is missing!")); - return issuerCommonName.equals("Yahoo Athenz CA") || issuerCommonName.equals("Athenz AWS CA"); - } - - private String getHostFromCalypsoCertificate(List<SubjectAlternativeName> sans) { - String openStackId = getUniqueInstanceId(sans); - return nodeCache.get().stream() - .filter(node -> node.id().equals(openStackId)) - .map(Node::hostname) - .findFirst() - .orElseThrow(() -> new NodeIdentifierException( - String.format( - "Cannot find node with openstack-id '%s' in node repository (SANs=%s)", - openStackId, - sans.stream().map(SubjectAlternativeName::getValue).collect(Collectors.joining(",", "[", "]"))))); - } - - private String getHostFromVespaCertificate(List<SubjectAlternativeName> sans) { - // TODO Remove this branch once all BM nodes are gone - if (sans.stream().anyMatch(san -> san.getValue().endsWith("ostk.yahoo.cloud"))) { - return getHostFromCalypsoCertificate(sans); - } - VespaUniqueInstanceId instanceId = VespaUniqueInstanceId.fromDottedString(getUniqueInstanceId(sans)); - if (!zone.environment().value().equals(instanceId.environment())) - throw new NodeIdentifierException("Invalid environment: " + instanceId.environment()); - if (!zone.region().value().equals(instanceId.region())) - throw new NodeIdentifierException("Invalid region(): " + instanceId.region()); - List<Node> applicationNodes = - nodeRepository.getNodes(ApplicationId.from(instanceId.tenant(), instanceId.application(), instanceId.instance())); - return applicationNodes.stream() - .filter( - node -> node.allocation() - .map(allocation -> allocation.membership().index() == instanceId.clusterIndex() - && allocation.membership().cluster().id().value().equals(instanceId.clusterId())) - .orElse(false)) - .map(Node::hostname) - .findFirst() - .orElseThrow(() -> new NodeIdentifierException("Could not find any node with instance id: " + instanceId.asDottedString())); - } - - private static String getUniqueInstanceId(List<SubjectAlternativeName> sans) { - return sans.stream() - .filter(san -> san.getType() == DNS_NAME) - .map(SubjectAlternativeName::getValue) - .filter(dnsName -> (dnsName.endsWith("yahoo.cloud") || dnsName.endsWith("oath.cloud")) && dnsName.contains(INSTANCE_ID_DELIMITER)) - .map(dnsName -> dnsName.substring(0, dnsName.indexOf(INSTANCE_ID_DELIMITER))) - .findFirst() - .orElseThrow(() -> new NodeIdentifierException("Could not find unique instance id from SAN addresses: " + sans)); - } - - static class NodeIdentifierException extends RuntimeException { - NodeIdentifierException(String message) { - super(message); - } - } - -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierFilter.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierFilter.java deleted file mode 100644 index 1c66c17a0bb..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.google.inject.Inject; -import com.yahoo.config.provision.Zone; -import com.yahoo.config.provisioning.ConfigServerSecurityConfig; -import com.yahoo.jdisc.Response; -import com.yahoo.jdisc.http.filter.DiscFilterRequest; -import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase; -import com.yahoo.log.LogLevel; -import com.yahoo.vespa.hosted.provision.NodeRepository; -import com.yahoo.yolean.chain.Provides; - -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Optional; -import java.util.logging.Logger; - -/** - * A filter that identifies the remote node based on the subject and subject alternative names in client certificate. - * A {@link NodePrincipal} object is assigned to user principal field if identification is successful. - * - * @author bjorncs - */ -@Provides("NodeIdentifierFilter") -public class NodeIdentifierFilter extends JsonSecurityRequestFilterBase { - - private static final Logger log = Logger.getLogger(NodeIdentifierFilter.class.getName()); - - private final NodeIdentifier nodeIdentifier; - - @Inject - public NodeIdentifierFilter(Zone zone, NodeRepository nodeRepository, ConfigServerSecurityConfig securityConfig) { - this.nodeIdentifier = new NodeIdentifier(zone, nodeRepository, securityConfig); - } - - @Override - protected Optional<ErrorResponse> filter(DiscFilterRequest request) { - List<X509Certificate> clientCertificateChain = request.getClientCertificateChain(); - if (clientCertificateChain.isEmpty()) - return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, 0, "Missing client certificate")); - try { - NodePrincipal identity = nodeIdentifier.resolveNode(clientCertificateChain); - request.setUserPrincipal(identity); - return Optional.empty(); - } catch (NodeIdentifier.NodeIdentifierException e) { - log.log(LogLevel.WARNING, "Node identification failed: " + e.getMessage(), e); - return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, 1, e.getMessage())); - } - } -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java deleted file mode 100644 index 56a5aa72910..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodePrincipal.java +++ /dev/null @@ -1,105 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.vespa.athenz.api.AthenzIdentity; -import com.yahoo.vespa.athenz.utils.AthenzIdentities; - -import java.security.Principal; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Represents the identity of a hosted Vespa node - * - * @author bjorncs - */ -public class NodePrincipal implements Principal { - private final String identityName; - private final String hostname; - private final List<X509Certificate> clientCertificateChain; - private final Type type; - - public static NodePrincipal withAthenzIdentity(String identityName, - List<X509Certificate> clientCertificateChain) { - return withAthenzIdentity(identityName, null, clientCertificateChain); - } - - public static NodePrincipal withAthenzIdentity(String identityName, - String hostname, - List<X509Certificate> clientCertificateChain) { - return new NodePrincipal(identityName, hostname, clientCertificateChain, Type.ATHENZ); - } - - public static NodePrincipal withLegacyIdentity(String hostname, - List<X509Certificate> clientCertificateChain) { - return new NodePrincipal(hostname, hostname, clientCertificateChain, Type.LEGACY); - } - - private NodePrincipal(String identityName, - String hostname, - List<X509Certificate> clientCertificateChain, - Type type) { - this.identityName = identityName; - this.hostname = hostname; - this.clientCertificateChain = clientCertificateChain; - this.type = type; - } - - public String getHostIdentityName() { - return identityName; - } - - public Optional<AthenzIdentity> getAthenzIdentityName() { - if (type == Type.LEGACY) return Optional.empty(); - return Optional.of(AthenzIdentities.from(identityName)); - } - - public Optional<String> getHostname() { - return Optional.ofNullable(hostname); - } - - public List<X509Certificate> getClientCertificateChain() { - return clientCertificateChain; - } - - public Type getType() { - return type; - } - - @Override - public String getName() { - if (hostname == null || identityName.equals(hostname)) { - return identityName; - } else { - return identityName + "/" + hostname; - } - } - - public enum Type { ATHENZ, LEGACY } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - NodePrincipal principal = (NodePrincipal) o; - return Objects.equals(identityName, principal.identityName) && - Objects.equals(hostname, principal.hostname) && - type == principal.type; - } - - @Override - public int hashCode() { - return Objects.hash(identityName, hostname, type); - } - - @Override - public String toString() { - return "NodePrincipal{" + - "identityName='" + identityName + '\'' + - ", hostname='" + hostname + '\'' + - ", type=" + type + - '}'; - } -} diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java deleted file mode 100644 index e2a40f398a9..00000000000 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -/** - * @author mpolden - */ -@ExportPackage -package com.yahoo.vespa.hosted.provision.restapi.v2.filter; - -import com.yahoo.osgi.annotation.ExportPackage;
\ No newline at end of file diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java deleted file mode 100644 index 3ea1570f770..00000000000 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizationFilterTest.java +++ /dev/null @@ -1,38 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.application.container.handler.Request.Method; -import com.yahoo.vespa.curator.mock.MockCurator; -import com.yahoo.vespa.hosted.provision.restapi.v2.filter.FilterTester.Request; -import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors; -import com.yahoo.vespa.hosted.provision.testutils.MockNodeRepository; -import org.junit.Test; - -/** - * @author mpolden - */ -public class AuthorizationFilterTest { - - private final FilterTester tester = new FilterTester(new AuthorizationFilter( - new MockNodeRepository(new MockCurator(), new MockNodeFlavors()), - NodeIdentifierTest.SECURITY_CONFIG)); - - @Test - public void filter() { - // These are just rudimentary tests of the filter. See AuthorizerTest for more exhaustive tests - tester.assertRequest(new Request(Method.GET, "/"), 500, - "{\"error-code\":\"INTERNAL_SERVER_ERROR\",\"message\":\"GET / denied for " + - "remote-addr: Principal is missing. NodeIdentifierFilter has not been applied.\"}"); - - tester.assertRequest(new Request(Method.GET, "/").commonName("foo"), 403, - "{\"error-code\":\"FORBIDDEN\",\"message\":\"GET / " + - "denied for remote-addr: Invalid credentials: NodePrincipal{identityName='foo', hostname='foo', type=LEGACY}\"}"); - - tester.assertRequest(new Request(Method.GET, "/nodes/v2/node/foo").commonName("bar"), - 403, "{\"error-code\":\"FORBIDDEN\",\"message\":\"GET /nodes/v2/node/foo " + - "denied for remote-addr: Invalid credentials: NodePrincipal{identityName='bar', hostname='bar', type=LEGACY}\"}"); - - tester.assertSuccess(new Request(Method.GET, "/nodes/v2/node/foo").commonName("foo")); - } - -} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizerTest.java deleted file mode 100644 index 939de2dff25..00000000000 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/AuthorizerTest.java +++ /dev/null @@ -1,196 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.config.provision.Flavor; -import com.yahoo.config.provision.NodeFlavors; -import com.yahoo.config.provision.NodeType; -import com.yahoo.vespa.curator.mock.MockCurator; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.testutils.MockNodeFlavors; -import com.yahoo.vespa.hosted.provision.testutils.MockNodeRepository; -import org.junit.Before; -import org.junit.Test; - -import java.net.URI; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import static com.yahoo.vespa.hosted.provision.restapi.v2.filter.NodeIdentifierTest.ATHENZ_PROVIDER_HOSTNAME; -import static com.yahoo.vespa.hosted.provision.restapi.v2.filter.NodeIdentifierTest.CONFIG_SERVER_IDENTITY; -import static com.yahoo.vespa.hosted.provision.restapi.v2.filter.NodeIdentifierTest.CONTROLLER_IDENTITY; -import static com.yahoo.vespa.hosted.provision.restapi.v2.filter.NodeIdentifierTest.SECURITY_CONFIG; -import static com.yahoo.vespa.hosted.provision.restapi.v2.filter.NodeIdentifierTest.TENANT_HOST_IDENTITY; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author mpolden - */ -public class AuthorizerTest { - - private Authorizer authorizer; - - @Before - public void before() { - NodeFlavors flavors = new MockNodeFlavors(); - MockNodeRepository nodeRepository = new MockNodeRepository(new MockCurator(), flavors); - authorizer = new Authorizer(nodeRepository, SECURITY_CONFIG); - - Set<String> ipAddresses = Set.of("127.0.0.1", "::1"); - Flavor flavor = flavors.getFlavorOrThrow("default"); - List<Node> nodes = List.of( - nodeRepository.createNode( - "host1", "host1", ipAddresses, Optional.empty(), flavor, NodeType.host), - nodeRepository.createNode( - "child1-1", "child1-1", ipAddresses, Optional.of("host1"), flavor, NodeType.tenant), - nodeRepository.createNode( - "child1-2", "child1-2", ipAddresses, Optional.of("host1"), flavor, NodeType.tenant), - nodeRepository.createNode( - "host2", "host2", ipAddresses, Optional.empty(), flavor, NodeType.host), - nodeRepository.createNode( - "child2-1", "child2-1", ipAddresses, Optional.of("host1.tld"), flavor, NodeType.tenant), - nodeRepository.createNode( - "proxy1", "proxy1", ipAddresses, Optional.of("proxyhost1"), flavor, NodeType.proxy), - nodeRepository.createNode( - "proxyhost1", "proxyhost1", ipAddresses, Optional.empty(), flavor, NodeType.proxyhost) - ); - nodeRepository.addNodes(nodes); - } - - @Test - public void root_authorization() { - assertFalse(authorizedTenantNode("", "")); - assertFalse(authorizedTenantNode("", "/")); - assertFalse(authorizedTenantNode("node1", "")); - assertFalse(authorizedTenantNode("node1", "/")); - } - - @Test - public void nodes_authorization() { - // Node can only access its own resources - assertFalse(authorizedTenantNode("node1", "/nodes/v2/node")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/node/")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/node/node2")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/state/dirty/")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/state/dirty/node2")); - // Path traversal fails gracefully - assertFalse(authorizedTenantNode("node1", "/nodes/v2/node/.")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/node/..")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/acl/node2")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/node/?parentHost=node2")); - // Node resource always takes precedence over filter - assertFalse(authorizedTenantNode("node1", "/nodes/v2/acl/node2?hostname=node1")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/command/reboot/")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/command/reboot/?hostname=")); - assertFalse(authorizedTenantNode("node1", "/nodes/v2/command/reboot/?hostname=node2")); - assertTrue(authorizedTenantNode("node1", "/nodes/v2/node/node1")); - assertTrue(authorizedTenantNode("node1", "/nodes/v2/state/dirty/node1")); - assertTrue(authorizedTenantNode("node1", "/nodes/v2/acl/node1")); - assertTrue(authorizedTenantNode("node1", "/nodes/v2/command/reboot?hostname=node1")); - assertTrue(authorizedTenantNode("node1", "/nodes/v2/node/?parentHost=node1")); - - // Host node can access itself and its children - assertFalse(authorizedTenantHostNode("host1", "/nodes/v2/node/child2-1")); - assertFalse(authorizedTenantHostNode("host1", "/nodes/v2/command/reboot?hostname=child2-1")); - assertTrue(authorizedTenantHostNode("host1", "/nodes/v2/node/host1")); - assertTrue(authorizedTenantHostNode("host1", "/nodes/v2/node/child1-1")); - assertTrue(authorizedTenantHostNode("host1", "/nodes/v2/command/reboot?hostname=child1-1")); - assertTrue(authorizedTenantHostNode("host1", "/athenz/v1/provider/identity-document/tenant/host1")); - assertTrue(authorizedTenantHostNode("host1", "/athenz/v1/provider/identity-document/node/child1-1")); - - // Trusted services can access everything in their own system - assertTrue(authorizedController(CONTROLLER_IDENTITY, "/")); - assertTrue(authorizedController(CONFIG_SERVER_IDENTITY, "/")); - assertTrue(authorizedController(CONTROLLER_IDENTITY, "/nodes/v2/node/")); - assertTrue(authorizedController(CONTROLLER_IDENTITY, "/nodes/v2/node/node1")); - assertTrue(authorizedController(CONFIG_SERVER_IDENTITY, "/nodes/v2/node/node1")); - } - - @Test - public void orchestrator_authorization() { - // Node can only access its own resources - assertFalse(authorizedTenantNode("node1", "/orchestrator/v1/hosts")); - assertFalse(authorizedTenantNode("node1", "/orchestrator/v1/hosts/")); - assertFalse(authorizedTenantNode("node1", "/orchestrator/v1/hosts/node2")); - assertFalse(authorizedTenantNode("node1", "/orchestrator/v1/hosts/node2/suspended")); - - // Node can suspend itself - assertTrue(authorizedTenantNode("node1", "/orchestrator/v1/hosts/node1")); - assertTrue(authorizedTenantNode("node1", "/orchestrator/v1/hosts/node1/suspended")); - - // Host node can suspend itself and its children - assertFalse(authorizedTenantHostNode("host1", "/orchestrator/v1/hosts/child2-1/suspended")); - assertFalse(authorizedTenantHostNode("host1", "/orchestrator/v1/suspensions/hosts/host1?hostname=child2-1")); - // All given hostnames must be children - assertFalse(authorizedTenantHostNode("host1", "/orchestrator/v1/suspensions/hosts/host1?hostname=child1-1&hostname=child2-1")); - assertTrue(authorizedTenantHostNode("host1", "/orchestrator/v1/hosts/host1/suspended")); - assertTrue(authorizedTenantHostNode("host1", "/orchestrator/v1/hosts/child1-1/suspended")); - assertTrue(authorizedTenantHostNode("host1", "/orchestrator/v1/suspensions/hosts/host1?hostname=child1-1")); - // Multiple children - assertTrue(authorizedTenantHostNode("host1", "/orchestrator/v1/suspensions/hosts/host1?hostname=child1-1&hostname=child1-2")); - } - - @Test - public void flags_authorization() { - // Tenant nodes cannot access flags resources - assertFalse(authorizedTenantNode("node1", "/flags/v1/data")); - assertFalse(authorizedTenantNode("node1", "/flags/v1/data/flagid")); - assertFalse(authorizedTenantNode("node1", "/flags/v1/foo")); - - // Host node can access data - assertTrue(authorizedTenantHostNode("host1", "/flags/v1/data")); - assertFalse(authorizedTenantHostNode("host1", "/flags/v1/data/flagid")); - assertFalse(authorizedTenantHostNode("host1", "/flags/v1/foo")); - assertTrue(authorizedTenantHostNode("proxy1-host", "/flags/v1/data")); - assertFalse(authorizedTenantHostNode("proxy1-host", "/flags/v1/data/flagid")); - assertFalse(authorizedTenantHostNode("proxy1-host", "/flags/v1/foo")); - assertTrue(authorizedController(CONFIG_SERVER_IDENTITY, "/flags/v1/data")); - assertFalse(authorizedController(CONFIG_SERVER_IDENTITY, "/flags/v1/data/flagid")); - assertFalse(authorizedController(CONFIG_SERVER_IDENTITY, "/flags/v1/foo")); - - // Controller can access everything - assertTrue(authorizedController(CONTROLLER_IDENTITY, "/flags/v1/data")); - assertTrue(authorizedController(CONTROLLER_IDENTITY, "/flags/v1/data/flagid")); - assertTrue(authorizedController(CONTROLLER_IDENTITY, "/flags/v1/foo")); - } - - @Test - public void routing_authorization() { - // Node of proxy or proxyhost type can access routing resource - assertFalse(authorizedTenantNode("node1", "/routing/v1/status")); - assertTrue(authorizedTenantNode("proxy1", "/routing/v1/status")); - assertTrue(authorizedTenantNode("proxyhost1", "/routing/v1/status")); - } - - @Test - public void zts_allowed_for_athenz_provider_api() { - assertTrue(authorizedLegacyNode(ATHENZ_PROVIDER_HOSTNAME, "/athenz/v1/provider/refresh")); - assertTrue(authorizedLegacyNode(ATHENZ_PROVIDER_HOSTNAME, "/athenz/v1/provider/instance")); - } - - private boolean authorizedTenantNode(String hostname, String path) { - return authorized(NodePrincipal.withAthenzIdentity("vespa.vespa.tenant", hostname, List.of()), path); - } - - private boolean authorizedTenantHostNode(String hostname, String path) { - return authorized(NodePrincipal.withAthenzIdentity(TENANT_HOST_IDENTITY, hostname, List.of()), path); - } - - private boolean authorizedLegacyNode(String hostname, String path) { - return authorized(NodePrincipal.withLegacyIdentity(hostname, List.of()), path); - } - - private boolean authorizedController(String controllerIdentity, String path) { - return authorized(NodePrincipal.withAthenzIdentity(controllerIdentity, List.of()), path); - } - - private boolean authorized(NodePrincipal principal, String path) { - return authorizer.test(principal, uri(path)); - } - - private static URI uri(String path) { - return URI.create("http://localhost").resolve(path); - } - -} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java deleted file mode 100644 index c6546f05955..00000000000 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/FilterTester.java +++ /dev/null @@ -1,155 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.application.container.handler.Request.Method; -import com.yahoo.container.jdisc.RequestHandlerTestDriver; -import com.yahoo.jdisc.http.filter.DiscFilterRequest; -import com.yahoo.jdisc.http.filter.SecurityRequestFilter; -import com.yahoo.security.KeyAlgorithm; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.X509CertificateBuilder; - -import javax.security.auth.x500.X500Principal; -import java.math.BigInteger; -import java.net.URI; -import java.security.KeyPair; -import java.security.cert.X509Certificate; -import java.time.Duration; -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @author mpolden - */ -public class FilterTester { - - private final SecurityRequestFilter filter; - - public FilterTester(SecurityRequestFilter filter) { - this.filter = filter; - } - - public void assertSuccess(Request request) { - assertFalse("No response written by filter", getResponse(request).isPresent()); - } - - public void assertRequest(Request request, int status, String body) { - Optional<Response> response = getResponse(request); - assertTrue("Expected response from filter", response.isPresent()); - assertEquals("Response body", body, response.get().body); - assertEquals("Content type", "application/json", - response.get().headers.get("Content-Type").get(0)); - assertEquals("Status code", status, response.get().status); - } - - private Optional<Response> getResponse(Request request) { - RequestHandlerTestDriver.MockResponseHandler handler = new RequestHandlerTestDriver.MockResponseHandler(); - filter.filter(toDiscFilterRequest(request), handler); - return Optional.ofNullable(handler.getResponse()) - .map(response -> new Response(response.getStatus(), response.headers(), handler.readAll())); - } - - private static DiscFilterRequest toDiscFilterRequest(Request request) { - DiscFilterRequest r = mock(DiscFilterRequest.class); - when(r.getMethod()).thenReturn(request.method().name()); - when(r.getUri()).thenReturn(URI.create("http://localhost").resolve(request.path())); - when(r.getRemoteAddr()).thenReturn(request.remoteAddr()); - when(r.getLocalAddr()).thenReturn(request.localAddr()); - if (request.commonName().isPresent()) { - X509Certificate cert = certificateFor(request.commonName().get(), KeyUtils.generateKeypair(KeyAlgorithm.EC)); - List<X509Certificate> certs = Collections.singletonList(cert); - when(r.getClientCertificateChain()).thenReturn(certs); - when(r.getUserPrincipal()).thenReturn(NodePrincipal.withLegacyIdentity(request.commonName().get(), certs)); - } - return r; - } - - - /** Create a self signed certificate for commonName using given public/private key pair */ - private static X509Certificate certificateFor(String commonName, KeyPair keyPair) { - Instant now = Instant.now(); - X500Principal subject = new X500Principal("CN=" + commonName); - return X509CertificateBuilder - .fromKeypair(keyPair, subject, now, now.plus(Duration.ofDays(30)), SHA256_WITH_ECDSA, BigInteger.valueOf(now.toEpochMilli())) - .setBasicConstraints(true, true) - .build(); - } - - private static class Response { - - private final int status; - private final Map<String, List<String>> headers; - private final String body; - - private Response(int status, Map<String, List<String>> headers, String body) { - this.status = status; - this.headers = headers; - this.body = body; - } - - } - - public static class Request { - - private final Method method; - private final String path; - private String localAddr; - private String remoteAddr; - private String commonName; - - public Request(Method method, String path) { - this.method = method; - this.path = path; - this.commonName = null; - this.localAddr = "local-addr"; - this.remoteAddr = "remote-addr"; - } - - public Method method() { - return method; - } - - public String path() { - return path; - } - - public String localAddr() { - return localAddr; - } - - public String remoteAddr() { - return remoteAddr; - } - - public Optional<String> commonName() { - return Optional.ofNullable(commonName); - } - - public Request commonName(String commonName) { - this.commonName = commonName; - return this; - } - - public Request localAddr(String localAddr) { - this.localAddr = localAddr; - return this; - } - - public Request remoteAddr(String remoteAddr) { - this.remoteAddr = remoteAddr; - return this; - } - - } - -} diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java deleted file mode 100644 index 914a2e62164..00000000000 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/NodeIdentifierTest.java +++ /dev/null @@ -1,272 +0,0 @@ -// 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.provision.restapi.v2.filter; - -import com.yahoo.component.Version; -import com.yahoo.config.provision.ApplicationId; -import com.yahoo.config.provision.ClusterMembership; -import com.yahoo.config.provision.ClusterSpec; -import com.yahoo.config.provision.Environment; -import com.yahoo.config.provision.NodeResources; -import com.yahoo.config.provision.NodeType; -import com.yahoo.config.provision.RegionName; -import com.yahoo.config.provision.SystemName; -import com.yahoo.config.provision.Zone; -import com.yahoo.config.provisioning.ConfigServerSecurityConfig; -import com.yahoo.security.KeyUtils; -import com.yahoo.security.Pkcs10Csr; -import com.yahoo.security.Pkcs10CsrBuilder; -import com.yahoo.security.X509CertificateBuilder; -import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId; -import com.yahoo.vespa.hosted.provision.Node; -import com.yahoo.vespa.hosted.provision.NodeRepositoryTester; -import com.yahoo.vespa.hosted.provision.node.Allocation; -import com.yahoo.vespa.hosted.provision.node.Generation; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import javax.security.auth.x500.X500Principal; -import java.math.BigInteger; -import java.security.KeyPair; -import java.security.cert.X509Certificate; -import java.time.Instant; -import java.util.Collections; -import java.util.Optional; - -import static com.yahoo.security.KeyAlgorithm.EC; -import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; -import static com.yahoo.vespa.athenz.identityprovider.api.IdentityType.NODE; -import static java.util.Collections.emptySet; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -/** - * @author bjorncs - */ -public class NodeIdentifierTest { - - @Rule - public final ExpectedException expectedException = ExpectedException.none(); - - static final String ATHENZ_PROVIDER_HOSTNAME = "zts.domain.tld"; - static final String CONTROLLER_IDENTITY = "vespa.controller"; - static final String CONFIG_SERVER_IDENTITY = "vespa.configserver"; - static final String PROXY_HOST_IDENTITY = "vespa.proxy"; - static final String TENANT_HOST_IDENTITY = "vespa.tenant-host"; - static final String TENANT_IDENTITY = "vespa.tenant"; - static final ConfigServerSecurityConfig SECURITY_CONFIG = new ConfigServerSecurityConfig.Builder() - .athenzProviderHostname(ATHENZ_PROVIDER_HOSTNAME) - .controllerHostIdentity(CONTROLLER_IDENTITY) - .configServerHostIdentity(CONFIG_SERVER_IDENTITY) - .proxyHostIdentity(PROXY_HOST_IDENTITY) - .tenantHostIdentity(TENANT_HOST_IDENTITY) - .tenantIdentity(TENANT_IDENTITY) - .build(); - - private static final String HOSTNAME = "myhostname"; - private static final String PROXY_HOSTNAME = "myproxyhostname"; - - private static final String OPENSTACK_ID = "OPENSTACK-ID"; - private static final String AWS_INSTANCE_ID = "i-abcdef123456"; - - private static final String INSTANCE_ID = "default"; - private static final Zone ZONE = new Zone(SystemName.main, Environment.prod, RegionName.defaultName()); - private static final KeyPair KEYPAIR = KeyUtils.generateKeypair(EC); - private static final X509Certificate ATHENZ_YAHOO_CA_CERT = createDummyCaCertificate("Yahoo Athenz CA"); - private static final X509Certificate ATHENZ_AWS_CA_CERT = createDummyCaCertificate("Athenz AWS CA"); - - @Test - public void rejects_unknown_cert() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - X509Certificate certificate = X509CertificateBuilder - .fromKeypair( - KEYPAIR, new X500Principal("CN=" + HOSTNAME), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), SHA256_WITH_ECDSA, BigInteger.ONE) - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - expectedException.expect(NodeIdentifier.NodeIdentifierException.class); - expectedException.expectMessage("(subject=myhostname, issuer=[myhostname])"); - identifier.resolveNode(singletonList(certificate)); - } - - @Test - public void accepts_openstack_host_certificate() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - nodeRepositoryDummy.addNode(OPENSTACK_ID, HOSTNAME, INSTANCE_ID, NodeType.host); - nodeRepositoryDummy.setNodeState(HOSTNAME, Node.State.active); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + TENANT_HOST_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_YAHOO_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .addSubjectAlternativeName(OPENSTACK_ID + ".instanceid.athenz.provider-name.ostk.yahoo.cloud") - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertTrue(identity.getHostname().isPresent()); - assertEquals(HOSTNAME, identity.getHostname().get()); - assertEquals(TENANT_HOST_IDENTITY, identity.getHostIdentityName()); - } - - @Test - public void accepts_aws_host_certificate() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - nodeRepositoryDummy.addNode(AWS_INSTANCE_ID, HOSTNAME, INSTANCE_ID, NodeType.host); - nodeRepositoryDummy.setNodeState(HOSTNAME, Node.State.active); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + TENANT_HOST_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_AWS_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .addSubjectAlternativeName(AWS_INSTANCE_ID + ".instanceid.athenz.aws.oath.cloud") - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertTrue(identity.getHostname().isPresent()); - assertEquals(HOSTNAME, identity.getHostname().get()); - assertEquals(TENANT_HOST_IDENTITY, identity.getHostIdentityName()); - } - - @Test - public void accepts_aws_proxy_host_certificate() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - nodeRepositoryDummy.addNode(AWS_INSTANCE_ID, PROXY_HOSTNAME, INSTANCE_ID, NodeType.proxyhost); - nodeRepositoryDummy.setNodeState(PROXY_HOSTNAME, Node.State.active); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + PROXY_HOST_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_AWS_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .addSubjectAlternativeName(AWS_INSTANCE_ID + ".instanceid.athenz.aws.oath.cloud") - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertTrue(identity.getHostname().isPresent()); - assertEquals(PROXY_HOSTNAME, identity.getHostname().get()); - assertEquals(PROXY_HOST_IDENTITY, identity.getHostIdentityName()); - } - - @Test - public void accepts_aws_configserver_host_certificate() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + CONFIG_SERVER_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_AWS_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .addSubjectAlternativeName(AWS_INSTANCE_ID + ".instanceid.athenz.aws.oath.cloud") - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertEquals(CONFIG_SERVER_IDENTITY, identity.getHostIdentityName()); - } - - @Test - public void accepts_zts_certificate() { - X509Certificate certificate = X509CertificateBuilder - .fromKeypair(KEYPAIR, new X500Principal("CN=" + ATHENZ_PROVIDER_HOSTNAME), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), SHA256_WITH_ECDSA, BigInteger.ONE) - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, new NodeRepositoryTester().nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertEquals(ATHENZ_PROVIDER_HOSTNAME, identity.getHostIdentityName()); - assertEquals(NodePrincipal.Type.LEGACY, identity.getType()); - } - - @Test - public void accepts_docker_container_certificate() { - String clusterId = "clusterid"; - int clusterIndex = 0; - String tenant = "tenant"; - String application = "application"; - String region = ZONE.region().value(); - String environment = ZONE.environment().value(); - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - Node node = createNode(clusterId, clusterIndex, tenant, application); - nodeRepositoryDummy.nodeRepository().addDockerNodes(singletonList(node), nodeRepositoryDummy.nodeRepository().lockAllocation()); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + TENANT_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - VespaUniqueInstanceId vespaUniqueInstanceId = new VespaUniqueInstanceId(clusterIndex, clusterId, INSTANCE_ID, application, tenant, region, environment, NODE); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_YAHOO_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .addSubjectAlternativeName(vespaUniqueInstanceId.asDottedString() + ".instanceid.athenz.provider-name.vespa.yahoo.cloud") - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertTrue(identity.getHostname().isPresent()); - assertEquals(HOSTNAME, identity.getHostname().get()); - assertEquals(TENANT_IDENTITY, identity.getHostIdentityName()); - } - - @Test - public void accepts_controller_certificate() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + CONTROLLER_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_YAHOO_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertFalse(identity.getHostname().isPresent()); - assertEquals(CONTROLLER_IDENTITY, identity.getHostIdentityName()); - } - - @Test - public void accepts_openstack_bm_tenant_certificate() { - NodeRepositoryTester nodeRepositoryDummy = new NodeRepositoryTester(); - nodeRepositoryDummy.addNode(OPENSTACK_ID, HOSTNAME, INSTANCE_ID, NodeType.tenant); - nodeRepositoryDummy.setNodeState(HOSTNAME, Node.State.active); - Pkcs10Csr csr = Pkcs10CsrBuilder - .fromKeypair(new X500Principal("CN=" + TENANT_IDENTITY), KEYPAIR, SHA256_WITH_ECDSA) - .build(); - X509Certificate certificate = X509CertificateBuilder - .fromCsr(csr, ATHENZ_YAHOO_CA_CERT.getSubjectX500Principal(), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), KEYPAIR.getPrivate(), SHA256_WITH_ECDSA, BigInteger.ONE) - .addSubjectAlternativeName(OPENSTACK_ID + ".instanceid.athenz.ostk.yahoo.cloud") - .build(); - NodeIdentifier identifier = new NodeIdentifier(ZONE, nodeRepositoryDummy.nodeRepository(), SECURITY_CONFIG); - NodePrincipal identity = identifier.resolveNode(singletonList(certificate)); - assertTrue(identity.getHostname().isPresent()); - assertEquals(HOSTNAME, identity.getHostname().get()); - assertEquals(TENANT_IDENTITY, identity.getHostIdentityName()); - } - - private static Node createNode(String clusterId, int clusterIndex, String tenant, String application) { - return Node - .createDockerNode( - singleton("1.2.3.4"), - emptySet(), - HOSTNAME, - Optional.of("parenthost"), - new NodeResources(1, 2, 50), - NodeType.tenant) - .with( - new Allocation( - ApplicationId.from(tenant, application, INSTANCE_ID), - ClusterMembership.from( - ClusterSpec.from( - ClusterSpec.Type.container, - new ClusterSpec.Id(clusterId), - ClusterSpec.Group.from(0), - Version.emptyVersion, - false, Collections.emptySet()), - clusterIndex), - Generation.initial(), - false)); - - } - - private static X509Certificate createDummyCaCertificate(String caCommonName) { - KeyPair keyPair = KeyUtils.generateKeypair(EC); - return X509CertificateBuilder - .fromKeypair( - keyPair, new X500Principal("CN=" + caCommonName), Instant.EPOCH, Instant.EPOCH.plusSeconds(60), SHA256_WITH_ECDSA, BigInteger.ONE) - .setBasicConstraints(true, true) - .build(); - - } - -} |