summaryrefslogtreecommitdiffstats
path: root/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/filter/Authorizer.java
blob: 44636727531f6ed88ec718593efdbb75f1ae7680 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// 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.vespa.athenz.api.AthenzIdentity;
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 AthenzIdentity controllerIdentity;
    private final Set<AthenzIdentity> trustedIdentities;
    private final Set<AthenzIdentity> hostAdminIdentities;

    Authorizer(NodeRepository nodeRepository, AthenzIdentity controllerIdentity, AthenzIdentity configServerIdentity,
               AthenzIdentity proxyIdentity, AthenzIdentity tenantIdentity) {
        this.nodeRepository = nodeRepository;
        this.controllerIdentity = controllerIdentity;
        this.trustedIdentities = Set.of(controllerIdentity, configServerIdentity);
        this.hostAdminIdentities = Set.of(controllerIdentity, configServerIdentity, proxyIdentity, tenantIdentity);
    }

    /** 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(controllerIdentity);
            }

            // 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 hostname.equals(NodeIdentifier.ZTS_AWS_IDENTITY) || hostname.equals(NodeIdentifier.ZTS_ON_PREM_IDENTITY);
            }

            // 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);
    }

}