Convert TlsAllowedDevicesMonitorImpl to OSGi DS
[netconf.git] / apps / callhome-provider / src / main / java / org / opendaylight / netconf / callhome / mount / tls / TlsAllowedDevicesMonitorImpl.java
1 /*
2  * Copyright (c) 2020 Pantheon Technologies, s.r.o. and others. All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.callhome.mount.tls;
9
10 import static java.nio.charset.StandardCharsets.US_ASCII;
11
12 import java.io.ByteArrayInputStream;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.security.PublicKey;
16 import java.security.cert.CertificateException;
17 import java.security.cert.CertificateFactory;
18 import java.util.Base64;
19 import java.util.Collection;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.ConcurrentMap;
25 import java.util.stream.Collectors;
26 import javax.annotation.PreDestroy;
27 import javax.inject.Inject;
28 import javax.inject.Singleton;
29 import org.eclipse.jdt.annotation.NonNull;
30 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
31 import org.opendaylight.mdsal.binding.api.DataBroker;
32 import org.opendaylight.mdsal.binding.api.DataObjectModification;
33 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
34 import org.opendaylight.mdsal.binding.api.DataTreeModification;
35 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
36 import org.opendaylight.netconf.callhome.protocol.tls.TlsAllowedDevicesMonitor;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.Keystore;
38 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.keystore.rev171017.trusted.certificates.TrustedCertificate;
39 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.NetconfCallhomeServer;
40 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.AllowedDevices;
41 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.Device;
42 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.netconf.callhome.server.rev201015.netconf.callhome.server.allowed.devices.device.transport.Tls;
43 import org.opendaylight.yangtools.concepts.Registration;
44 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
45 import org.osgi.service.component.annotations.Activate;
46 import org.osgi.service.component.annotations.Component;
47 import org.osgi.service.component.annotations.Deactivate;
48 import org.osgi.service.component.annotations.Reference;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 @Singleton
53 @Component(service = TlsAllowedDevicesMonitor.class, immediate = true, property = "type=default")
54 public final class TlsAllowedDevicesMonitorImpl implements TlsAllowedDevicesMonitor, AutoCloseable {
55     private static final Logger LOG = LoggerFactory.getLogger(TlsAllowedDevicesMonitorImpl.class);
56
57     private final ConcurrentMap<String, String> deviceToPrivateKey = new ConcurrentHashMap<>();
58     private final ConcurrentMap<String, String> deviceToCertificate = new ConcurrentHashMap<>();
59     private final ConcurrentMap<String, PublicKey> certificateToPublicKey = new ConcurrentHashMap<>();
60     private final Registration allowedDevicesReg;
61     private final Registration certificatesReg;
62
63     @Inject
64     @Activate
65     public TlsAllowedDevicesMonitorImpl(@Reference final DataBroker dataBroker) {
66         allowedDevicesReg = dataBroker.registerDataTreeChangeListener(
67             DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION,
68                 InstanceIdentifier.create(NetconfCallhomeServer.class).child(AllowedDevices.class).child(Device.class)),
69             new AllowedDevicesMonitor());
70         certificatesReg = dataBroker.registerDataTreeChangeListener(
71             DataTreeIdentifier.create(LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(Keystore.class)),
72             new CertificatesMonitor());
73     }
74
75     @PreDestroy
76     @Deactivate
77     @Override
78     public void close() {
79         allowedDevicesReg.close();
80         certificatesReg.close();
81     }
82
83     @Override
84     public Optional<String> findDeviceIdByPublicKey(final PublicKey key) {
85         // Find certificate names by the public key
86         final Set<String> certificates = certificateToPublicKey.entrySet().stream()
87             .filter(v -> key.equals(v.getValue()))
88             .map(Map.Entry::getKey)
89             .collect(Collectors.toSet());
90
91         // Find devices names associated with a certificate name
92         final Set<String> deviceNames = deviceToCertificate.entrySet().stream()
93             .filter(v -> certificates.contains(v.getValue()))
94             .map(Map.Entry::getKey)
95             .collect(Collectors.toSet());
96
97         // In real world scenario it is not possible to have multiple certificates with the same private/public key,
98         // but in theor/synthetic tests it is practically possible to generate mulitple certificates from a single
99         // private key. In such case it's not possible to pin certificate to particular device.
100         if (deviceNames.size() > 1) {
101             LOG.error("Unable to find device by provided certificate. Possible reason: one certificate configured "
102                 + "with multiple devices/names or multiple certificates contain same public key");
103             return Optional.empty();
104         } else {
105             return deviceNames.stream().findFirst();
106         }
107     }
108
109     @Override
110     public Set<String> findAllowedKeys() {
111         return Set.copyOf(deviceToPrivateKey.values());
112     }
113
114     private final class CertificatesMonitor implements ClusteredDataTreeChangeListener<Keystore> {
115         @Override
116         public void onDataTreeChanged(@NonNull final Collection<DataTreeModification<Keystore>> changes) {
117             changes.stream().map(DataTreeModification::getRootNode)
118                 .flatMap(v -> v.getModifiedChildren().stream())
119                 .filter(v -> v.getDataType().equals(TrustedCertificate.class))
120                 .map(v -> (DataObjectModification<TrustedCertificate>) v)
121                 .forEach(this::updateCertificate);
122         }
123
124         private void updateCertificate(final DataObjectModification<TrustedCertificate> change) {
125             switch (change.getModificationType()) {
126                 case DELETE:
127                     deleteCertificate(change.getDataBefore());
128                     break;
129                 case SUBTREE_MODIFIED:
130                 case WRITE:
131                     deleteCertificate(change.getDataBefore());
132                     writeCertificate(change.getDataAfter());
133                     break;
134                 default:
135                     break;
136             }
137         }
138
139         private void deleteCertificate(final TrustedCertificate dataBefore) {
140             if (dataBefore != null) {
141                 LOG.debug("Removing public key mapping for certificate {}", dataBefore.getName());
142                 certificateToPublicKey.remove(dataBefore.getName());
143             }
144         }
145
146         private void writeCertificate(final TrustedCertificate dataAfter) {
147             if (dataAfter != null) {
148                 LOG.debug("Adding public key mapping for certificate {}", dataAfter.getName());
149                 certificateToPublicKey.putIfAbsent(dataAfter.getName(), buildPublicKey(dataAfter.getCertificate()));
150             }
151         }
152
153         private PublicKey buildPublicKey(final String encoded) {
154             final byte[] decoded = Base64.getMimeDecoder().decode(encoded.getBytes(US_ASCII));
155             try {
156                 final CertificateFactory factory = CertificateFactory.getInstance("X.509");
157                 try (InputStream in = new ByteArrayInputStream(decoded)) {
158                     return factory.generateCertificate(in).getPublicKey();
159                 }
160             } catch (final CertificateException | IOException e) {
161                 LOG.error("Unable to build X.509 certificate from encoded value: {}", e.getLocalizedMessage());
162             }
163             return null;
164         }
165     }
166
167     private final class AllowedDevicesMonitor implements ClusteredDataTreeChangeListener<Device> {
168         @Override
169         public void onDataTreeChanged(final Collection<DataTreeModification<Device>> mods) {
170             for (final DataTreeModification<Device> dataTreeModification : mods) {
171                 final DataObjectModification<Device> deviceMod = dataTreeModification.getRootNode();
172                 switch (deviceMod.getModificationType()) {
173                     case DELETE:
174                         deleteDevice(deviceMod.getDataBefore());
175                         break;
176                     case SUBTREE_MODIFIED:
177                     case WRITE:
178                         deleteDevice(deviceMod.getDataBefore());
179                         writeDevice(deviceMod.getDataAfter());
180                         break;
181                     default:
182                         break;
183                 }
184             }
185         }
186
187         private void deleteDevice(final Device dataBefore) {
188             if (dataBefore != null && dataBefore.getTransport() instanceof Tls) {
189                 LOG.debug("Removing device {}", dataBefore.getUniqueId());
190                 deviceToPrivateKey.remove(dataBefore.getUniqueId());
191                 deviceToCertificate.remove(dataBefore.getUniqueId());
192             }
193         }
194
195         private void writeDevice(final Device dataAfter) {
196             if (dataAfter != null && dataAfter.getTransport() instanceof Tls tls) {
197                 LOG.debug("Adding device {}", dataAfter.getUniqueId());
198                 final var tlsClientParams = tls.getTlsClientParams();
199                 deviceToPrivateKey.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getKeyId());
200                 deviceToCertificate.putIfAbsent(dataAfter.getUniqueId(), tlsClientParams.getCertificateId());
201             }
202         }
203     }
204 }