summaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/athenz/instanceproviderservice/identitydocument/IdentityDocumentGenerator.java
blob: ffeca67645b2c067f3869f0db61515e69548b878 (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
// 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.identitydocument;

import com.google.inject.Inject;
import com.yahoo.config.provision.Zone;
import com.yahoo.net.HostName;
import com.yahoo.vespa.athenz.api.AthenzService;
import com.yahoo.vespa.athenz.identityprovider.api.EntityBindingsMapper;
import com.yahoo.vespa.athenz.identityprovider.api.IdentityDocument;
import com.yahoo.vespa.athenz.identityprovider.api.SignedIdentityDocument;
import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.KeyProvider;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.config.AthenzProviderServiceConfig;
import com.yahoo.vespa.hosted.athenz.instanceproviderservice.impl.Utils;
import com.yahoo.vespa.hosted.provision.Node;
import com.yahoo.vespa.hosted.provision.NodeRepository;
import com.yahoo.vespa.hosted.provision.node.Allocation;

import java.net.InetAddress;
import java.net.URI;
import java.security.PrivateKey;
import java.security.Signature;
import java.time.Instant;
import java.util.Base64;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

/**
 * @author mortent
 * @author bjorncs
 */
public class IdentityDocumentGenerator {

    private final NodeRepository nodeRepository;
    private final Zone zone;
    private final KeyProvider keyProvider;
    private final AthenzProviderServiceConfig.Zones zoneConfig;

    @Inject
    public IdentityDocumentGenerator(AthenzProviderServiceConfig config,
                                     NodeRepository nodeRepository,
                                     Zone zone,
                                     KeyProvider keyProvider) {
        this.zoneConfig = Utils.getZoneConfig(config, zone);
        this.nodeRepository = nodeRepository;
        this.zone = zone;
        this.keyProvider = keyProvider;
    }

    public SignedIdentityDocument generateSignedIdentityDocument(String hostname) {
        Node node = nodeRepository.getNode(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname));
        try {
            IdentityDocument identityDocument = generateIdDocument(node);
            String identityDocumentString = Utils.getMapper().writeValueAsString(EntityBindingsMapper.toIdentityDocumentEntity(identityDocument));

            String encodedIdentityDocument =
                    Base64.getEncoder().encodeToString(identityDocumentString.getBytes());
            Signature sigGenerator = Signature.getInstance("SHA512withRSA");

            PrivateKey privateKey = keyProvider.getPrivateKey(zoneConfig.secretVersion());
            sigGenerator.initSign(privateKey);
            sigGenerator.update(encodedIdentityDocument.getBytes());
            String signature = Base64.getEncoder().encodeToString(sigGenerator.sign());

            return new SignedIdentityDocument(
                    identityDocument,
                    signature,
                    SignedIdentityDocument.DEFAULT_KEY_VERSION,
                    identityDocument.providerUniqueId(),
                    toZoneDnsSuffix(zone, zoneConfig.certDnsSuffix()),
                    new AthenzService(zoneConfig.domain(), zoneConfig.serviceName()),
                    URI.create(zoneConfig.ztsUrl()),
                    SignedIdentityDocument.DEFAULT_DOCUMENT_VERSION,
                    identityDocument.configServerHostname(),
                    identityDocument.instanceHostname(),
                    identityDocument.createdAt(),
                    identityDocument.ipAddresses());
        } catch (Exception e) {
            throw new RuntimeException("Exception generating identity document: " + e.getMessage(), e);
        }
    }

    private IdentityDocument generateIdDocument(Node node) {
        Allocation allocation = node.allocation().orElseThrow(() -> new RuntimeException("No allocation for node " + node.hostname()));
        VespaUniqueInstanceId providerUniqueId = new VespaUniqueInstanceId(
                allocation.membership().index(),
                allocation.membership().cluster().id().value(),
                allocation.owner().instance().value(),
                allocation.owner().application().value(),
                allocation.owner().tenant().value(),
                zone.region().value(),
                zone.environment().value());

        Set<String> ips = new HashSet<>(node.ipAddresses());
        return new IdentityDocument(
                providerUniqueId,
                HostName.getLocalhost(),
                node.hostname(),
                Instant.now(),
                ips);
    }

    private static String toZoneDnsSuffix(Zone zone, String dnsSuffix) {
        return zone.environment().value() + "-" + zone.region().value() + "." + dnsSuffix;
    }

    /*
     * Basic access control until we have mutual auth where athenz x509certs are distributed on all docker nodes by node admin
     * Checks:
     *  If remote hostname == requested hostname --> OK
     *  If remote hostname is parent of requested hostname in node repo --> OK
     *  Otherwise NOT OK
     */
    // TODO Move this check to AuthorizationFilter in node-repository
    boolean validateAccess(String hostname, String remoteAddr) {
        try {
            InetAddress addr = InetAddress.getByName(remoteAddr);
            String remoteHostname = addr.getHostName();
            if (Objects.equals(hostname, remoteHostname)) {
                return true;
            }
            Node node = nodeRepository.getNode(hostname).orElseThrow(() -> new RuntimeException("Unable to find node " + hostname));
            return node.parentHostname()
                    .map(parent -> Objects.equals(parent, remoteHostname))
                    .orElse(false);
         } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}